// ==UserScript== // @name MZ - NT Player Search // @namespace douglaskampl // @version 2.9 // @description Searches for players who match specific requirements // @author Douglas Vieira // @match https://www.managerzone.com/?p=national_teams&type=senior // @match https://www.managerzone.com/?p=national_teams&type=u21 // @icon https://yt3.googleusercontent.com/ytc/AIdro_mDHaJkwjCgyINFM7cdUV2dWPPnL9Q58vUsrhOmRqkatg=s160-c-k-c0x00ffffff-no-rj // @grant GM_xmlhttpRequest // @grant GM_addStyle // @connect mzlive.eu // @connect pub-02de1c06eac643f992bb26daeae5c7a0.r2.dev // @require https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js // @run-at document-idle // @license MIT // @downloadURL none // ==/UserScript== (function () { 'use strict'; GM_addStyle(` @import url('https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap'); #leftmenu_nt_search { margin-top:5px; } .mz-search-open-btn { display:block; padding:2px 4px; color:#000; text-decoration:none; font-size:12px; font-family:Arial,sans-serif; } .mz-search-open-btn:hover { background-color:#f0f0f0; text-decoration:none; } .mz-search-container { position:fixed; top:50%; left:50%; transform:translate(-50%,-50%) scale(.95); background:linear-gradient(135deg,#0a0a0a 0%,#1a1a2e 100%); color:#f0f0f0; padding:2rem; border-radius:12px; box-shadow:0 8px 32px rgba(83,11,237,.3),0 4px 8px rgba(0,0,0,.2); z-index:9999; visibility:hidden; width:800px; max-width:99%; opacity:0; transition:all .3s cubic-bezier(0.4,0,0.2,1); border:1px solid rgba(138,43,226,.1); } .mz-search-container.visible { visibility:visible; opacity:1; transform:translate(-50%,-50%) scale(1) } .mz-search-header { display:flex; justify-content:space-between; align-items:center; margin-bottom:2rem; padding-bottom:1rem; border-bottom:1px solid rgba(138,43,226,.2) } .mz-search-header h2 { font-family:'Space Mono',monospace; margin:0; color:violet; font-size:1.5rem; text-shadow:0 0 10px rgba(138,43,226,.5) } .mz-search-grid { display:grid; grid-template-columns:repeat(4,1fr); gap:1rem; margin-bottom:1.5rem } .mz-search-field { display:flex; flex-direction:column; gap:.5rem } .mz-search-field label { color:#ff9966; font-size:.875rem; text-transform:uppercase; letter-spacing:1px } .mz-search-field select { padding:.75rem; border:1px solid rgba(138,43,226,.3); border-radius:8px; background:#1a1a2e; color:#f0f0f0; font-size:1rem; transition:all .2s; appearance:none; background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23ff9966' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E"); background-repeat:no-repeat; background-position:right .75rem center; background-size:1rem; } .mz-search-field select:focus { outline:none; border-color:#ff9966; box-shadow:0 0 0 2px rgba(138,43,226,.2) } .mz-search-button { width:auto; max-width:300px; padding:0.5rem 1rem; background:#009b3a; color:#ffdf00; border:none; border-radius:8px; font-weight:500; font-size:0.9rem; cursor:pointer; transition:all .2s; text-transform:uppercase; letter-spacing:2px; box-shadow:0 4px 6px rgba(0,0,0,.1); display:block; margin:1rem auto; } .mz-search-button:not(:disabled):hover { transform:translateY(-2px); box-shadow:0 6px 8px rgba(0,0,0,.2) } .mz-search-button:disabled { opacity:0.5; cursor:not-allowed; background:#666 } .mz-progress { margin-top:1.5rem; padding:1rem; background:rgba(26,26,46,.5); border-radius:8px; visibility:hidden; opacity:0; transition:all .3s } .mz-progress.visible { visibility:visible; opacity:1 } .mz-progress-info { display:flex; justify-content:space-between; align-items:center; margin-bottom:1rem; color:#ff9966; font-size:.875rem } .mz-progress-bar { width:100%; height:6px; background:#1a1a2e; border-radius:3px; overflow:hidden } .mz-progress-fill { height:100%; width:0; background:linear-gradient(135deg,#4834d4 0%,#6366f1 100%); transition:width .3s ease-out } .mz-search-log { margin-top:1rem; padding:1rem; background:rgba(26,26,46,.3); border-radius:8px; font-family:monospace; font-size:.875rem; max-height:150px; overflow-y:auto; scrollbar-width:thin; scrollbar-color:#6366f1 #1a1a2e; } .mz-search-log::-webkit-scrollbar { width:8px; height:8px } .mz-search-log::-webkit-scrollbar-track { background:#1a1a2e; border-radius:4px } .mz-search-log::-webkit-scrollbar-thumb { background:#6366f1; border-radius:4px } .mz-search-log::-webkit-scrollbar-thumb:hover { background:#4834d4 } .mz-search-log-entry { margin-bottom:.5rem; padding:.5rem; background:rgba(26,26,46,.5); border-radius:4px; color:#00ffff; animation:slideIn 0.3s ease-out forwards; opacity:0; transform:translateX(-20px) } @keyframes slideIn { from { opacity:0; transform:translateX(-20px) } to { opacity:1; transform:translateX(0) } } .mz-guestbook-link { position:fixed; bottom:1rem; right:1rem; color:#ff9966; transition:all .2s; } .mz-guestbook-link:hover { color:#6366f1; transform:scale(1.1) } .mz-country-select { width:200px; } .mz-country-select select { width:100%; } .mz-loading { position:fixed; top:50%; left:50%; transform:translate(-50%,-50%); background:rgba(10,10,20,.95); padding:1rem; border-radius:8px; z-index:10000; box-shadow:0 8px 32px rgba(83,11,237,.2); visibility:hidden; opacity:0; transition:all .3s; } .mz-loading.visible { visibility:visible; opacity:1 } .mz-spinner { position:relative; width:20px; height:20px; } .mz-spinner::before, .mz-spinner::after { content:''; position:absolute; border-radius:50%; animation:pulse 1.8s ease-in-out infinite; transform-origin:center; } .mz-spinner::before { width:100%; height:100%; background:rgba(99,102,241,.5); animation-delay:-0.9s; transform:scale(0.3); } .mz-spinner::after { width:75%; height:75%; background:rgba(99,102,241,.8); top:12.5%; transform:scale(0.3); } @keyframes pulse { 0%, 100% { transform:scale(0.3); opacity:1 } 50% { transform:scale(0.6); opacity:.25 } } .mz-results-button { width:auto; max-width:300px; padding:0.5rem 1rem; background:#009b3a; color:#ffdf00; border:none; border-radius:8px; font-weight:500; font-size:0.9rem; cursor:pointer; transition:all .2s; text-transform:uppercase; letter-spacing:2px; box-shadow:0 4px 6px rgba(0,0,0,.1); display:none; margin:1rem auto; } .mz-results-button:hover { transform:translateY(-2px); box-shadow:0 6px 8px rgba(0,0,0,.2) } .mz-results-modal { position:fixed; top:50%; left:50%; transform:translate(-50%,-50%); background:linear-gradient(135deg,#0a0a0a 0%,#1a1a2e 100%); color:#f0f0f0; padding:0; border-radius:12px; z-index:10001; width:90%; height:90vh; overflow:hidden; box-shadow:0 8px 32px rgba(83,11,237,.3); animation:modalSlideIn 0.3s ease-out forwards; } @keyframes modalSlideIn { from { opacity:0; transform:translate(-50%,-48%) } to { opacity:1; transform:translate(-50%,-50%) } } .mz-results-header { position:sticky; top:0; display:flex; justify-content:space-between; align-items:center; padding:1.5rem; background:inherit; border-bottom:1px solid rgba(138,43,226,.2); z-index:1; } .mz-results-title { font-family:'Space Mono',monospace; margin:0; font-size:1.5rem; color:#fff; text-shadow:0 0 10px rgba(138,43,226,.5); } .mz-results-close { background:none; border:none; color:#ff9966; font-size:1.5rem; cursor:pointer; transition:all 0.2s; padding:0.5rem; } .mz-results-close:hover { color:#6366f1; transform:scale(1.1) } .mz-results-content { padding:1.5rem; height:calc(90vh - 5rem); overflow-y:auto; scrollbar-width:thin; scrollbar-color:#6366f1 #1a1a2e; } .mz-results-content::-webkit-scrollbar { width:8px } .mz-results-content::-webkit-scrollbar-track { background:#1a1a2e } .mz-results-content::-webkit-scrollbar-thumb { background:#6366f1; border-radius:4px; } .mz-results-content::-webkit-scrollbar-thumb:hover { background:#4834d4 } .mz-player-card { background:rgba(26,26,46,.5); border-radius:8px; margin-bottom:1.5rem; padding:1.5rem; transition:all 0.2s; border:1px solid rgba(138,43,226,.1); } .mz-player-card:hover { transform:translateY(-2px); box-shadow:0 4px 12px rgba(83,11,237,.2) } .mz-player-header { display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:1rem } .mz-player-info { flex:1 } .mz-player-name { font-size:1.25rem; font-weight:bold; color:#fff; margin:0 0 0.5rem 0 } .mz-player-details { display:grid; grid-template-columns:repeat(auto-fit, minmax(200px, 1fr)); gap:1rem; color:#ff9966; font-size:0.875rem; } .mz-player-skills { display:grid; grid-template-columns:repeat(auto-fill, minmax(250px, 1fr)); gap:1rem; margin-top:1rem; } .mz-skill-row { display:flex; align-items:center; padding:0.5rem; background:rgba(26,26,46,.3); border-radius:4px; } .mz-skill-name { flex:1; font-size:0.875rem; color:#f0f0f0 } .mz-skill-value { display:flex; align-items:center; gap:0.5rem; } .mz-skill-level { width:100px; height:8px; background:#1a1a2e; border-radius:4px; overflow:hidden; } .mz-skill-fill { height:100%; background:linear-gradient(135deg,#4834d4 0%,#6366f1 100%); transition:width 0.3s } .mz-skill-number { font-size:0.875rem; color:#ff9966; min-width:2rem; text-align:right } .mz-results-pagination { display:flex; justify-content:center; align-items:center; gap:1rem; margin:1rem 0; padding:1rem; border-bottom:1px solid rgba(138,43,226,.2); } .mz-pagination-button { background:#1a1a2e; color:#f0f0f0; border:1px solid rgba(138,43,226,.3); border-radius:4px; padding:0.5rem 1rem; cursor:pointer; transition:all 0.2s; } .mz-pagination-button:not(:disabled):hover { background:#2a2a4e; transform:translateY(-1px) } .mz-pagination-button:disabled { opacity:0.5; cursor:not-allowed } .mz-pagination-info { color:#ff9966; font-size:0.875rem } .mz-results-total { padding:1rem; text-align:right; color:#888; font-size:0.875rem } .mz-export-button { background:#1a1a2e; color:#f0f0f0; border:1px solid rgba(138,43,226,.3); border-radius:4px; padding:0.5rem 1rem; cursor:pointer; transition:all 0.2s; margin-left:1rem; } .mz-export-button:hover { background:#2a2a4e; transform:translateY(-1px) } .mz-export-button:active { transform:translateY(1px) } .mz-header-controls { display:flex; align-items:center; gap:1rem } `); const MASSIVE_COUNTRIES = ['BR', 'CN', 'AR', 'SE', 'PL', 'TR']; const PLAYERS_PER_PAGE = 20; class Logger { constructor(container, batchSize = 10) { this.container = container; this.batchSize = batchSize; this.queue = []; this.timeout = null; } getTimestamp() { const now = new Date(); return `[${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}]`; } log(message, type = 'info') { this.queue.push({ message: `${this.getTimestamp()} ${message}`, type }); if (this.queue.length >= this.batchSize) { this.flush(); } else if (!this.timeout) { this.timeout = setTimeout(() => this.flush(), 100); } } flush() { if (!this.queue.length) return; const fragment = document.createDocumentFragment(); this.queue.forEach(({ message, type }) => { const entry = document.createElement('div'); entry.className = `mz-search-log-entry ${type}`; entry.textContent = message; fragment.appendChild(entry); }); this.container.appendChild(fragment); this.container.scrollTop = this.container.scrollHeight; this.queue = []; if (this.timeout) { clearTimeout(this.timeout); this.timeout = null; } } } class RequestQueue { constructor(maxConcurrent = 5, delay = 100) { this.queue = []; this.maxConcurrent = maxConcurrent; this.delay = delay; this.running = 0; this.processed = 0; } add(request) { return new Promise((resolve, reject) => { const wrappedRequest = async () => { try { await new Promise(res => setTimeout(res, this.delay)); const result = await request(); this.processed++; resolve(result); } catch (error) { reject(error); } finally { this.running--; this.processNext(); } }; this.queue.push(wrappedRequest); this.processNext(); }); } processNext() { while (this.running < this.maxConcurrent && this.queue.length > 0) { this.running++; const request = this.queue.shift(); request(); } } reset() { this.queue = []; this.running = 0; this.processed = 0; } } class ChunkProcessor { constructor(chunkSize = 25) { this.chunkSize = chunkSize; } async process(items, processFn, onChunkComplete) { const chunks = this.createChunks(items); let processed = 0; for (const chunk of chunks) { await Promise.all(chunk.map(processFn)); processed += chunk.length; if (onChunkComplete) { onChunkComplete(processed, items.length); } await new Promise(res => setTimeout(res, 50)); } } createChunks(items) { const chunks = []; for (let i = 0; i < items.length; i += this.chunkSize) { chunks.push(items.slice(i, i + this.chunkSize)); } return chunks; } } class ProgressManager { constructor(element, throttleMs = 100) { this.element = element; this.throttleMs = throttleMs; this.lastUpdate = 0; this.pendingUpdate = null; } update(percent) { const now = Date.now(); if (now - this.lastUpdate >= this.throttleMs) { this.updateProgress(percent); this.lastUpdate = now; } else if (!this.pendingUpdate) { this.pendingUpdate = setTimeout(() => { this.updateProgress(percent); this.lastUpdate = Date.now(); this.pendingUpdate = null; }, this.throttleMs); } } updateProgress(percent) { if (this.element) { this.element.style.width = `${percent}%`; } } } class NTPlayerParser { constructor(minRequirements) { this.minRequirements = minRequirements; this.skillMapping = { "Speed": "speed", "Stamina": "stamina", "Play Intelligence": "playIntelligence", "Passing": "passing", "Shooting": "shooting", "Heading": "heading", "Keeping": "keeping", "Ball Control": "ballControl", "Tackling": "tackling", "Aerial Passing": "aerialPassing", "Set Plays": "setPlays", "Experience": "experience" }; } parseSkills(html) { const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); const rows = doc.querySelectorAll('.player_skills tr'); if (!rows.length) return null; const skills = {}; let totalBalls = 0; const totalBallsElement = doc.querySelector('td[title="All skills combined except Form and Experience"] span.bold'); if (totalBallsElement) { totalBalls = parseInt(totalBallsElement.textContent, 10) || 0; } rows.forEach(row => { const skillElem = row.querySelector('td span.clippable'); if (!skillElem) return; const rawSkillName = skillElem.textContent.trim(); if (rawSkillName === "Form") return; if (!(rawSkillName in this.skillMapping)) return; const skillKey = this.skillMapping[rawSkillName]; const valueCell = row.querySelector('.skillval'); if (!valueCell) return; const rawValue = valueCell.textContent.replace(/[()]/g, "").trim(); const value = parseInt(rawValue, 10); if (!isNaN(value)) { skills[skillKey] = value; } }); if (Object.keys(skills).length === 0) return null; if (!this.validateSkills(skills)) return null; return { skills, totalBalls }; } validateSkills(skills) { return Object.entries(this.minRequirements) .filter(([key]) => key in skills) .every(([key, minValue]) => skills[key] >= minValue); } async fetchAndParsePlayer(playerId, ntid, cid) { const url = `https://www.managerzone.com/ajax.php?p=nationalTeams&sub=search&ntid=${ntid}&cid=${cid}&type=national_team&pid=${playerId}&sport=soccer`; try { const response = await fetch(url); const html = await response.text(); return this.parseSkills(html); } catch (error) { return null; } } } class PlayerData { constructor(id, name, teamName, teamId, age, value, salary, totalBalls, skills) { this.id = id; this.name = name; this.teamName = teamName; this.teamId = teamId || null; this.age = age; this.value = value; this.salary = salary; this.totalBalls = totalBalls; this.skills = skills; } toExcelRow() { return { 'ID': this.id, 'Name': this.name, 'Team': this.teamName, 'Age': this.age, 'Value': this.value, 'Salary': this.salary, 'Total Balls': this.totalBalls, 'Speed': this.skills.speed || 0, 'Stamina': this.skills.stamina || 0, 'Play Intelligence': this.skills.playIntelligence || 0, 'Short Passing': this.skills.passing || 0, 'Shooting': this.skills.shooting || 0, 'Heading': this.skills.heading || 0, 'Keeping': this.skills.keeping || 0, 'Ball Control': this.skills.ballControl || 0, 'Tackling': this.skills.tackling || 0, 'Aerial Passing': this.skills.aerialPassing || 0, 'Set Plays': this.skills.setPlays || 0, 'Experience': this.skills.experience || 0 }; } } class NTPlayerSearcher { constructor() { this.requestQueue = new RequestQueue(5, 100); this.chunkProcessor = new ChunkProcessor(25); this.searchValues = { speed: 0, stamina: 0, playIntelligence: 0, passing: 0, shooting: 0, heading: 0, keeping: 0, ballControl: 0, tackling: 0, aerialPassing: 0, setPlays: 0, experience: 0, minAge: 16, maxAge: 96, totalBalls: 9, country: '', countryData: null }; this.isSearching = false; this.teamIds = new Set(); this.playerIds = new Map(); this.processedLeagues = 0; this.totalLeagues = 0; this.validPlayers = new Map(); } async fetchTopPlayers(country, page = 0, isU21 = false) { try { const baseUrl = `https://mzlive.eu/mzlive.php?action=list&type=top100&mode=players&country=${country}¤cy=EUR`; const url = isU21 ? `${baseUrl}&age=u21&page=${page}` : `${baseUrl}&page=${page}`; const response = await this.requestQueue.add(() => new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url, onload: res => resolve(res), onerror: err => reject(err) }); }) ); const data = JSON.parse(response.responseText); const players = data.players || []; const playerEntries = players.map(player => [ player.id.toString(), { id: player.id.toString(), name: player.name, teamName: player.team_name, teamId: player.team_id?.toString() || null, age: player.age, value: parseInt(player.value), salary: 0 } ]); this.playerIds = new Map([...this.playerIds, ...playerEntries]); return players.map(player => player.id.toString()); } catch (error) { this.logger.log(`Error fetching top 100 players: ${error.message}`, 'error'); return []; } } async fetchAllTop100Players(country) { const maxPages = MASSIVE_COUNTRIES.includes(country) ? 20 : 5; const isU21 = this.searchValues.maxAge <= 21; const pages = Array.from({ length: maxPages + 1 }, (_, i) => i); const chunkSize = 5; const results = []; for (let i = 0; i < pages.length; i += chunkSize) { const chunk = pages.slice(i, i + chunkSize); const chunkResults = await Promise.all( chunk.map(page => this.fetchTopPlayers(country, page, isU21)) ); results.push(...chunkResults); await new Promise(res => setTimeout(res, 100)); } return results.flat(); } async fetchCountriesList() { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: 'https://pub-02de1c06eac643f992bb26daeae5c7a0.r2.dev/json/countries.json', onload: res => resolve(JSON.parse(res.responseText)), onerror: err => reject(err) }); }); } async fetchUserCountry() { const usernameElem = document.querySelector('#header-username'); if (!usernameElem) return null; const username = usernameElem.textContent.trim(); const response = await fetch(`https://www.managerzone.com/xml/manager_data.php?sport_id=1&username=${username}`); const text = await response.text(); const parser = new DOMParser(); const xmlDoc = parser.parseFromString(text, "text/xml"); return xmlDoc.querySelector('UserData')?.getAttribute('countryShortname') || null; } async init() { const loading = this.showLoading(); loading.classList.add('visible'); try { const [countries, userCountry] = await Promise.all([ this.fetchCountriesList(), this.fetchUserCountry() ]); this.countries = countries; this.userCountry = userCountry; this.searchValues.country = userCountry; await this.appendSearchTab(); this.setUpEvents(); const logContainer = document.querySelector('.mz-search-log'); this.logger = new Logger(logContainer); const progressBar = document.querySelector('.mz-progress-fill'); this.progressManager = new ProgressManager(progressBar); } finally { loading.classList.remove('visible'); setTimeout(() => loading.remove(), 300); } } showLoading() { const loading = document.createElement('div'); loading.className = 'mz-loading'; const spinner = document.createElement('div'); spinner.className = 'mz-spinner'; loading.appendChild(spinner); document.body.appendChild(loading); return loading; } async getLeagueIds(countryCode) { try { const response = await this.requestQueue.add(() => new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: `https://mzlive.eu/mzlive.php?action=list&type=leagues&country=${countryCode}`, onload: res => resolve(res), onerror: err => reject(err) }); }) ); const leagues = JSON.parse(response.responseText); const maxDivision = MASSIVE_COUNTRIES.includes(countryCode) ? 6 : 3; return leagues.filter(league => { const name = league.name.toLowerCase(); if (name.startsWith('div')) { const divLevel = parseInt(name.split('.')[0].replace('div', '')); return divLevel <= maxDivision; } return true; }).map(league => league.id); } catch (error) { this.logger.log(`Error fetching leagues: ${error.message}`, 'error'); throw error; } } async getTeamIds(leagueId) { try { const response = await this.requestQueue.add(() => fetch(`https://www.managerzone.com/xml/team_league.php?sport_id=1&league_id=${leagueId}`) ); const text = await response.text(); const parser = new DOMParser(); const xmlDoc = parser.parseFromString(text, "text/xml"); const teams = xmlDoc.getElementsByTagName('Team'); return Array.from(teams).map(team => team.getAttribute('teamId')); } catch (error) { this.logger.log(`Error fetching teams for league ${leagueId}: ${error.message}`, 'error'); return []; } } async processLeagueBatch(leagueIds) { await this.chunkProcessor.process( leagueIds, async (leagueId) => { try { const teamIds = await this.getTeamIds(leagueId); teamIds.forEach(id => this.teamIds.add(id)); this.processedLeagues++; this.logger.log(`Processed league ${leagueId}`); } catch (error) { this.logger.log(`Failed to process league ${leagueId}: ${error}`, 'error'); } }, (processed, total) => { const progressPercent = (processed / total) * 100; this.progressManager.update(progressPercent); } ); } async fetchPlayerList(teamId) { try { const response = await this.requestQueue.add(() => fetch(`https://www.managerzone.com/xml/team_playerlist.php?sport_id=1&team_id=${teamId}`) ); const text = await response.text(); const parser = new DOMParser(); const xmlDoc = parser.parseFromString(text, "text/xml"); const teamPlayers = xmlDoc.querySelector('TeamPlayers'); const teamName = teamPlayers?.getAttribute('teamName') || ''; const actualTeamId = teamPlayers?.getAttribute('teamId') || teamId; const players = xmlDoc.getElementsByTagName('Player'); const targetCountry = this.searchValues.country.toLowerCase(); const validPlayers = Array.from(players).filter(player => { const age = parseInt(player.getAttribute('age')); const countryCode = player.getAttribute('countryShortname').toLowerCase(); return age >= this.searchValues.minAge && age <= this.searchValues.maxAge && countryCode === targetCountry; }); validPlayers.forEach(player => { const playerId = player.getAttribute('id'); const playerName = player.getAttribute('name'); const value = parseInt(player.getAttribute('value')) || 0; const salary = parseInt(player.getAttribute('salary')) || 0; const age = parseInt(player.getAttribute('age')); if (playerId) { this.playerIds.set(playerId, { id: playerId, name: playerName, teamName: teamName, teamId: actualTeamId, age: age, value: value, salary: salary }); } }); } catch (error) { this.logger.log(`Error fetching players for team ${teamId}: ${error.message}`, 'error'); } } async processTeamBatch(teamIds) { await this.chunkProcessor.process( teamIds, async (teamId) => { await this.fetchPlayerList(teamId); this.logger.log(`Processed team ${teamId}`); } ); } async searchForPlayers() { if (!this.searchValues.country) { this.logger.log('No country selected', 'error'); return; } this.teamIds = new Set(); this.playerIds = new Map(); this.processedLeagues = 0; this.validPlayers = new Map(); const countryCode = this.searchValues.country; this.logger.log(`Starting search for country ${countryCode}`); try { if (this.searchValues.maxAge > 18) { await this.fetchAllTop100Players(countryCode); this.logger.log(`Found ${this.playerIds.size} players from MZLists`); } const leagueIds = await this.getLeagueIds(countryCode); this.totalLeagues = leagueIds.length; this.logger.log(`Found ${leagueIds.length} leagues to process`); await this.processLeagueBatch(leagueIds); this.logger.log('Processing teams...'); await this.processTeamBatch(Array.from(this.teamIds)); const ntPlayerParser = new NTPlayerParser(this.searchValues); const { ntid, cid } = this.searchValues.countryData; this.logger.log('Processing player skills...'); const playerEntries = Array.from(this.playerIds.entries()); let processedCount = 0; await this.chunkProcessor.process( playerEntries, async ([playerId, playerData]) => { const parsedData = await ntPlayerParser.fetchAndParsePlayer(playerId, ntid, cid); if (parsedData && parsedData.totalBalls >= this.searchValues.totalBalls) { this.validPlayers.set(playerId, new PlayerData( playerId, playerData.name, playerData.teamName, playerData.teamId, playerData.age, playerData.value, playerData.salary, parsedData.totalBalls, parsedData.skills )); this.logger.log(`Player ${playerData.name} (${playerId}) meets the specified requirements`); } processedCount++; if (processedCount % 10 === 0) { this.progressManager.update((processedCount / playerEntries.length) * 100); } } ); this.logger.log('Finishing…'); await new Promise(resolve => setTimeout(resolve, 500)); const finalCount = this.validPlayers.size; this.logger.log(`Done: found ${finalCount} valid players`); document.querySelector('.mz-results-button').style.display = finalCount > 0 ? "inline-block" : "none"; return Array.from(this.validPlayers.keys()); } catch (error) { this.logger.log(`Error during search: ${error.message}`, 'error'); console.error('Search failed:', error); throw error; } } async performSearch() { if (this.isSearching || !this.searchValues.country) return; this.isSearching = true; const internalSearchButton = document.querySelector('.mz-search-container .mz-search-button'); internalSearchButton.disabled = true; const loading = this.showLoading(); const progress = document.querySelector('.mz-progress'); const logContainer = document.querySelector('.mz-search-log'); const resultsButton = document.querySelector('.mz-results-button'); loading.classList.add('visible'); progress.classList.add('visible'); logContainer.innerHTML = ''; resultsButton.style.display = 'none'; try { await this.searchForPlayers(); } catch (error) { this.logger.log(`Error during search: ${error.message}`, 'error'); console.error('Search failed:', error); } finally { this.isSearching = false; internalSearchButton.disabled = false; progress.classList.remove('visible'); loading.classList.remove('visible'); setTimeout(() => loading.remove(), 300); } } getFiltersAppliedText() { const filters = []; if (this.searchValues.country) { filters.push(`Country: ${this.searchValues.country}`); } filters.push(`Age: ${this.searchValues.minAge} - ${this.searchValues.maxAge}`); filters.push(`Total Balls >= ${this.searchValues.totalBalls}`); const skillFields = [ 'speed', 'stamina', 'playIntelligence', 'passing', 'shooting', 'heading', 'keeping', 'ballControl', 'tackling', 'aerialPassing', 'setPlays', 'experience' ]; skillFields.forEach(skill => { if (this.searchValues[skill] > 0) { filters.push(`${this.formatSkillName(skill)} >= ${this.searchValues[skill]}`); } }); return filters.join(', '); } showResults() { if (this.validPlayers.size === 0) { alert("No valid players found."); return; } const modal = document.createElement('div'); modal.className = 'mz-results-modal'; const modalHeader = document.createElement('div'); modalHeader.className = 'mz-results-header'; const headerControls = document.createElement('div'); headerControls.className = 'mz-header-controls'; const closeButton = document.createElement('button'); closeButton.className = 'mz-results-close'; closeButton.innerHTML = '×'; const exportButton = document.createElement('button'); exportButton.className = 'mz-export-button'; exportButton.textContent = 'Export'; exportButton.onclick = () => this.exportToExcel(); headerControls.appendChild(exportButton); headerControls.appendChild(closeButton); modalHeader.innerHTML = `

