// ==UserScript==
// @name MZ - NT Player Search
// @namespace douglaskampl
// @version 2.96
// @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
// ==/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;
}
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;
}
const orderedSkillKeys = [
"speed",
"stamina",
"playIntelligence",
"passing",
"shooting",
"heading",
"keeping",
"ballControl",
"tackling",
"aerialPassing",
"setPlays",
"experience"
];
let skillRows = Array.from(rows);
// Se houver uma linha extra (provavelmente "Form"/"Forma"), ignore
if (skillRows.length > orderedSkillKeys.length) { skillRows = skillRows.slice(0, orderedSkillKeys.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[orderedSkillKeys[index]] = 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 = `
<div>
<h2 class="mz-results-title">Search Results</h2>
<div class="mz-results-total">Total players found: ${this.validPlayers.size}</div>
<div class="mz-results-filters" style="font-size: 0.9rem; color: #aaa; margin-top: 0.5rem;">
Filters applied: ${this.getFiltersAppliedText()}
</div>
</div>`;
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 ? `
<button class="mz-pagination-button" ${page === 1 ? 'disabled' : ''} data-action="prev">Previous</button>
<span class="mz-pagination-info">Page ${page} of ${totalPages}</span>
<button class="mz-pagination-button" ${page === totalPages ? 'disabled' : ''} data-action="next">Next</button>
` : '';
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]) => `
<div class="mz-skill-row">
<span class="mz-skill-name">${this.formatSkillName(skill)}</span>
<div class="mz-skill-value">
<div class="mz-skill-level">
<div class="mz-skill-fill" style="width: ${(value / 10) * 100}%"></div>
</div>
<span class="mz-skill-number">${value}</span>
</div>
</div>
`).join('');
const playerCard = document.createElement('div');
playerCard.className = 'mz-player-card';
playerCard.innerHTML = `
<div class="mz-player-header">
<div class="mz-player-info">
<h3 class="mz-player-name">
<a href="https://www.managerzone.com/?p=players&pid=${player.id}" target="_blank">
${player.name} (${player.id})
</a>
</h3>
<div class="mz-player-details">
<div>Team: ${player.teamId ? `<a href="https://www.managerzone.com/?p=team&tid=${player.teamId}" target="_blank">${player.teamName}</a>` : player.teamName}</div>
<div>Age: ${player.age}</div>
<div>Value: ${new Intl.NumberFormat('en-US').format(player.value)} USD</div>
<div>Total Balls: ${player.totalBalls}</div>
</div>
</div>
</div>
<div class="mz-player-skills">
${skillBars}
</div>
`;
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 <i class="fa fa-search"></i>';
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]) => `
<div class="mz-search-field">
<label>Min. ${label}</label>
<select name="${field}">
${this.generateOptions(10)}
</select>
</div>
`).join('');
searchContainer.innerHTML = `
<div class="mz-search-header">
<h2>NT Player Search</h2>
</div>
<div class="mz-search-grid">
${skillsHTML}
<div class="mz-search-field">
<label>Min. TotalBalls</label>
<select name="totalBalls">
${this.generateOptions(110, 9)}
</select>
</div>
<div class="mz-search-field">
<label>Min. Age</label>
<select name="minAge">
${this.generateOptions(96, 16)}
</select>
</div>
<div class="mz-search-field">
<label>Max. Age</label>
<select name="maxAge">
${this.generateOptions(96, 16)}
</select>
</div>
<div class="mz-country-select mz-search-field">
<label>Country</label>
<select name="country" required>
${this.generateCountryOptions()}
</select>
</div>
</div>
<div class="mz-search-buttons">
<button class="mz-search-button">${씨발('go')}</button>
<button class="mz-results-button" style="display: none;">Show Results</button>
</div>
<div class="mz-progress">
<div class="mz-progress-info">
<span>Scanning players...</span>
</div>
<div class="mz-progress-bar">
<div class="mz-progress-fill"></div>
</div>
</div>
<div class="mz-search-log"></div>
<a href="https://www.managerzone.com/?p=guestbook&uid=8577497"
class="mz-guestbook-link"
title="Questions?">
<i class="fa-solid fa-book"></i>
</a>`;
document.body.appendChild(searchContainer);
}
generateCountryOptions() {
return `
<option value="">Select country</option>
${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 `
<option value="${country.code}"
data-ntid="${country.ntid}"
data-cid="${country.cid}"
${isSelected ? 'selected' : ''}>
${displayName}
</option>`;
}).join('')}
`;
}
generateOptions(max, min = 0) {
return Array.from({ length: max - min + 1 }, (_, i) => {
const value = i + min;
return `<option value="${value}">${value}</option>`;
}).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();
})();