// ==UserScript== // @name MZ - Training History // @namespace douglaskampl // @version 5.1 // @description Displays skill gains across 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/history.css // @run-at document-idle // @license MIT // @downloadURL none // ==/UserScript== (() => { 'use strict'; GM_addStyle(GM_getResourceText('trainingHistoryStyles')); 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, }; 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 SKILL_TRANSLATIONS = { 'Szybkość': 'Speed', 'Kondycja': 'Stamina', 'Przegląd gry': 'Play Intelligence', 'Podania dołem': 'Passing', 'Podania doÅem': 'Passing', 'Strzał': 'Shooting', 'Gra głową': 'Heading', 'Gra na bramce': 'Keeping', 'Panowanie': 'Ball Control', 'Odbiór piłki': 'Tackling', 'Podania górą': 'Aerial Passing', 'Stałe fragmenty': 'Set Plays', 'Velocidade': 'Speed', 'Resistência': 'Stamina', 'Inteligência': 'Play Intelligence', 'Inteligên. Jogo': 'Play Intelligence', 'Passe Curto': 'Passing', 'Passes Curtos': 'Passing', 'Chute': 'Shooting', 'Remate': 'Shooting', 'Cabeceio': 'Heading', 'Cabeceamento': 'Heading', 'Defesa a Gol': 'Keeping', 'Guarda-Redes': 'Keeping', 'Controle de Bola': 'Ball Control', 'Controlo Bola': 'Ball Control', 'Desarme': 'Tackling', 'Passe Longo': 'Aerial Passing', 'Passes Longos': 'Aerial Passing', 'Bola Parada': 'Set Plays', 'Bolas Paradas': 'Set Plays', 'Velocidad': 'Speed', 'Resistencia': 'Stamina', 'Inteligencia': 'Play Intelligence', 'Pases': 'Passing', 'Remates': 'Shooting', 'Cabezazos': 'Heading', 'Atajando': 'Keeping', 'Control de balón': 'Ball Control', 'Entradas': 'Tackling', 'Pases Largos': 'Aerial Passing', 'Balón Parado': 'Set Plays', '速度': 'Speed', '耐力': 'Stamina', '意识': 'Play Intelligence', '传球': 'Passing', '射门': 'Shooting', '头球': 'Heading', '守门': 'Keeping', '控球': 'Ball Control', '抢断': 'Tackling', '传中': 'Aerial Passing', '定位': 'Set Plays', 'Gyorsaság': 'Speed', 'Állóképesség': 'Stamina', 'Játékint.': 'Play Intelligence', 'Átadás': 'Passing', 'Lövés': 'Shooting', 'Fejelés': 'Heading', 'Védés': 'Keeping', 'Labdakezelés': 'Ball Control', 'Szerelés': 'Tackling', 'Ívelés': 'Aerial Passing', 'Pontrúgások': 'Set Plays', 'Hız': 'Speed', 'Dayanıklılık': 'Stamina', 'Oyun zekası': 'Play Intelligence', 'Paslaşma': 'Passing', 'Şut çekme': 'Shooting', 'Kafa vuruşu': 'Heading', 'Kalecilik': 'Keeping', 'Top kontrolü': 'Ball Control', 'Top çalma': 'Tackling', 'Orta yapma': 'Aerial Passing', 'Duran top': 'Set Plays', 'Скорост': 'Speed', 'Издръжливост': 'Stamina', 'Разбиране на играта': 'Play Intelligence', 'Подаване': 'Passing', 'Стрелба': 'Shooting', 'Игра с глава': 'Heading', 'Пазене': 'Keeping', 'Техника': 'Ball Control', 'Отнемане': 'Tackling', 'Подаване по въздух': 'Aerial Passing', 'Статични положения': 'Set Plays', 'Snabbhet': 'Speed', 'Kondition': 'Stamina', 'Speluppf.': 'Play Intelligence', 'Passning': 'Passing', 'Skott': 'Shooting', 'Nick': 'Heading', 'Målvakt': 'Keeping', 'Teknik': 'Ball Control', 'Tackling': 'Tackling', 'Inlägg': 'Aerial Passing', 'Fasta Sit.': 'Set Plays', 'Hurtighed': 'Speed', 'Udholdenhed': 'Stamina', 'Spilforståelse': 'Play Intelligence', 'Afleveringer': 'Passing', 'Skud': 'Shooting', 'Hovedstød': 'Heading', 'Målmandsspil': 'Keeping', 'Teknik': 'Ball Control', 'Tackling': 'Tackling', 'Tværaflevering': 'Aerial Passing', 'Dødbolde': 'Set Plays', 'Hitrost': 'Speed', 'Kondicija': 'Stamina', 'Inteligenca': 'Play Intelligence', 'Podajanje': 'Passing', 'Streljanje': 'Shooting', 'Igra z glavo': 'Heading', 'Branjenje': 'Keeping', 'Kontrola žoge': 'Ball Control', 'Odvzem žoge': 'Tackling', 'Dolge podaje': 'Aerial Passing', 'Prosti streli': 'Set Plays', 'Kecepatan': 'Speed', 'Stamina': 'Stamina', 'Intelegensia Permainan': 'Play Intelligence', 'Operan': 'Passing', 'Tembakan': 'Shooting', 'Sundulan': 'Heading', 'Tangkapan': 'Keeping', 'Kontrol Bola': 'Ball Control', 'Tekel': 'Tackling', 'Umpan Silang/Daerah': 'Aerial Passing', 'Bola Mati': 'Set Plays', 'Скорость': 'Speed', 'Выносливость': 'Stamina', 'Игровой интеллект': 'Play Intelligence', 'Пас': 'Passing', 'Удар': 'Shooting', 'Игра головой': 'Heading', 'Вратарство': 'Keeping', 'Техника': 'Ball Control', 'Отбор': 'Tackling', 'Длинный пас': 'Aerial Passing', 'Стандарты': 'Set Plays', 'Snelheid': 'Speed', 'Uithouding': 'Stamina', 'Spelintelligentie': 'Play Intelligence', 'Passen': 'Passing', 'Schieten': 'Shooting', 'Koppen': 'Heading', 'Keepen': 'Keeping', 'Balcontrole': 'Ball Control', 'Tackelen': 'Tackling', 'Lange passen': 'Aerial Passing', 'Spelhervattingen': 'Set Plays', 'Velocità': 'Speed', 'Resistenza': 'Stamina', 'Intelligenza G.': 'Play Intelligence', 'Passaggio': 'Passing', 'Tiro': 'Shooting', 'Colpo di testa': 'Heading', 'Parare': 'Keeping', 'Controllo Palla': 'Ball Control', 'Contrasto': 'Tackling', 'Cross': 'Aerial Passing', 'Azioni da fermo': 'Set Plays', 'Experiencia': 'Experience', 'Estado físico': 'Form', 'Tapasztalat': 'Experience', 'Forma': 'Form', 'Tecrübe': 'Experience', 'Form': 'Form', 'Опит': 'Experience', 'Rutin': 'Experience', 'Erfaring': 'Experience', 'Izkušenost': 'Experience', 'Pengalaman': 'Experience', 'Опыт': 'Experience', 'Ervaring': 'Experience', 'Esperienza': 'Experience', }; const insensitiveSkillTranslations = Object.fromEntries( Object.entries(SKILL_TRANSLATIONS).map(([key, value]) => [key.toLowerCase(), value]) ); const getEnglishSkillName = (nativeName) => { if (!nativeName) return ''; return insensitiveSkillTranslations[nativeName.toLowerCase()] || nativeName; }; let myTeamId = null; let preferredCurrency = GM_getValue('PREFERRED_CURRENCY', 'USD'); 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 = {}; 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 d = new Date(s.data[i + 1].x); const sea = getSeasonFn(d); if (!bySeason[sea]) bySeason[sea] = []; const sid = s.data[i + 1].y.toString(); const sk = SKILL_MAP[sid] || 'Unknown'; bySeason[sea].push({ dateString: d.toLocaleDateString(), skillName: sk }); if (!skillTotals[sk]) skillTotals[sk] = 0; skillTotals[sk]++; total++; if (sea < earliest) earliest = sea; } }); }); } return { bySeason, skillTotals, total, earliestSeason: earliest, chips, chipsBySeason }; }; 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, container) => { const out = []; const currentMap = {}; const currentAge = parsePlayerAge(container); 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 } = 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 = ''; 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 = ``; } else if (scoutData.lp > 0 && scoutData.lpPotentialIndices?.includes(idx)) { potentialClass = ` th-skill-potential-lp${scoutData.lp}`; potentialIcon = ``; } } if (prevMap) { const prevVal = prevMap[k] || 0; const change = v - prevVal; if (change > 0) { changeHTML = `(+${change})`; totalIncreaseFromPrev += change; } } const baseSkillRowStartHtml = `
${potentialIcon}${k}
`; comparisonHtml += `${baseSkillRowStartHtml}
(${v})
${changeHTML}
`; if (arrivalMap && isCurrentState) { const arrivalVal = arrivalMap[k] || 0; const gainSinceArrival = v - arrivalVal; initialBallsVizHTML = `${arrivalVal > 0 ? '●'.repeat(arrivalVal) : ''}`; if (gainSinceArrival > 0) { gainSinceArrivalTextHTML = `(+${gainSinceArrival})`; totalGainSinceArrival += gainSinceArrival; gainedBallsVizHTML = `${'●'.repeat(gainSinceArrival)}`; } else { gainSinceArrivalTextHTML = ''; gainedBallsVizHTML = ''; } arrivalGainHtml += `${baseSkillRowStartHtml}
${initialBallsVizHTML} ${gainedBallsVizHTML}
${gainSinceArrivalTextHTML}
`; } }); 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, container, currentSeason) => { const { bySeason, total, skillTotals, chips, chipsBySeason, earliestSeason } = processedData; let html = ''; const currentAge = parsePlayerAge(container); 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 = '
Chips: '; seasonChipsHtml += chipsBySeason[se].map(chip => { const simplifiedName = chip.name.split('')[1]?.trim() || chip.name; return `${simplifiedName} (${chip.dateString})`; }).join(', '); seasonChipsHtml += '
'; } html += `