Search Results

Total players found: ${this.validPlayers.size}
Filters applied: ${this.getFiltersAppliedText()}
`; modalHeader.appendChild(headerControls); const modalContent = document.createElement('div'); modalContent.className = 'mz-results-content'; const playersContainer = document.createElement('div'); playersContainer.className = 'mz-players-container'; const paginationContainer = document.createElement('div'); paginationContainer.className = 'mz-results-pagination'; const playersArray = Array.from(this.validPlayers.values()) .sort((a, b) => b.totalBalls - a.totalBalls); let currentPage = 1; const totalPages = Math.ceil(playersArray.length / PLAYERS_PER_PAGE); const renderPage = (page) => { playersContainer.innerHTML = ''; const startIndex = (page - 1) * PLAYERS_PER_PAGE; const pagePlayers = playersArray.slice(startIndex, startIndex + PLAYERS_PER_PAGE); const paginationMarkup = totalPages > 1 ? ` Page ${page} of ${totalPages} ` : ''; paginationContainer.innerHTML = paginationMarkup; if (totalPages > 1) { const prevBtn = paginationContainer.querySelector('[data-action="prev"]'); const nextBtn = paginationContainer.querySelector('[data-action="next"]'); if (prevBtn) { prevBtn.addEventListener('click', () => { if (currentPage > 1) { currentPage--; renderPage(currentPage); } }); } if (nextBtn) { nextBtn.addEventListener('click', () => { if (currentPage < totalPages) { currentPage++; renderPage(currentPage); } }); } } pagePlayers.forEach(player => { const skillBars = Object.entries(player.skills) .map(([skill, value]) => `
${this.formatSkillName(skill)}
${value}
`).join(''); const playerCard = document.createElement('div'); playerCard.className = 'mz-player-card'; playerCard.innerHTML = `

