Multi-language FMP position analyzer and player comparison tool with auto-language detection
当前为
// ==UserScript==
// @name FMP Position Analyzer and Comparator
// @namespace http://tampermonkey.net/
// @version 4.0
// @description Multi-language FMP position analyzer and player comparison tool with auto-language detection
// @author FMP Assistant
// @match https://footballmanagerproject.com/Team/Player*
// @icon https://www.google.com/s2/favicons?sz=64&domain=footballmanagerproject.com
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @require https://code.jquery.com/jquery-3.6.0.min.js
// @license MIT
// ==/UserScript==
/*
* FMP Position Analyzer and Comparator v4.0
* Multi-language FMP position highlighter and player comparator
* Automatically detects game language (EN/TR/DE/ES/FR/IT)
* Provides professional player analysis and comparison tools
*
* Features:
* - Auto-language detection
* - Position-based skill highlighting
* - Player saving system (up to 5 players)
* - Advanced comparison modal
* - Draggable popup windows
*
* Supported positions: GK, DC, DL/DR, DMC, MC, ML/MR, AMC, AML/AMR, FC/ST
*
* Author: FMP Assistant
* License: MIT
*/
(function() {
'use strict';
// ===== MULTI-LANGUAGE SUPPORT =====
const translations = {
en: {
savePlayer: '💾 Save Player',
compareWindow: '📊 Comparison Window',
modalTitle: '👥 Player Comparison',
clearList: 'Clear List',
savedCount: 'Saved players',
noPlayers: 'No players saved. Go to player profile and click "Save".',
confirmClear: 'Delete all saved players?',
playerSaved: ' saved successfully!',
maxPlayers: 'Maximum 5 players. Please clear list first.',
unknownPlayer: 'Unknown Player',
unknownPosition: 'Unknown',
age: 'Age',
salary: 'Salary',
rating: 'Rating',
quality: 'Quality',
points: 'POINTS',
difference: 'DIFF',
feature: 'FEATURE'
},
tr: {
savePlayer: '💾 Oyuncuyu Kaydet',
compareWindow: '📊 Karşılaştırma Penceresi',
modalTitle: '👥 Oyuncu Karşılaştırma',
clearList: 'Listeyi Temizle',
savedCount: 'Kayıtlı oyuncular',
noPlayers: 'Henüz oyuncu kaydedilmedi. Oyuncu profiline gidip "Kaydet" butonuna basın.',
confirmClear: 'Tüm kayıtlı oyuncular silinsin mi?',
playerSaved: ' başarıyla kaydedildi!',
maxPlayers: 'Maksimum 5 oyuncu. Lütfen önce listeyi temizleyin.',
unknownPlayer: 'Bilinmeyen Oyuncu',
unknownPosition: 'Bilinmiyor',
age: 'Yaş',
salary: 'Maaş',
rating: 'Derece',
quality: 'Kalite',
points: 'PUAN',
difference: 'FARK',
feature: 'ÖZELLİK'
},
de: {
savePlayer: '💾 Spieler speichern',
compareWindow: '📊 Vergleichsfenster',
modalTitle: '👥 Spielervergleich',
clearList: 'Liste löschen',
savedCount: 'Gespeicherte Spieler',
noPlayers: 'Keine Spieler gespeichert. Gehe zum Spielerprofil und klicke "Speichern".',
confirmClear: 'Alle gespeicherten Spieler löschen?',
playerSaved: ' erfolgreich gespeichert!',
maxPlayers: 'Maximal 5 Spieler. Bitte zuerst Liste löschen.',
unknownPlayer: 'Unbekannter Spieler',
unknownPosition: 'Unbekannt',
age: 'Alter',
salary: 'Gehalt',
rating: 'Bewertung',
quality: 'Qualität',
points: 'PUNKTE',
difference: 'DIFF',
feature: 'MERKMAL'
},
es: {
savePlayer: '💾 Guardar Jugador',
compareWindow: '📊 Ventana Comparación',
modalTitle: '👥 Comparación de Jugadores',
clearList: 'Limpiar Lista',
savedCount: 'Jugadores guardados',
noPlayers: 'No hay jugadores guardados. Ve al perfil y haz clic en "Guardar".',
confirmClear: '¿Eliminar todos los jugadores guardados?',
playerSaved: ' guardado exitosamente!',
maxPlayers: 'Máximo 5 jugadores. Por favor limpia la lista primero.',
unknownPlayer: 'Jugador Desconocido',
unknownPosition: 'Desconocido',
age: 'Edad',
salary: 'Salario',
rating: 'Valoración',
quality: 'Calidad',
points: 'PUNTOS',
difference: 'DIF',
feature: 'CARACTERÍSTICA'
},
fr: {
savePlayer: '💾 Sauvegarder Joueur',
compareWindow: '📊 Fenêtre Comparaison',
modalTitle: '👥 Comparaison de Joueurs',
clearList: 'Vider la Liste',
savedCount: 'Joueurs sauvegardés',
noPlayers: 'Aucun joueur sauvegardé. Allez sur un profil et cliquez "Sauvegarder".',
confirmClear: 'Supprimer tous les joueurs sauvegardés?',
playerSaved: ' sauvegardé avec succès!',
maxPlayers: 'Maximum 5 joueurs. Veuillez vider la liste d\'abord.',
unknownPlayer: 'Joueur Inconnu',
unknownPosition: 'Inconnu',
age: 'Âge',
salary: 'Salaire',
rating: 'Note',
quality: 'Qualité',
points: 'POINTS',
difference: 'DIFF',
feature: 'CARACTÉRISTIQUE'
},
it: {
savePlayer: '💾 Salva Giocatore',
compareWindow: '📊 Finestra Confronto',
modalTitle: '👥 Confronto Giocatori',
clearList: 'Pulisci Lista',
savedCount: 'Giocatori salvati',
noPlayers: 'Nessun giocatore salvato. Vai al profilo e clicca "Salva".',
confirmClear: 'Eliminare tutti i giocatori salvati?',
playerSaved: ' salvato con successo!',
maxPlayers: 'Massimo 5 giocatori. Per favore pulisci prima la lista.',
unknownPlayer: 'Giocatore Sconosciuto',
unknownPosition: 'Sconosciuto',
age: 'Età',
salary: 'Stipendio',
rating: 'Valutazione',
quality: 'Qualità',
points: 'PUNTI',
difference: 'DIFF',
feature: 'CARATTERISTICA'
}
};
// Auto-detect game language
function detectGameLanguage() {
const htmlLang = document.documentElement.lang;
if (htmlLang && translations[htmlLang]) {
return htmlLang;
}
// Fallback: Check for common text patterns
const bodyText = document.body.innerText;
if (bodyText.includes('Yaş') || bodyText.includes('Maaş')) return 'tr';
if (bodyText.includes('Alter') || bodyText.includes('Gehalt')) return 'de';
if (bodyText.includes('Edad') || bodyText.includes('Salario')) return 'es';
if (bodyText.includes('Âge') || bodyText.includes('Salaire')) return 'fr';
if (bodyText.includes('Età') || bodyText.includes('Stipendio')) return 'it';
return 'en'; // Default to English
}
const currentLang = detectGameLanguage();
const t = translations[currentLang] || translations.en;
// ===== POSITION DEFINITIONS (Language Independent) =====
const positionSkills = {
'KL': { primary: ['Poz', '1e1', 'ElK'], secondary: ['Ref', 'HH', 'Sçr', 'Zıp'] },
'GK': { primary: ['Poz', '1e1', 'ElK'], secondary: ['Ref', 'HH', 'Sçr', 'Zıp'] },
'DC': { primary: ['Mrkj', 'TpK', 'Poz'], secondary: ['Kaf', 'Day', 'Hız'] },
'DL': { primary: ['TpK', 'Ort', 'Poz'], secondary: ['Pas', 'Tek', 'Hız'] },
'DR': { primary: ['TpK', 'Ort', 'Poz'], secondary: ['Pas', 'Tek', 'Hız'] },
'DMC': { primary: ['Mrkj', 'TpK', 'Poz'], secondary: ['Kaf', 'Pas', 'Day'] },
'MC': { primary: ['Pas', 'Tek', 'Poz'], secondary: ['TpK', 'Kaf', 'Day'] },
'ML': { primary: ['Ort', 'Pas', 'Poz'], secondary: ['Tek', 'Kaf', 'Hız'] },
'MR': { primary: ['Ort', 'Pas', 'Poz'], secondary: ['Tek', 'Kaf', 'Hız'] },
'AMC': { primary: ['Pas', 'Bit', 'Tek'], secondary: ['Ort', 'Uza', 'Poz'] },
'AML': { primary: ['Ort', 'Pas', 'Poz'], secondary: ['Tek', 'Bit', 'Hız'] },
'AMR': { primary: ['Ort', 'Pas', 'Poz'], secondary: ['Tek', 'Bit', 'Hız'] },
'FC': { primary: ['Bit', 'Kaf'], secondary: ['Uza', 'Poz', 'Hız'] },
'ST': { primary: ['Bit', 'Kaf'], secondary: ['Uza', 'Poz', 'Hız'] }
};
// ===== STYLES =====
GM_addStyle(`
.fmp-primary-skill { background-color: #ffd700 !important; color: #000 !important; font-weight: bold !important; border-radius: 4px; }
.fmp-secondary-skill { background-color: #b0e0e6 !important; color: #000 !important; font-weight: bold !important; border-radius: 4px; }
#playerdata .skilltable th, #playerdata .skilltable td { padding: 2px 4px !important; }
/* Modal Styles */
.fmp-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
visibility: hidden;
opacity: 0;
transition: opacity 0.3s, visibility 0.3s;
}
.fmp-modal-overlay.active {
visibility: visible;
opacity: 1;
}
.fmp-modal {
background-color: white;
border-radius: 10px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
width: 95%;
max-width: 1200px;
max-height: 90vh;
overflow: hidden;
display: flex;
flex-direction: column;
transform: translateY(-20px);
transition: transform 0.3s;
}
.fmp-modal.active {
transform: translateY(0);
}
.fmp-modal-header {
background: linear-gradient(to right, #007bff, #0056b3);
color: white;
padding: 15px 20px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: move;
user-select: none;
}
.fmp-modal-title {
font-size: 18px;
font-weight: bold;
margin: 0;
}
.fmp-modal-close {
background: none;
border: none;
color: white;
font-size: 24px;
cursor: pointer;
line-height: 1;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: background-color 0.2s;
}
.fmp-modal-close:hover {
background-color: rgba(255, 255, 255, 0.2);
}
.fmp-modal-content {
padding: 20px;
overflow-y: auto;
flex-grow: 1;
}
/* Comparison Table */
#fmp-compare-table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
font-size: 0.95em;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
#fmp-compare-table th, #fmp-compare-table td {
border: 1px solid #b0c4de;
padding: 8px;
text-align: center;
}
#fmp-compare-table th {
background-color: #d8e6f7;
font-weight: bold;
text-transform: uppercase;
}
.fmp-diff-positive { color: green; font-weight: bold; background-color: #e6ffe6; }
.fmp-diff-negative { color: red; font-weight: bold; background-color: #ffe6e6; }
.fmp-clear-btn {
background-color: #dc3545;
color: white;
border: none;
padding: 8px 15px;
border-radius: 4px;
cursor: pointer;
margin-left: 10px;
font-size: 0.9em;
transition: background-color 0.2s;
}
.fmp-clear-btn:hover {
background-color: #c82333;
}
/* Buttons */
#fmp-save-player-btn, #fmp-open-modal-btn {
color: white;
border: none;
padding: 8px 15px;
border-radius: 5px;
cursor: pointer;
font-weight: bold;
font-size: 14px;
margin-left: 10px;
white-space: nowrap;
transition: background-color 0.2s;
}
#fmp-save-player-btn {
background-color: #007bff;
}
#fmp-save-player-btn:hover {
background-color: #0056b3;
}
#fmp-open-modal-btn {
background-color: #28a745;
}
#fmp-open-modal-btn:hover {
background-color: #218838;
}
`);
// ===== CORE FUNCTIONS =====
function getPlayerMainPosition() {
const posElement = document.querySelector('.pitch-position');
if (posElement) return posElement.textContent.trim();
const positionElementB = document.querySelector('#playerdata .playerpos b');
if (positionElementB) {
const text = positionElementB.textContent.trim();
return text.split(' ')[0].replace(/[^a-zA-Z]/g, '').toUpperCase();
}
return t.unknownPosition;
}
function highlightSkills() {
const mainPosition = getPlayerMainPosition();
if (!positionSkills[mainPosition]) return;
const config = positionSkills[mainPosition];
const skillTable = document.querySelector('#playerdata .skilltable');
if (!skillTable) return;
const headers = skillTable.querySelectorAll('th');
headers.forEach((th, index) => {
const skillName = th.textContent.trim();
const isPrimary = config.primary.includes(skillName);
const isSecondary = config.secondary.includes(skillName);
if (isPrimary || isSecondary) {
if (isPrimary) th.classList.add('fmp-primary-skill');
if (isSecondary) th.classList.add('fmp-secondary-skill');
const parentRow = th.parentElement;
const valueRow = parentRow.nextElementSibling;
if (valueRow && valueRow.children[index]) {
const valueCell = valueRow.children[index];
const numSpan = valueCell.querySelector('.num');
const styleClass = isPrimary ? 'fmp-primary-skill' : 'fmp-secondary-skill';
if (numSpan) numSpan.classList.add(styleClass);
else valueCell.classList.add(styleClass);
}
}
});
}
function extractPlayerName() {
let nameElement = $('.lheader h3');
if (nameElement.length === 0) nameElement = $('h1');
if (nameElement.length === 0) nameElement = $('.lheader').find(':header').first();
if (nameElement.length > 0) {
let rawText = nameElement.contents().filter(function() {
return this.nodeType === 3;
}).text().trim();
if (!rawText) rawText = nameElement.text().trim();
let cleanName = rawText.replace(/^\d+\.\s*/, '').trim();
if (cleanName) return cleanName;
}
return t.unknownPlayer;
}
async function extractPlayerData() {
const player = {};
const urlParams = new URLSearchParams(window.location.search);
player.id = urlParams.get('id');
player.name = extractPlayerName();
player.position = getPlayerMainPosition();
if (!player.id) return null;
// Extract skills from table
player.skills = {};
const skillTable = $('#playerdata .skilltable');
const headers = skillTable.find('th');
const values = skillTable.find('td');
headers.each((index, th) => {
const skillName = $(th).text().trim();
const skillValueText = $(values[index]).text().trim();
const skillValue = parseInt(skillValueText.match(/(\d+)/)?.[0], 10) || 0;
if(skillName) {
player.skills[skillName] = skillValue;
}
});
// Extract additional data from JSON
try {
const response = await fetch(`/Team/Player?handler=PlayerData&playerId=${player.id}`);
if (response.ok) {
const json = await response.json();
if (json && json.player) {
if (json.player.age) player.age = `${json.player.age.years} ${t.age} ${json.player.age.months}M`;
if (json.player.wage) player.salary = json.player.wage.toLocaleString();
if (json.player.rating) {
player.rating = json.player.rating;
player.lastRating = json.player.rating;
}
if (json.player.qi) player.qi = json.player.qi;
}
}
} catch (e) {
console.error("JSON data fetch failed:", e);
}
// Fallback data extraction
const infoText = $('.infotable').text();
if (!player.age) {
const ageMatch = infoText.match(/(Yaş|Age|Alter|Edad|Âge|Età)\s*(\d+)[.,](\d+)/);
player.age = ageMatch ? `${ageMatch[2]} ${t.age} ${ageMatch[3]}M` : t.unknownPosition;
}
if (!player.salary || player.salary === t.unknownPosition) {
const wageMatch = infoText.match(/(Maaş|Wage|Gehalt|Salario|Salaire|Stipendio)\s*ⓕ\s*([\d,.]+)/);
player.salary = wageMatch ? wageMatch[2] : t.unknownPosition;
}
player.value = t.unknownPosition;
player.timestamp = new Date().toLocaleString();
player.lang = currentLang; // Store player's language
return player;
}
async function savePlayer() {
const player = await extractPlayerData();
if (!player) {
alert('Player data could not be fetched. Please refresh the page.');
return;
}
let savedPlayers = await GM_getValue('fmp_saved_players', {});
if (Object.keys(savedPlayers).length >= 5 && !savedPlayers[player.id]) {
alert(t.maxPlayers);
return;
}
savedPlayers[player.id] = player;
await GM_setValue('fmp_saved_players', savedPlayers);
alert(player.name + t.playerSaved);
await updateCompareModal();
}
// ===== MODAL FUNCTIONS =====
function createModal() {
if ($('#fmp-modal-overlay').length) return;
const modalHTML = `
<div class="fmp-modal-overlay" id="fmp-modal-overlay">
<div class="fmp-modal" id="fmp-modal">
<div class="fmp-modal-header" id="fmp-modal-header">
<h3 class="fmp-modal-title">${t.modalTitle}</h3>
<button class="fmp-modal-close" id="fmp-modal-close">×</button>
</div>
<div class="fmp-modal-content" id="fmp-modal-content">
<p>${t.noPlayers}</p>
</div>
</div>
</div>
`;
$('body').append(modalHTML);
// Close modal functionality
$('#fmp-modal-close, #fmp-modal-overlay').on('click', function(e) {
if (e.target === this) {
$('#fmp-modal-overlay').removeClass('active');
$('#fmp-modal').removeClass('active');
}
});
makeModalDraggable();
}
function makeModalDraggable() {
const modal = document.getElementById('fmp-modal');
const header = document.getElementById('fmp-modal-header');
let isDragging = false;
let currentX, currentY, initialX, initialY, xOffset = 0, yOffset = 0;
header.addEventListener("mousedown", dragStart);
document.addEventListener("mousemove", drag);
document.addEventListener("mouseup", dragEnd);
function dragStart(e) {
initialX = e.clientX - xOffset;
initialY = e.clientY - yOffset;
if (e.target === header || header.contains(e.target)) {
isDragging = true;
}
}
function drag(e) {
if (isDragging) {
e.preventDefault();
currentX = e.clientX - initialX;
currentY = e.clientY - initialY;
xOffset = currentX;
yOffset = currentY;
setTranslate(currentX, currentY, modal);
}
}
function setTranslate(xPos, yPos, el) {
el.style.transform = "translate3d(" + xPos + "px, " + yPos + "px, 0)";
}
function dragEnd(e) {
initialX = currentX;
initialY = currentY;
isDragging = false;
}
}
async function updateCompareModal() {
const players = await GM_getValue('fmp_saved_players', {});
const playerIds = Object.keys(players);
let contentHTML = '';
if (playerIds.length === 0) {
contentHTML = `<p>${t.noPlayers}</p>`;
} else {
contentHTML = `
<div style="margin-bottom: 15px;">
<button class="fmp-clear-btn" id="fmp-clear-btn">${t.clearList}</button>
<span style="margin-left: 10px; font-size: 0.9em; color: #666;">
${t.savedCount}: ${playerIds.length}/5
</span>
</div>
${createCompareTable(players)}
`;
}
$('#fmp-modal-content').html(contentHTML);
$('#fmp-clear-btn').on('click', async () => {
if (confirm(t.confirmClear)) {
await GM_setValue('fmp_saved_players', {});
await updateCompareModal();
}
});
}
function openModal() {
$('#fmp-modal-overlay').addClass('active');
$('#fmp-modal').addClass('active');
}
function createOpenModalButton() {
if ($('#fmp-open-modal-btn').length) return;
const $headerCell = $('.lheader h3').parent();
if ($headerCell.length) {
const $btn = $(`<button id="fmp-open-modal-btn">${t.compareWindow}</button>`);
$btn.on('click', openModal);
$('#fmp-save-player-btn').after($btn);
}
}
function createCompareTable(players) {
const playerIds = Object.keys(players);
const allSkills = new Set();
playerIds.forEach(id => {
if(players[id].skills) {
Object.keys(players[id].skills).forEach(skill => allSkills.add(skill));
}
});
const sortedSkills = Array.from(allSkills).sort();
let html = '<table id="fmp-compare-table"><thead><tr>';
html += `<th style="width:10%">${t.feature}</th>`;
playerIds.forEach(id => {
const displayName = (players[id].name && players[id].name !== t.unknownPlayer) ? players[id].name : `Player ${players[id].id}`;
html += `<th colspan="2" style="background-color:#2c5a8a; color:white;">${displayName.toUpperCase()} <br><small>(${players[id].position})</small></th>`;
});
html += '</tr><tr><th> </th>';
playerIds.forEach(() => { html += `<th>${t.points}</th><th>${t.difference}</th>`; });
html += '</tr></thead><tbody>';
const infoKeys = [
{ label: t.age, key: 'age' },
{ label: t.salary, key: 'salary' },
{ label: `${t.quality} (${t.rating})`, key: 'rating' },
{ label: 'QI', key: 'qi' }
];
infoKeys.forEach(info => {
html += `<tr><td><b>${info.label}</b></td>`;
playerIds.forEach(id => {
let val = players[id][info.key] || '-';
html += `<td colspan="2" style="font-weight:bold;">${val}</td>`;
});
html += '</tr>';
});
sortedSkills.forEach(skill => {
html += `<tr><td style="text-align:left;font-weight:bold;">${skill}</td>`;
const refVal = players[playerIds[0]].skills[skill] || 0;
playerIds.forEach((id, idx) => {
const val = players[id].skills[skill] || 0;
let diffHtml = '';
if (idx > 0) {
const diff = val - refVal;
if (diff > 0) diffHtml = `<span class="fmp-diff-positive">+${diff}</span>`;
else if (diff < 0) diffHtml = `<span class="fmp-diff-negative">${diff}</span>`;
else diffHtml = '<span style="color:gray">-</span>';
}
html += `<td>${val}</td><td>${diffHtml}</td>`;
});
html += '</tr>';
});
html += '</tbody></table>';
return html;
}
function createSaveButton() {
if ($('#fmp-save-player-btn').length) return;
const $headerCell = $('.lheader h3').parent();
if ($headerCell.length) {
const $btn = $(`<button id="fmp-save-player-btn">${t.savePlayer}</button>`);
$btn.on('click', savePlayer);
$('.lheader h3').after($btn);
}
}
// ===== INITIALIZATION =====
setTimeout(async () => {
createSaveButton();
createModal();
createOpenModalButton();
highlightSkills();
await updateCompareModal();
}, 1000);
})();