${label} — ${items.length} Ball${items.length !== 1 ? 's' : ''} Earned

${seasonChipsHtml}
'; }); html += `

Total balls earned: ${total}

`; const fs = Object.entries(skillTotals) .filter(([, count]) => count > 0) .sort(([, countA], [, countB]) => countB - countA) .map(([skill, count]) => `${skill} (${count})`) .join(', '); html += `

${fs}

`; const allChipsSorted = [...chips].sort((a, b) => a.date - b.date); if (allChipsSorted.length > 0) { html += `

All Applied Chips

`; } return html; }; const buildStatesLayout = (processedData, container, currentSeason, scoutData, transferData) => { const { bySeason, skillTotals, earliestSeason, chipsBySeason } = processedData; const finalMap = gatherCurrentSkills(container); const seasonGains = fillSeasonGains(bySeason, earliestSeason, currentSeason, skillTotals); const arr = buildSeasonCheckpointData(earliestSeason, currentSeason, finalMap, seasonGains, container); const arrivalMap = arr.length > 0 ? arr[0].distribution : null; let paginatedHtml = '
'; let allViewHtml = ''; allViewHtml += '
'; 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 += `${firstHpSkill}`; if (secondHpSkill) hpSkillsText += `/${secondHpSkill}`; } hpSkillsText = hpSkillsText ? ` ${hpSkillsText}` : ''; let lpSkillsText = ''; if (lp > 0 && firstLpSkill) { lpSkillsText += `${firstLpSkill}`; if (secondLpSkill) lpSkillsText += `/${secondLpSkill}`; } lpSkillsText = lpSkillsText ? ` ${lpSkillsText}` : ''; scoutHtml = `
TrainingSpeed ${speedText} | ${hpText}${hpSkillsText} | ${lpText}${lpSkillsText}
`; } return scoutHtml + paginatedHtml + allViewHtml; }; const generateTabsHTML = (name, evo, st) => `