${player.name} (${player.id})

Team: ${player.teamId ? `${player.teamName}` : player.teamName}
Age: ${player.age}
Value: ${new Intl.NumberFormat('en-US').format(player.value)} USD
Total Balls: ${player.totalBalls}
${skillBars}
`; playersContainer.appendChild(playerCard); }); }; modalContent.appendChild(paginationContainer); modalContent.appendChild(playersContainer); modal.appendChild(modalHeader); modal.appendChild(modalContent); document.body.appendChild(modal); renderPage(currentPage); closeButton.addEventListener('click', () => { modal.remove(); }); document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { modal.remove(); } }); } formatSkillName(skill) { const names = { speed: 'Speed', stamina: 'Stamina', playIntelligence: 'Play Intelligence', passing: 'Short Passing', shooting: 'Shooting', heading: 'Heading', keeping: 'Keeping', ballControl: 'Ball Control', tackling: 'Tackling', aerialPassing: 'Aerial Passing', setPlays: 'Set Plays', experience: 'Experience' }; return names[skill] || skill; } exportToExcel() { if (this.validPlayers.size === 0) return; const worksheet = XLSX.utils.json_to_sheet( Array.from(this.validPlayers.values()).map(player => player.toExcelRow()) ); const workbook = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, worksheet, "Players"); const date = new Date().toISOString().split('T')[0]; XLSX.writeFile(workbook, `mz_players_${date}.xlsx`); } async appendSearchTab() { const leftNav = document.querySelector('ul.leftnav'); if (!leftNav) { console.error('Left navigation menu not found'); return false; } const menuItem = document.createElement('li'); menuItem.id = 'leftmenu_nt_search'; const openButton = document.createElement('a'); openButton.href = '#'; openButton.className = 'mz-search-open-btn'; openButton.innerHTML = 'NT Player Search '; menuItem.appendChild(openButton); leftNav.appendChild(menuItem); const searchContainer = document.createElement('div'); searchContainer.className = 'mz-search-container'; const 씨발 = k => ({ go: { en: 'Go', pt: 'Buscar', es: 'Ir', fr: 'Aller', de: 'Los', it: 'Vai', nl: 'Ga', ru: 'Поехали', ja: '行く', zh: '去', ko: '가자', sv: 'Gå', no: 'Gå', da: 'Gå', fi: 'Mene', pl: 'Idź', cs: 'Jdi', sk: 'Choď', hu: 'Menj', tr: 'Git', el: 'Πήγαινε', ro: 'Du-te', bg: 'Отиди', hr: 'Idi', sr: 'Иди', lt: 'Eik', lv: 'Ejam', et: 'Mine', sl: 'Pojdi', is: 'Fara', ar: 'انطلق', hi: 'जाओ', bn: 'চলো', ur: 'چلو', vi: 'Đi', th: 'ไป', id: 'Pergi', ms: 'Pergi', fa: 'برو', he: 'לך', ca: 'Vés' }[navigator.language.slice(0, 2)] || 'Go' })[k]; const skillFields = [ ['speed', 'Speed'], ['stamina', 'Stamina'], ['playIntelligence', 'Play Intel.'], ['passing', 'Short Passing'], ['shooting', 'Shooting'], ['heading', 'Heading'], ['keeping', 'Keeping'], ['ballControl', 'Ball Control'], ['tackling', 'Tackling'], ['aerialPassing', 'Aerial Passing'], ['setPlays', 'Set Plays'], ['experience', 'Experience'] ]; const skillsHTML = skillFields.map(([field, label]) => `
`).join(''); searchContainer.innerHTML = `

