FMP Position Analyzer and Comparator

Multi-language FMP position analyzer and player comparison tool with auto-language detection

当前为 2025-11-26 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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>&nbsp;</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);

})();