// ==UserScript== // @name MZ - NT Player Search // @namespace douglaskampl // @version 2.99 // @description Searches for players who match specific requirements (NC/NCA only) // @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 // @connect https://www.managerzone.com/ // @require https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js // @run-at document-idle // @license MIT // @downloadURL https://update.greasyfork.cloud/scripts/527163/MZ%20-%20NT%20Player%20Search.user.js // @updateURL https://update.greasyfork.cloud/scripts/527163/MZ%20-%20NT%20Player%20Search.meta.js // ==/UserScript== (function () { 'use strict'; GM_addStyle(`@import url('https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap');.nt-search-open-btn{display:inline-block;padding:4px 8px;color:white;font-weight:bold;text-decoration:none;font-size:12px;font-family:'Space Mono',monospace;background:linear-gradient(135deg, #ff6e40, #ff5252, #448aff);border-radius:4px;transition:all .2s ease-in-out;border:1px solid rgba(138,43,226,.1);text-shadow:1px 1px 2px rgba(0,0,0,.3);margin-left:10px;vertical-align:middle}.nt-search-open-btn:hover{background:linear-gradient(145deg,rgba(40,40,70,.9),rgba(50,50,90,.9));color:lightgray;text-decoration:none;box-shadow:inset 0 0 5px rgba(138,43,226,.2)}.nt-search-open-btn i{margin-left:5px;color:violet}.nt-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)}.nt-search-container.visible{visibility:visible;opacity:1;transform:translate(-50%,-50%) scale(1)}.nt-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)}.nt-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)}.nt-search-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:1rem;margin-bottom:1.5rem}.nt-search-field{display:flex;flex-direction:column;gap:.5rem}.nt-search-field label{color:#ff9966;font-size:.875rem;text-transform:uppercase;letter-spacing:1px}.nt-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}.nt-search-field select:focus{outline:none;border-color:#ff9966;box-shadow:0 0 0 2px rgba(138,43,226,.2)}.nt-search-field select:disabled{opacity:0.5;cursor:not-allowed;background:#333}.nt-search-buttons{display:flex;justify-content:center;align-items:center;gap:1rem;margin-top:1rem}.nt-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)}.nt-search-button:not(:disabled):hover{transform:translateY(-2px);box-shadow:0 6px 8px rgba(0,0,0,.2)}.nt-search-button:disabled{opacity:0.5;cursor:not-allowed;background:#666}.nt-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}.nt-search-log::-webkit-scrollbar{width:8px;height:8px}.nt-search-log::-webkit-scrollbar-track{background:#1a1a2e;border-radius:4px}.nt-search-log::-webkit-scrollbar-thumb{background:#6366f1;border-radius:4px}.nt-search-log::-webkit-scrollbar-thumb:hover{background:#4834d4}.nt-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)}}.nt-search-guestbook-link{position:fixed;bottom:1rem;right:1rem;color:#ff9966;transition:all .2s}.nt-search-guestbook-link:hover{color:#6366f1;transform:scale(1.1)}.nt-search-country-select{width:200px}.nt-search-country-select select{width:100%}.nt-search-loading{display:flex;justify-content:center;align-items:center;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(10,10,20,.9);z-index:10000;visibility:hidden;opacity:0;transition:opacity .3s}.nt-search-loading.visible{visibility:visible;opacity:1}.nt-orbital-spinner{position:relative;width:60px;height:60px}.nt-orbiter{position:absolute;width:10px;height:10px;border-radius:50%;top:50%;left:50%;margin:-5px}.nt-orbiter:nth-child(1){background:violet;animation:orbit1 2s linear infinite}.nt-orbiter:nth-child(2){background:#ff9966;animation:orbit2 2s linear infinite 0.2s}.nt-orbiter:nth-child(3){background:#00ffff;animation:orbit3 2s linear infinite 0.4s}@keyframes orbit1{0%{transform:rotate(0deg) translateX(25px) rotate(0deg) scale(1)}50%{transform:rotate(180deg) translateX(25px) rotate(-180deg) scale(0.7)}100%{transform:rotate(360deg) translateX(25px) rotate(-360deg) scale(1)}}@keyframes orbit2{0%{transform:rotate(120deg) translateX(25px) rotate(-120deg) scale(1)}50%{transform:rotate(300deg) translateX(25px) rotate(-300deg) scale(0.7)}100%{transform:rotate(480deg) translateX(25px) rotate(-480deg) scale(1)}}@keyframes orbit3{0%{transform:rotate(240deg) translateX(25px) rotate(-240deg) scale(1)}50%{transform:rotate(420deg) translateX(25px) rotate(-420deg) scale(0.7)}100%{transform:rotate(600deg) translateX(25px) rotate(-600deg) scale(1)}}.nt-search-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}.nt-search-results-button:hover{transform:translateY(-2px);box-shadow:0 6px 8px rgba(0,0,0,.2)}.nt-search-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%)}}.nt-search-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}.nt-search-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)}.nt-search-results-close{background:none;border:none;color:#ff9966;font-size:1.5rem;cursor:pointer;transition:all 0.2s;padding:0.5rem}.nt-search-results-close:hover{color:#6366f1;transform:scale(1.1)}.nt-search-results-content{padding:1.5rem;height:calc(90vh - 5rem);overflow-y:auto;scrollbar-width:thin;scrollbar-color:#6366f1 #1a1a2e}.nt-search-results-content::-webkit-scrollbar{width:8px}.nt-search-results-content::-webkit-scrollbar-track{background:#1a1a2e}.nt-search-results-content::-webkit-scrollbar-thumb{background:#6366f1;border-radius:4px}.nt-search-results-content::-webkit-scrollbar-thumb:hover{background:#4834d4}.nt-search-players-container{display:flex;flex-wrap:wrap;gap:1.5rem;margin:1rem 0}.nt-search-player-card{background:rgba(26,26,46,.5);border-radius:8px;padding:1rem;transition:all 0.2s;border:1px solid rgba(138,43,226,.1);flex:1 1 calc(50% - 1rem);box-sizing:border-box;display:flex;flex-direction:column;min-width:350px}.nt-search-player-card:hover{transform:translateY(-2px);box-shadow:0 4px 12px rgba(83,11,237,.2)}.nt-search-player-header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:1rem}.nt-search-player-info{flex:1}.nt-search-player-name{font-size:1.1rem;font-weight:bold;color:#fff;margin:0 0 0.5rem 0}.nt-search-player-name a{color:inherit;text-decoration:none}.nt-search-player-name a:hover{color:violet}.nt-search-player-details{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:.5rem;color:#ff9966;font-size:0.875rem}.nt-search-skills-list{margin-top:1rem;display:flex;flex-direction:column;gap:4px}.nt-search-skill-row{display:flex;align-items:center;background:transparent;padding:0;box-shadow:none;min-height:24px}.nt-search-skill-name{font-size:.8rem;color:#f0f0f0;flex-basis:80px;margin-right:8px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.nt-search-skill-value{display:flex;align-items:center;gap:4px;color:#ff9966;flex-shrink:0}.nt-search-skill-value img{height:.9em;width:auto;vertical-align:middle}.nt-search-skill-value-text{font-size:.8rem;white-space:nowrap}.nt-search-player-total-balls{margin-top:0.75rem;font-weight:bold;color:#ffcc66;text-align:right;font-size:0.9rem}.nt-search-results-pagination{display:flex;justify-content:center;align-items:center;gap:1rem;padding:1rem 0;border-top:1px solid rgba(138,43,226,.1);border-bottom:1px solid rgba(138,43,226,.1);margin:0 -1.5rem 1rem -1.5rem}.nt-search-results-pagination.bottom{border-top:1px solid rgba(138,43,226,.1);border-bottom:none;margin-top:1rem;margin-bottom:0}.nt-search-results-pagination.top{border-bottom:1px solid rgba(138,43,226,.1);border-top:none;margin-bottom:1rem;margin-top:0}.nt-search-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}.nt-search-pagination-button:not(:disabled):hover{background:#2a2a4e;transform:translateY(-1px)}.nt-search-pagination-button:disabled{opacity:0.5;cursor:not-allowed}.nt-search-pagination-info{color:#ff9966;font-size:0.875rem}.nt-search-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}.nt-search-export-button:hover{background:#2a2a4e;transform:translateY(-1px)}.nt-search-export-button:active{transform:translateY(1px)}.nt-search-header-controls{display:flex;align-items:center;gap:1rem}`); const MASSIVE_COUNTRIES = ['BR', 'CN', 'AR', 'SE', 'PL', 'TR']; const PLAYERS_PER_PAGE = 10; const ORDERED_SKILL_KEYS = [ "speed", "stamina", "playIntelligence", "passing", "shooting", "heading", "keeping", "ballControl", "tackling", "aerialPassing", "setPlays", "experience" ]; class Logger { constructor(container, flushInterval = 400) { this.container = container; this.flushInterval = flushInterval; this.queue = []; this.timeout = null; this.scheduled = false; } 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.scheduled) { this.scheduled = true; this.timeout = setTimeout(() => this.flush(), this.flushInterval); } } flush() { if (!this.queue.length || !this.container) { this.scheduled = false; return; } const fragment = document.createDocumentFragment(); this.queue.forEach(({ message, type }) => { const entry = document.createElement('div'); entry.className = `nt-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; } this.scheduled = false; } } 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 NTPlayerParser { constructor(minRequirements) { this.minRequirements = minRequirements; this.logger = null; } 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] span.bold'); if (totalBallsElement) { totalBalls = parseInt(totalBallsElement.textContent, 10) || 0; } let skillRows = Array.from(rows); if (skillRows.length > ORDERED_SKILL_KEYS.length) { skillRows = skillRows.slice(0, ORDERED_SKILL_KEYS.length); } skillRows.forEach((row, index) => { const valueCell = row.querySelector('.skillval'); if (!valueCell) return; const rawValue = valueCell.textContent.replace(/[()]/g, "").trim(); const value = parseInt(rawValue, 10); if (!isNaN(value)) { skills[ORDERED_SKILL_KEYS[index]] = value; } }); if (Object.keys(skills).length === 0) return null; ORDERED_SKILL_KEYS.forEach(key => { if (!(key in skills)) { skills[key] = 0; } }); if (!this.validateSkills(skills)) return null; return { skills, totalBalls }; } validateSkills(skills) { return Object.entries(this.minRequirements) .filter(([key]) => key in skills && typeof skills[key] === 'number') .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); if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`); const html = await response.text(); return this.parseSkills(html); } catch (error) { if (this.logger) this.logger.log(`Error parsing player ${playerId}: ${error.message}`, '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() { const row = { 'ID': this.id, 'Name': this.name, 'Team': this.teamName, 'Age': this.age, 'Value': this.value, 'Salary': this.salary, 'Total Balls': this.totalBalls, }; ORDERED_SKILL_KEYS.forEach(key => { row[NTPlayerSearcher.prototype.formatSkillName(key)] = this.skills[key] || 0; }); return row; } } 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: 40, 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(); this.loadingElement = null; this.logger = null; this.countries = []; this.userCountry = null; this.username = null; this.currentResultsPage = 1; this.resultsListeners = { prev: null, next: null, esc: null }; } 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), ontimeout: () => reject(new Error(`Timeout fetching Top100 page ${page}`)) }); }) ); const data = JSON.parse(response.responseText); const players = (data.players || []).filter(player => { return player.age >= this.searchValues.minAge && player.age <= this.searchValues.maxAge; }); 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) || 0, salary: 0 } ]); this.playerIds = new Map([...this.playerIds, ...playerEntries]); return players.map(player => player.id.toString()); } catch (error) { if (this.logger) this.logger.log(`Error fetching Top100 players (page ${page}): ${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 = []; if (this.logger) this.logger.log(`Fetching Top100 players...`); 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)); } if (this.logger) this.logger.log(`Finished fetching Top100 players.`); 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), ontimeout: () => reject(new Error('Timeout fetching countries list')) }); }); } async fetchUserCountry() { const usernameElem = document.querySelector('#header-username'); if (!usernameElem) return { userCountry: null, username: null }; const username = usernameElem.textContent.trim(); try { const response = await fetch(`https://www.managerzone.com/xml/manager_data.php?sport_id=1&username=${username}`); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); const text = await response.text(); const parser = new DOMParser(); const xmlDoc = parser.parseFromString(text, "text/xml"); const parserError = xmlDoc.querySelector('parsererror'); if (parserError) { console.error('XML parsing error:', parserError.textContent); return { userCountry: null, username: username }; } const countryCode = xmlDoc.querySelector('UserData')?.getAttribute('countryShortname') || null; return { userCountry: countryCode, username: username }; } catch (error) { console.error("Error fetching user country:", error); if (this.logger) this.logger.log(`Error fetching user country: ${error.message}`, 'error'); return { userCountry: null, username: username }; } } async checkUserRole(ntid, cid, username) { if (!ntid || !cid || !username) { if (this.logger) this.logger.log("Missing ntid, cid, or username for role check.", "warn"); return false; } const url = `https://www.managerzone.com/ajax.php?p=nationalTeams&sub=team&ntid=${ntid}&cid=${cid}&type=national_team&sport=soccer`; try { const response = await fetch(url); if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`); const html = await response.text(); const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); const profileLinks = doc.querySelectorAll('table.padding a[href*="/?p=profile&uid="]'); for (const link of profileLinks) { if (link.textContent.trim() === username) { if (this.logger) this.logger.log(`User ${username} confirmed as NC/NCA.`, 'info'); return true; } } if (this.logger) this.logger.log(`User ${username} is not NC or NCA for this country.`, 'info'); return false; } catch (error) { console.error(`Error checking user role: ${error.message}`); if (this.logger) this.logger.log(`Error checking user role: ${error.message}`, 'error'); return false; } } async init() { this.createLoadingElement(); this.showLoading(); try { const [countries, { userCountry, username }] = await Promise.all([ this.fetchCountriesList(), this.fetchUserCountry() ]); this.countries = countries || []; this.userCountry = userCountry; this.username = username; let isAuthorized = false; let userCountryData = null; if (this.userCountry && this.username && this.countries.length > 0) { userCountryData = this.countries.find(c => c.code === this.userCountry); if (userCountryData) { this.searchValues.country = this.userCountry; this.searchValues.countryData = { ntid: userCountryData.ntid, cid: userCountryData.cid }; } } const tempLogContainer = document.createElement('div'); this.logger = new Logger(tempLogContainer); if (userCountryData && this.username) { isAuthorized = await this.checkUserRole(userCountryData.ntid, userCountryData.cid, this.username); } else { if (this.logger) this.logger.log("Could not find user country data or username.", "warn"); } if (isAuthorized) { const appended = await this.appendSearchTab(); if (!appended) { throw new Error("Failed to append search tab elements."); } const logContainer = document.querySelector('.nt-search-log'); if (logContainer) { this.logger.container = logContainer; if (!this.userCountry) { this.logger.log("Could not determine user country data.", "warn"); } } else { console.error("Log container not found after appending tab."); this.logger = { log: console.log, flush: () => {} }; } this.setUpEvents(); } else { console.log("User is not authorized (NC/NCA) or required data missing. NT Search UI not added."); if (this.logger) this.logger.log("User not authorized (NC/NCA) or essential data missing. Search tool disabled.", "warn"); } } catch (error) { console.error("Initialization failed:", error); if (this.logger) { this.logger.log(`Initialization failed: ${error.message}`, 'error'); } else { alert(`Initialization failed: ${error.message}`); } } finally { if (this.logger) this.logger.flush(); this.hideLoading(); } } createLoadingElement() { if (this.loadingElement) return; this.loadingElement = document.createElement('div'); this.loadingElement.className = 'nt-search-loading'; const spinnerContainer = document.createElement('div'); spinnerContainer.className = 'nt-orbital-spinner'; spinnerContainer.innerHTML = `
`; this.loadingElement.appendChild(spinnerContainer); document.body.appendChild(this.loadingElement); } showLoading() { if (this.loadingElement) { this.loadingElement.classList.add('visible'); } } hideLoading() { if (this.loadingElement) { this.loadingElement.classList.remove('visible'); } } 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), ontimeout: () => reject(new Error('Timeout fetching leagues')) }); }) ); 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', '')); if(isNaN(divLevel)) return true; return divLevel <= maxDivision; } return true; }).map(league => league.id); } catch (error) { if (this.logger) this.logger.log(`Error fetching leagues: ${error.message}`, 'error'); return []; } } 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}`) ); if (!response.ok) throw new Error(`HTTP error! status: ${response.status} for league ${leagueId}`); const text = await response.text(); const parser = new DOMParser(); const xmlDoc = parser.parseFromString(text, "text/xml"); const parserError = xmlDoc.querySelector('parsererror'); if (parserError) { console.error(`XML parsing error for league ${leagueId}:`, parserError.textContent); throw new Error(`XML parsing error for league ${leagueId}`); } const teams = xmlDoc.getElementsByTagName('Team'); return Array.from(teams).map(team => team.getAttribute('teamId')); } catch (error) { if (this.logger) this.logger.log(`Error fetching teams for league ${leagueId}: ${error.message}`, 'error'); return []; } } async processLeagueBatch(leagueIds) { if (!leagueIds || leagueIds.length === 0) { if (this.logger) this.logger.log("No league IDs to process.", "warn"); return; } if (this.logger) this.logger.log(`Processing ${leagueIds.length} leagues...`); await this.chunkProcessor.process( leagueIds, async (leagueId) => { try { const teamIds = await this.getTeamIds(leagueId); if (teamIds && teamIds.length > 0) { teamIds.forEach(id => this.teamIds.add(id)); } this.processedLeagues++; } catch (error) { if (this.logger) this.logger.log(`Failed to process league ${leagueId}: ${error}`, 'error'); } } ); if (this.logger) this.logger.log(`Finished processing leagues. Found ${this.teamIds.size} unique teams.`); } 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}`) ); if (!response.ok) throw new Error(`HTTP error! status: ${response.status} for team ${teamId}`); const text = await response.text(); const parser = new DOMParser(); const xmlDoc = parser.parseFromString(text, "text/xml"); const parserError = xmlDoc.querySelector('parsererror'); if (parserError) { console.error(`XML parsing error for team ${teamId}:`, parserError.textContent); throw new Error(`XML parsing error for team ${teamId}`); } const teamPlayers = xmlDoc.querySelector('TeamPlayers'); if (!teamPlayers) { if (this.logger) this.logger.log(`No TeamPlayers data found for team ${teamId}`, 'warn'); return; } const teamName = teamPlayers.getAttribute('teamName') || `Team ${teamId}`; const actualTeamId = teamPlayers.getAttribute('teamId') || teamId; const players = xmlDoc.getElementsByTagName('Player'); const targetCountry = this.searchValues.country.toLowerCase(); Array.from(players).forEach(player => { const age = parseInt(player.getAttribute('age')); const countryCodeAttr = player.getAttribute('countryShortname'); if(!countryCodeAttr) return; const countryCode = countryCodeAttr.toLowerCase(); if (age >= this.searchValues.minAge && age <= this.searchValues.maxAge && countryCode === targetCountry) { const playerId = player.getAttribute('id'); const playerName = player.getAttribute('name'); const value = parseInt(player.getAttribute('value')) || 0; const salary = parseInt(player.getAttribute('salary')) || 0; if (playerId && playerName) { this.playerIds.set(playerId, { id: playerId, name: playerName, teamName: teamName, teamId: actualTeamId, age: age, value: value, salary: salary }); } } }); } catch (error) { if (this.logger) this.logger.log(`Error fetching players for team ${teamId}: ${error.message}`, 'error'); } } async processTeamBatch(teamIds) { if (!teamIds || teamIds.length === 0) { if (this.logger) this.logger.log("No team IDs to process.", "warn"); return; } const totalTeams = teamIds.length; let processedTeams = 0; if (this.logger) this.logger.log(`Processing ${totalTeams} teams...`); await this.chunkProcessor.process( teamIds, async (teamId) => { await this.fetchPlayerList(teamId); processedTeams++; if (processedTeams % 100 === 0 || processedTeams === totalTeams) { if (this.logger) this.logger.log(`Team processing: ${processedTeams}/${totalTeams}`); } } ); if (this.logger) this.logger.log(`Finished processing teams. Found ${this.playerIds.size} potential players.`); } async searchForPlayers() { if (!this.searchValues.country || !this.searchValues.countryData) { if (this.logger) this.logger.log('Country not selected or country data missing.', 'error'); alert('Please ensure a country is selected.'); return; } this.teamIds = new Set(); this.playerIds = new Map(); this.processedLeagues = 0; this.totalLeagues = 0; this.validPlayers = new Map(); this.requestQueue.reset(); this.currentResultsPage = 1; const countryCode = this.searchValues.country; if (this.logger) this.logger.log(`Starting search for country: ${countryCode}`); try { if (this.searchValues.maxAge > 18) { await this.fetchAllTop100Players(countryCode); if (this.logger) this.logger.log(`Found ${this.playerIds.size} players from Top100.`); } const leagueIds = await this.getLeagueIds(countryCode); this.totalLeagues = leagueIds.length; if(this.totalLeagues === 0 && this.playerIds.size === 0){ if (this.logger) this.logger.log(`No leagues found and no top players matched. Stopping search.`, 'warn'); return; } await this.processLeagueBatch(leagueIds); await this.processTeamBatch(Array.from(this.teamIds)); const { ntid, cid } = this.searchValues.countryData; const ntPlayerParser = new NTPlayerParser(this.searchValues); ntPlayerParser.logger = this.logger; const playerEntries = Array.from(this.playerIds.entries()); if(playerEntries.length === 0) { if (this.logger) this.logger.log('No potential players found after gathering IDs.', 'warn'); return; } if (this.logger) this.logger.log(`Processing skills for ${playerEntries.length} players...`); let processedCount = 0; let validCount = 0; const totalPlayersToParse = playerEntries.length; const yieldFrequency = 25; await this.chunkProcessor.process( playerEntries, async ([playerId, playerData]) => { try { 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 )); validCount++; } } catch (parseError) { if (this.logger) this.logger.log(`Error processing player ${playerId} (${playerData.name}): ${parseError.message}`, 'error'); } finally { processedCount++; if (processedCount % 100 === 0 || processedCount === totalPlayersToParse) { if (this.logger) this.logger.log(`Skill check progress: ${processedCount}/${totalPlayersToParse}`); } if (processedCount % yieldFrequency === 0) { await new Promise(res => setTimeout(res, 0)); } } } ); if (this.logger) this.logger.log('Finishing skill processing...'); await new Promise(resolve => setTimeout(resolve, 200)); const finalCount = this.validPlayers.size; if (this.logger) this.logger.log(`Search complete: found ${finalCount} players matching all criteria.`); const resultsButton = document.querySelector('.nt-search-results-button'); if(resultsButton) resultsButton.style.display = finalCount > 0 ? "inline-block" : "none"; return Array.from(this.validPlayers.keys()); } catch (error) { if (this.logger) this.logger.log(`Critical error during search: ${error.message}`, 'error'); console.error('Search failed critically:', error); alert(`An error occurred during the search: ${error.message}`); } finally { if (this.logger) this.logger.flush(); } } async performSearch() { if (this.isSearching) { if(this.logger) this.logger.log("Search already in progress.", "warn"); return; } if (!this.searchValues.country || !this.searchValues.countryData) { alert("Please select a country before searching."); return; } this.isSearching = true; const internalSearchButton = document.querySelector('.nt-search-container .nt-search-button'); if(internalSearchButton) internalSearchButton.disabled = true; const logContainer = document.querySelector('.nt-search-log'); const resultsButton = document.querySelector('.nt-search-results-button'); this.showLoading(); if(logContainer) logContainer.innerHTML = ''; if(resultsButton) resultsButton.style.display = 'none'; if (!this.logger || !this.logger.container) { console.error("Logger not initialized before search start."); const logCont = document.querySelector('.nt-search-log'); if(logCont) this.logger = new Logger(logCont); else this.logger = { log: console.log, flush: () => {} }; } try { await this.searchForPlayers(); } catch (error) { if (this.logger) this.logger.log(`Unhandled error during search execution: ${error.message}`, 'error'); console.error('Search execution failed:', error); alert(`An unexpected error occurred: ${error.message}`); } finally { this.isSearching = false; if(internalSearchButton) internalSearchButton.disabled = false; this.hideLoading(); if(this.logger) this.logger.flush(); } } getFiltersAppliedText() { const filters = []; const countryName = this.countries.find(c => c.code === this.searchValues.country)?.name || this.searchValues.country; if (countryName) { filters.push(`Country: ${countryName}`); } filters.push(`Age: ${this.searchValues.minAge} - ${this.searchValues.maxAge}`); filters.push(`Min Total Balls: ${this.searchValues.totalBalls}`); ORDERED_SKILL_KEYS.forEach(skill => { if (this.searchValues[skill] > 0) { filters.push(`Min ${this.formatSkillName(skill)}: ${this.searchValues[skill]}`); } }); return filters.join('; '); } createPaginationControls(page, totalPages) { const container = document.createElement('div'); container.className = 'nt-search-results-pagination'; if (totalPages > 1) { const prevBtn = document.createElement('button'); prevBtn.className = 'nt-search-pagination-button'; prevBtn.textContent = 'Previous'; prevBtn.disabled = page === 1; prevBtn.dataset.action = "prev"; const pageInfo = document.createElement('span'); pageInfo.className = 'nt-search-pagination-info'; pageInfo.textContent = `Page ${page} of ${totalPages}`; const nextBtn = document.createElement('button'); nextBtn.className = 'nt-search-pagination-button'; nextBtn.textContent = 'Next'; nextBtn.disabled = page === totalPages; nextBtn.dataset.action = "next"; container.appendChild(prevBtn); container.appendChild(pageInfo); container.appendChild(nextBtn); } return container; } renderResultsPage(page) { const playersContainer = document.querySelector('.nt-search-players-container'); const paginationTopContainer = document.querySelector('.nt-search-results-pagination.top'); const paginationBottomContainer = document.querySelector('.nt-search-results-pagination.bottom'); const modalContent = document.querySelector('.nt-search-results-content'); if (!playersContainer || !paginationTopContainer || !paginationBottomContainer || !modalContent) return; this.currentResultsPage = page; playersContainer.textContent = ''; paginationTopContainer.textContent = ''; paginationBottomContainer.textContent = ''; const playersArray = Array.from(this.validPlayers.values()) .sort((a, b) => b.totalBalls - a.totalBalls); const totalPages = Math.ceil(playersArray.length / PLAYERS_PER_PAGE); const startIndex = (page - 1) * PLAYERS_PER_PAGE; const pagePlayers = playersArray.slice(startIndex, startIndex + PLAYERS_PER_PAGE); this.removePaginationListeners(); this.resultsListeners.prev = () => { if (this.currentResultsPage > 1) { this.renderResultsPage(this.currentResultsPage - 1); if(modalContent) modalContent.scrollTop = 0; } }; this.resultsListeners.next = () => { if (this.currentResultsPage < totalPages) { this.renderResultsPage(this.currentResultsPage + 1); if(modalContent) modalContent.scrollTop = 0; } }; const topControls = this.createPaginationControls(page, totalPages); const bottomControls = this.createPaginationControls(page, totalPages); paginationTopContainer.appendChild(topControls); paginationBottomContainer.appendChild(bottomControls); this.addPaginationListeners(paginationTopContainer); this.addPaginationListeners(paginationBottomContainer); const fragment = document.createDocumentFragment(); pagePlayers.forEach(player => { let skillsHTML = ''; ORDERED_SKILL_KEYS.forEach((skillKey) => { const value = player.skills[skillKey] || 0; const skillName = this.formatSkillName(skillKey); skillsHTML += `