NT Player Search

${skillsHTML}
Scanning players...
`; document.body.appendChild(searchContainer); } generateCountryOptions() { return ` ${this.countries.map(country => { const isSelected = country.code === this.userCountry; if (isSelected) { this.searchValues.countryData = { ntid: country.ntid, cid: country.cid }; } const displayName = country.name === 'Czech Republic' ? 'Czechia' : country.name === 'Macedonia' ? 'North Macedonia' : country.name; return ` `; }).join('')} `; } generateOptions(max, min = 0) { return Array.from({ length: max - min + 1 }, (_, i) => { const value = i + min; return ``; }).join(''); } handleSelectChange(e) { const select = e.target; if (select.name === 'country') { const option = select.selectedOptions[0]; this.searchValues.country = select.value; this.searchValues.countryData = { ntid: option.dataset.ntid, cid: option.dataset.cid }; } else { this.searchValues[select.name] = parseInt(select.value); } } setUpEvents() { const openButton = document.querySelector('.mz-search-open-btn'); const searchContainer = document.querySelector('.mz-search-container'); const internalSearchButton = searchContainer.querySelector('.mz-search-button'); const resultsButton = searchContainer.querySelector('.mz-results-button'); const selects = searchContainer.querySelectorAll('select'); selects.forEach(select => { select.addEventListener('change', (e) => this.handleSelectChange(e)); }); openButton.addEventListener('click', (e) => { e.preventDefault(); searchContainer.classList.add('visible'); }); document.addEventListener('click', (e) => { if (!searchContainer.contains(e.target) && !openButton.contains(e.target) && searchContainer.classList.contains('visible')) { searchContainer.classList.remove('visible'); } }); document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && searchContainer.classList.contains('visible')) { searchContainer.classList.remove('visible'); } }); internalSearchButton.addEventListener('click', () => this.performSearch()); resultsButton.addEventListener('click', () => this.showResults()); } } const searcher = new NTPlayerSearcher(); searcher.init(); })();