${name}

1 / 1
${evo}
${st}
`; 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) { console.error("Failed to fetch or parse scout data:", error); return null; } }; const fetchTransferData = async (pid, getSeasonFn) => { try { const response = await fetch(`https://www.managerzone.com/?p=players&pid=${pid}`); if (!response.ok) throw new Error(`HTTP error ${response.status}`); const text = await response.text(); const doc = new DOMParser().parseFromString(text, 'text/html'); const transferTable = doc.querySelector('table.hitlist'); const transferDataHeader = transferTable?.querySelector('thead td strong'); if (!transferTable || !transferDataHeader || !transferDataHeader.textContent.match(/Data|Date/i)) { return {}; } const transfersBySeason = {}; const rows = transferTable.querySelectorAll('tbody tr'); rows.forEach(row => { const cells = row.querySelectorAll('td'); if (cells.length < 5) return; const dateStrRaw = cells[0]?.textContent.trim(); const actionOrFromTeamCell = cells[1]; const toTeamCell = cells[3]; const priceCell = cells[4]; const dateMatch = dateStrRaw.match(/(\d{2})-(\d{2})-(\d{4})/); if (!dateMatch) return; const day = dateMatch[1]; const month = dateMatch[2]; const year = dateMatch[3]; 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 if (actionOrFromTeamCell.textContent.trim() !== '-') { fromTeamName = actionOrFromTeamCell.textContent.trim(); } 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) { console.error("Failed to fetch or parse transfer data:", error); return {}; } }; 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); try { 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, cont, curSeason); const stHTML = buildStatesLayout(processedTrainingData, cont, curSeason, scoutData, transferData); const finalHTML = generateTabsHTML(nm, evoHTML, stHTML); modal.querySelector('.th-modal-content').innerHTML = finalHTML; setTimeout(() => { attachTabEvents(modal); initPaginationState(modal); }, 50); } catch (error) { console.error("Error fetching combined player data:", error); if (spinnerInstance) spinnerInstance.stop(); if(spinnerEl) spinnerEl.style.display = 'none'; const contentDiv = modal.querySelector('.th-modal-content'); if (contentDiv) { contentDiv.innerHTML = `

Failed to process player data. (${error.message || 'Unknown error'})

(Are you a club member? Is the player still active? Check console for details.)

`; } } }; const insertButtons = (getSeasonFn, csi) => { const containers = document.querySelectorAll('.playerContainer'); containers.forEach(cc => { if (!hasVisibleSkills(cc)) return; const f = cc.querySelectorAll('.floatRight[id^="player_id_"]'); f.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; const b = document.createElement('button'); b.className = 'th-btn'; b.innerHTML = ''; 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((error) => {console.error("Failed to fetch team ID:", error)}); }; const run = () => { initTeamId(); if (!canRunUserscript()) { console.log("MZ Training History: Not a Club Member"); return; } const csi = getCurrentSeasonInfo(); if (!csi) { console.error("MZ Training History: Could not determine current season info."); 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(); })();