TM Squad Viewer

Enhanced squad overview with R5/REC ratings, training table, skill tooltips and sortable tables

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

// ==UserScript==
// @name         TM Squad Viewer
// @namespace    https://trophymanager.com
// @version      1.1.0
// @description  Enhanced squad overview with R5/REC ratings, training table, skill tooltips and sortable tables
// @match        https://trophymanager.com/club/*/squad*
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    /* ═══════════════════════════════════════════════════════════
       CONSTANTS
       ═══════════════════════════════════════════════════════════ */
    const POS_MULTIPLIERS = [0.3, 0.3, 0.9, 0.6, 1.5, 0.9, 0.9, 0.6, 0.3];

    const COLOR_LEVELS = [
        { color: '#ff4c4c' }, { color: '#ff8c00' }, { color: '#ffd700' },
        { color: '#90ee90' }, { color: '#00cfcf' }, { color: '#5b9bff' }, { color: '#cc88ff' }
    ];

    const REC_THRESHOLDS = [5.5, 5, 4, 3, 2, 1, 0];
    const R5_THRESHOLDS = [110, 100, 90, 80, 70, 60, 0];
    const AGE_THRESHOLDS = [30, 28, 26, 24, 22, 20, 0];
    const RTN_THRESHOLDS = [90, 60, 40, 30, 20, 10, 0];
    const TI_THRESHOLDS  = [12, 9, 6, 4, 2, 1, -Infinity];

    const TRN_LABELS = ['Str/Wor/Sta', 'Mar/Tac', 'Cro/Pac', 'Pas/Tec/Set', 'Hea/Pos', 'Fin/Lon'];
    const TRN_DOT_COLORS = ['#555', '#ef4444', '#f59e0b', '#eab308', '#84cc16', '#22c55e'];

    /* ─── Weight tables ─────────────────────────────────────── */
    //              Str         Sta         Pac         Mar         Tac         Wor         Pos         Pas         Cro         Tec         Hea         Fin         Lon         Set
    const WEIGHT_R5 = [
        [0.41029304, 0.18048062, 0.56730138, 1.06344654, 1.02312672, 0.40831256, 0.58235457, 0.12717479, 0.05454137, 0.09089830, 0.42381693, 0.04626272, 0.02199046, 0], // DC
        [0.42126371, 0.18293193, 0.60567629, 0.91904794, 0.89070915, 0.40038476, 0.56146633, 0.15053902, 0.15955429, 0.15682932, 0.42109742, 0.09460329, 0.03589655, 0], // DL/R
        [0.23412419, 0.32032289, 0.62194779, 0.63162534, 0.63143081, 0.45218831, 0.47370658, 0.55054737, 0.17744915, 0.39932519, 0.26915814, 0.16413124, 0.07404301, 0], // DMC
        [0.27276905, 0.26814289, 0.61104798, 0.39865092, 0.42862643, 0.43582015, 0.46617076, 0.44931076, 0.25175412, 0.46446692, 0.29986350, 0.43843061, 0.21494592, 0], // DML/R
        [0.25219260, 0.25112993, 0.56090649, 0.18230261, 0.18376490, 0.45928749, 0.53498118, 0.59461481, 0.09851189, 0.61601950, 0.31243959, 0.65402884, 0.29982016, 0], // MC
        [0.28155678, 0.24090675, 0.60680245, 0.19068879, 0.20018012, 0.45148647, 0.48230007, 0.42982389, 0.26268609, 0.57933805, 0.31712419, 0.65824985, 0.29885649, 0], // ML/R
        [0.22029884, 0.29229690, 0.63248227, 0.09904394, 0.10043602, 0.47469498, 0.52919791, 0.77555880, 0.10531819, 0.71048302, 0.27667115, 0.56813972, 0.21537826, 0], // OMC
        [0.21151292, 0.35804710, 0.88688492, 0.14391236, 0.13769621, 0.46586605, 0.34446036, 0.51377701, 0.59723919, 0.75126119, 0.16550722, 0.29966502, 0.12417045, 0], // OML/R
        [0.35479780, 0.14887553, 0.43273380, 0.00023928, 0.00021111, 0.46931131, 0.57731335, 0.41686333, 0.05607604, 0.62121195, 0.45370457, 1.03660702, 0.43205492, 0], // F
        [0.45462811, 0.30278232, 0.45462811, 0.90925623, 0.45462811, 0.90925623, 0.45462811, 0.45462811, 0.30278232, 0.15139116, 0.15139116]  // GK
    ];

    //              Str         Sta         Pac         Mar         Tac         Wor         Pos         Pas         Cro         Tec         Hea         Fin         Lon         Set
    const WEIGHT_RB = [
        [0.10493615, 0.05208547, 0.07934211, 0.14448971, 0.13159554, 0.06553072, 0.07778375, 0.06669303, 0.05158306, 0.02753168, 0.12055170, 0.01350989, 0.02549169, 0.03887550], // DC
        [0.07715535, 0.04943315, 0.11627229, 0.11638685, 0.12893778, 0.07747251, 0.06370799, 0.03830611, 0.10361093, 0.06253997, 0.09128094, 0.01314110, 0.02449199, 0.03726305], // DL/R
        [0.08219824, 0.08668831, 0.07434242, 0.09661001, 0.08894242, 0.08998026, 0.09281287, 0.08868309, 0.04753574, 0.06042619, 0.05396986, 0.05059984, 0.05660203, 0.03060871], // DMC
        [0.06744248, 0.06641401, 0.09977251, 0.08253749, 0.09709316, 0.09241026, 0.08513703, 0.06127851, 0.10275520, 0.07985941, 0.04618960, 0.03927270, 0.05285911, 0.02697852], // DML/R
        [0.07304213, 0.08174111, 0.07248656, 0.08482334, 0.07078726, 0.09568392, 0.09464529, 0.09580381, 0.04746231, 0.07093008, 0.04595281, 0.05955544, 0.07161249, 0.03547345], // MC
        [0.06527363, 0.06410270, 0.09701305, 0.07406706, 0.08563595, 0.09648566, 0.08651209, 0.06357183, 0.10819222, 0.07386495, 0.03245554, 0.05430668, 0.06572005, 0.03279859], // ML/R
        [0.07842736, 0.07744888, 0.07201150, 0.06734457, 0.05002348, 0.08350204, 0.08207655, 0.11181914, 0.03756112, 0.07486004, 0.06533972, 0.07457344, 0.09781475, 0.02719742], // OMC
        [0.06545375, 0.06145378, 0.10503536, 0.06421508, 0.07627526, 0.09232981, 0.07763931, 0.07001035, 0.11307331, 0.07298351, 0.04248486, 0.06462713, 0.07038293, 0.02403557], // OML/R
        [0.07738289, 0.05022488, 0.07790481, 0.01356516, 0.01038191, 0.06495444, 0.07721954, 0.07701905, 0.02680715, 0.07759692, 0.12701687, 0.15378395, 0.12808992, 0.03805251], // F
        [0.07466384, 0.07466384, 0.07466384, 0.14932769, 0.10452938, 0.14932769, 0.10452938, 0.10344411, 0.07512610, 0.04492581, 0.04479831]  // GK
    ];

    const FIELD_LABELS = ['Str', 'Sta', 'Pac', 'Mar', 'Tac', 'Wor', 'Pos', 'Pas', 'Cro', 'Tec', 'Hea', 'Fin', 'Lon', 'Set'];
    const GK_LABELS = ['Str', 'Pac', 'Jum', 'Sta', 'One', 'Ref', 'Aer', 'Com', 'Kic', 'Thr', 'Han'];

    /* ═══════════════════════════════════════════════════════════
       UTILITIES
       ═══════════════════════════════════════════════════════════ */
    const fix2 = v => (Math.round(v * 100) / 100).toFixed(2);

    const getColor = (value, thresholds) => {
        for (let i = 0; i < thresholds.length; i++) {
            if (value >= thresholds[i]) return COLOR_LEVELS[i].color;
        }
        return COLOR_LEVELS[COLOR_LEVELS.length - 1].color;
    };

    const getPositionIndex = pos => {
        switch (pos.toLowerCase()) {
            case 'gk': return 9;
            case 'dc': case 'dcl': case 'dcr': return 0;
            case 'dr': case 'dl': return 1;
            case 'dmc': case 'dmcl': case 'dmcr': return 2;
            case 'dmr': case 'dml': return 3;
            case 'mc': case 'mcl': case 'mcr': return 4;
            case 'mr': case 'ml': return 5;
            case 'omc': case 'omcl': case 'omcr': return 6;
            case 'omr': case 'oml': return 7;
            case 'fc': case 'fcl': case 'fcr': case 'f': return 8;
            default: return 0;
        }
    };

    const posSortOrder = idx => idx === 9 ? 0 : idx + 1;

    const skillColor = v => {
        if (v >= 20) return '#d4af37';
        if (v >= 19) return '#c0c0c0';
        if (v >= 16) return '#66dd44';
        if (v >= 12) return '#cccc00';
        if (v >= 8) return '#ee9900';
        return '#ee6633';
    };

    const posGroupColor = posIdx => {
        if (posIdx === 9) return '#4ade80';             // GK – green
        if (posIdx <= 1) return '#60a5fa';             // DC, DL/R – blue
        if (posIdx <= 7) return '#fbbf24';             // DMC–OML/R – yellow
        return '#f87171';                               // F – red
    };

    /* ─── Season / TI helpers ────────────────────────────────── */
    const TRAINING1   = new Date('2023-01-16T23:00:00Z');   // s65 first training
    const SEASON_DAYS = 84;
    const WAGE_RATE   = 15.8079;
    const WAGE_RATE_NEW = 23.75;

    const getCurrentSession = () => {
        const now = new Date();
        let day = (now.getTime() - TRAINING1.getTime()) / 1000 / 3600 / 24;
        while (day > SEASON_DAYS - 16 / 24) day -= SEASON_DAYS;
        const s = Math.floor(day / 7) + 1;
        return s <= 0 ? 12 : s;
    };

    const currentSession = getCurrentSession();

    const calculateTI = (asi, wage, isGK) => {
        if (!asi || !wage || wage <= 30000) return null;
        const w  = isGK ? 48717927500 : 263533760000;
        const wr = WAGE_RATE;
        const { pow, log, round } = Math;
        const log27 = log(pow(2, 7));
        return round((pow(2, log(w * asi) / log27) - pow(2, log(w * wage / wr) / log27)) * 10);
    };

    /* ═══════════════════════════════════════════════════════════
       R5 & REC CALCULATIONS
       ═══════════════════════════════════════════════════════════ */
    const calculateRemainders = (posIdx, skills, asi) => {
        const weight = posIdx === 9 ? 48717927500 : 263533760000;
        const skillSum = skills.reduce((sum, s) => sum + parseInt(s), 0);
        const remainder = Math.round((Math.pow(2, Math.log(weight * asi) / Math.log(Math.pow(2, 7))) - skillSum) * 10) / 10;

        let rec = 0, ratingR = 0, remainderW1 = 0, remainderW2 = 0, not20 = 0;
        for (let i = 0; i < WEIGHT_RB[posIdx].length; i++) {
            rec += skills[i] * WEIGHT_RB[posIdx][i];
            ratingR += skills[i] * WEIGHT_R5[posIdx][i];
            if (skills[i] != 20) {
                remainderW1 += WEIGHT_RB[posIdx][i];
                remainderW2 += WEIGHT_R5[posIdx][i];
                not20++;
            }
        }
        if (remainder / not20 > 0.9 || !not20) {
            not20 = posIdx === 9 ? 11 : 14;
            remainderW1 = 1;
            remainderW2 = 5;
        }
        rec = fix2((rec + remainder * remainderW1 / not20 - 2) / 3);
        return { remainder, remainderW2, not20, ratingR, rec };
    };

    const calculateBaseR5 = (posIdx, skills, asi, rou) => {
        const r = calculateRemainders(posIdx, skills, asi);
        const routineBonus = (3 / 100) * (100 - 100 * Math.pow(Math.E, -rou * 0.035));
        const ratingR = r.ratingR + (r.remainder * r.remainderW2 / r.not20);
        return Number(fix2(ratingR + routineBonus * 5));
    };

    const calculateR5 = (posIdx, skills, asi, rou) => {
        let rating = calculateBaseR5(posIdx, skills, asi, rou);
        const rou2 = (3 / 100) * (100 - 100 * Math.pow(Math.E, -rou * 0.035));
        const r = calculateRemainders(posIdx, skills, asi);
        const { pow, E } = Math;

        const goldstar = skills.filter(s => s == 20).length;
        const skillsB = skills.map(s => s == 20 ? 20 : s * 1 + r.remainder / (skills.length - goldstar));
        const sr = skillsB.map((s, i) => i === 1 ? s : s + rou2);

        if (skills.length !== 11) {
            const headerBonus = sr[10] > 12
                ? fix2((pow(E, (sr[10] - 10) ** 3 / 1584.77) - 1) * 0.8 +
                    pow(E, sr[0] ** 2 * 0.007 / 8.73021) * 0.15 +
                    pow(E, sr[6] ** 2 * 0.007 / 8.73021) * 0.05)
                : 0;
            const fkBonus = fix2(pow(E, (sr[13] + sr[12] + sr[9] * 0.5) ** 2 * 0.002) / 327.92526);
            const ckBonus = fix2(pow(E, (sr[13] + sr[8] + sr[9] * 0.5) ** 2 * 0.002) / 983.65770);
            const pkBonus = fix2(pow(E, (sr[13] + sr[11] + sr[9] * 0.5) ** 2 * 0.002) / 1967.31409);
            const allBonus = headerBonus * 1 + fkBonus * 1 + ckBonus * 1 + pkBonus * 1;

            const defSkillsSq = sr[0] ** 2 + sr[1] ** 2 * 0.5 + sr[2] ** 2 * 0.5 + sr[3] ** 2 + sr[4] ** 2 + sr[5] ** 2 + sr[6] ** 2;
            const gainBase = fix2(defSkillsSq / 6 / 22.9 ** 2);
            const offSkillsSq = sr[0] ** 2 * 0.5 + sr[1] ** 2 * 0.5 + sr[2] ** 2 + sr[3] ** 2 + sr[4] ** 2 + sr[5] ** 2 + sr[6] ** 2;
            const keepBase = fix2(offSkillsSq / 6 / 22.9 ** 2);

            const m = POS_MULTIPLIERS[posIdx];
            return fix2(rating + allBonus + gainBase * m * 1 + keepBase * m * 1);
        }
        return fix2(rating);
    };

    /* ═══════════════════════════════════════════════════════════
       SKILL EXTRACTION FROM POST DATA
       ═══════════════════════════════════════════════════════════ */
    const extractSkills = p => {
        const isGK = parseInt(p.handling) > 0;
        if (isGK) {
            // Weight order: Str, Pac, Jum, Sta, One, Ref, Aer, Com, Kic, Thr, Han
            return {
                isGK: true,
                skills: [
                    parseInt(p.strength), parseInt(p.pace), parseInt(p.jumping),
                    parseInt(p.stamina), parseInt(p.oneonones), parseInt(p.reflexes),
                    parseInt(p.arialability), parseInt(p.communication),
                    parseInt(p.kicking), parseInt(p.throwing), parseInt(p.handling)
                ],
                labels: GK_LABELS
            };
        }
        // Weight order: Str, Sta, Pac, Mar, Tac, Wor, Pos, Pas, Cro, Tec, Hea, Fin, Lon, Set
        return {
            isGK: false,
            skills: [
                parseInt(p.strength), parseInt(p.stamina), parseInt(p.pace),
                parseInt(p.marking), parseInt(p.tackling), parseInt(p.workrate),
                parseInt(p.positioning), parseInt(p.passing), parseInt(p.crossing),
                parseInt(p.technique), parseInt(p.heading), parseInt(p.finishing),
                parseInt(p.longshots), parseInt(p.setpieces)
            ],
            labels: FIELD_LABELS
        };
    };

    /* ═══════════════════════════════════════════════════════════
       DATA PROCESSING
       ═══════════════════════════════════════════════════════════ */
    let processed = false;
    let allPlayers = [];
    let bTeamPlayers = [];
    let bTeamFetched = false;
    let bTeamName = '';
    let fetchingBTeam = false;
    const onSaleIds = new Set();

    const scanForSales = () => {
        document.querySelectorAll('img[src*="auction_hammer"]').forEach(img => {
            const tr = img.closest('tr');
            if (!tr) return;
            const link = tr.querySelector('a[href*="/players/"]');
            if (!link) return;
            const m = link.getAttribute('href').match(/\/players\/(\d+)/);
            if (m) onSaleIds.add(m[1]);
        });
    };

    const mapPlayer = p => {
        const { isGK, skills, labels } = extractSkills(p);
        const positions = (p.favposition || 'dc').split(',').map(s => s.trim());
        positions.sort((a, b) => getPositionIndex(a) - getPositionIndex(b));
        const posIdx = getPositionIndex(positions[0]);
        const asi = parseInt(p.asi) || 0;
        const routine = parseFloat(p.rutine) || 0;

        let r5 = 0, rec = 0;
        if (asi > 0 && skills.every(s => !isNaN(s))) {
            r5 = Number(calculateR5(posIdx, skills, asi, routine));
            rec = Number(calculateRemainders(posIdx, skills, asi).rec);
            if (positions.length > 1) {
                const posIdx2 = getPositionIndex(positions[1]);
                if (posIdx2 !== posIdx) {
                    const r5_2 = Number(calculateR5(posIdx2, skills, asi, routine));
                    const rec_2 = Number(calculateRemainders(posIdx2, skills, asi).rec);
                    if (r5_2 > r5) r5 = r5_2;
                    if (rec_2 > rec) rec = rec_2;
                }
            }
        }

        const wage = parseInt(p.wage) || 0;
        const tiRaw = calculateTI(asi, wage, isGK);
        const ti = tiRaw !== null && currentSession > 0 ? Number((tiRaw / currentSession).toFixed(1)) : null;

        return {
            id: p.player_id,
            name: p.player_name || p.player_name_long || '',
            country: p.player_country || p.country || '',
            pos: positions.map(s => s.toUpperCase()).join(', '),
            posList: positions.map(s => ({ name: s.toUpperCase(), idx: getPositionIndex(s) })),
            favpos: p.favposition || '',
            posIdx, isGK,
            age: parseInt(p.age) || 0,
            month: parseInt(p.month) || 0,
            asi, r5, rec, routine, ti,
            no: parseInt(p.no) || 0,
            gp: parseInt(p.stats?.gp) || 0,
            goals: parseInt(p.stats?.goals) || 0,
            assists: parseInt(p.stats?.assists) || 0,
            rating: parseInt(p.stats?.rating) || 0,
            mom: parseInt(p.stats?.mom) || 0,
            cards: parseInt(p.stats?.cards) || 0,
            ga: parseInt(p.stats?.ga) || 0,
            wage,
            ban: p.ban || '0',
            banPoints: parseInt(p.ban_points) || parseInt(p.strafpoint) || 0,
            injury: p.injury || '0',
            training: p.training || '',
            trainingCustom: p.training_custom || '',
            skills, labels,
            retire: p.retire || '0'
        };
    };

    const processData = data => {
        if (!data || !data.post || !Object.keys(data.post).length) return;
        allPlayers = Object.values(data.post).map(mapPlayer);
        processed = true;
        renderPanel();

        if (!bTeamFetched) {
            bTeamFetched = true;
            const clubId = location.pathname.match(/\/club\/(\d+)/)?.[1];
            if (clubId) fetchBTeamInfo(clubId);
        }
    };

    const fetchBTeamInfo = clubId => {
        fetch(`/club/${clubId}/`)
            .then(r => r.text())
            .then(html => {
                const idMatch = html.match(/B-Team:\s*<\/strong>\s*<a\s+href="\/club\/(\d+)\//)
                    || html.match(/B-Team:[\s\S]*?\/club\/(\d+)\//);
                if (!idMatch) return;
                const bTeamId = idMatch[1];
                const nameMatch = html.match(/B-Team:\s*<\/strong>\s*<a[^>]*>([^<]+)<\/a>/)
                    || html.match(/B-Team:[\s\S]*?club_link='\d+'>([^<]+)<\/a>/);
                bTeamName = nameMatch ? nameMatch[1].trim() : 'B-Team';
                fetchingBTeam = true;
                $.post('/ajax/players_get_select.ajax.php', { type: 'change', club_id: bTeamId })
                    .done(res => {
                        fetchingBTeam = false;
                        try {
                            const data = typeof res === 'string' ? JSON.parse(res) : res;
                            if (data && data.post && Object.keys(data.post).length) {
                                bTeamPlayers = Object.values(data.post).map(p => {
                                    const pl = mapPlayer(p);
                                    pl.isBTeam = true;
                                    return pl;
                                });
                                renderPanel();
                            }
                        } catch (e) { /* ignore */ }
                    })
                    .fail(() => { fetchingBTeam = false; });
            })
            .catch(() => { });
    };

    /* ═══════════════════════════════════════════════════════════
       CSS
       ═══════════════════════════════════════════════════════════ */
    const injectCSS = () => {
        const style = document.createElement('style');
        style.textContent = `
            #tmsq-panel {
                background: #1c3410;
                border-radius: 10px;
                padding: 14px;
                margin: 10px 0 16px 0;
                font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
                color: #c8e0b4;
                box-shadow: 0 4px 24px rgba(0,0,0,0.5);
                border: 1px solid #2a4a1c;
            }
            #tmsq-panel * { box-sizing: border-box; }

            .tmsq-header {
                display: flex; align-items: center; justify-content: space-between;
                margin-bottom: 10px;
            }
            .tmsq-title {
                font-size: 15px; font-weight: 700; color: #fff;
                display: flex; align-items: center; gap: 6px;
            }

            .tmsq-summary {
                display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px;
                padding: 8px 10px; background: #162e0e; border-radius: 8px;
            }
            .tmsq-sum-item {
                display: flex; flex-direction: column; align-items: center;
                min-width: 72px; padding: 5px 10px;
                background: #1c3410; border-radius: 6px;
            }
            .tmsq-sum-lbl { font-size: 9px; color: #6a9a58; text-transform: uppercase; letter-spacing: 0.5px; margin-top: 2px; }
            .tmsq-sum-val { font-size: 15px; font-weight: 700; }

            .tmsq-table-wrap {
                overflow-x: auto; border-radius: 8px;
                border: 1px solid #2a4a1c;
            }
            .tmsq-table {
                width: 100%; border-collapse: collapse; font-size: 12px;
            }
            .tmsq-table thead th {
                background: #162e0e; color: #6a9a58; padding: 6px 7px;
                text-align: left; font-size: 10px; font-weight: 700;
                text-transform: uppercase; letter-spacing: 0.4px;
                border-bottom: 1px solid #2a4a1c; cursor: pointer;
                user-select: none; white-space: nowrap;
                position: sticky; top: 0; z-index: 2;
            }
            .tmsq-table thead th:hover { color: #c8e0b4; background: #243d18; }
            .tmsq-table thead th.sorted { color: #6cc040; }

            .tmsq-table tbody tr {
                border-bottom: 1px solid rgba(42,74,28,.4);
                transition: background 0.12s;
            }
            .tmsq-table tbody tr:nth-child(odd) { background: #1c3410; }
            .tmsq-table tbody tr:nth-child(even) { background: #162e0e; }
            .tmsq-table tbody tr:hover { background: #243d18 !important; }

            .tmsq-table td {
                padding: 4px 7px; white-space: nowrap; vertical-align: middle;
            }
            .tmsq-table td.r, .tmsq-table th.r { text-align: right; }
            .tmsq-table td.c, .tmsq-table th.c { text-align: center; }
            .tmsq-table .pos-bar {
                width: 3px; padding: 0; border-radius: 2px;
            }

            .tmsq-link {
                color: #90b878; text-decoration: none; font-weight: 500;
            }
            .tmsq-link:hover { color: #c8e0b4; text-decoration: underline; }

            .tmsq-flag {
                margin-right: 4px; vertical-align: middle;
            }

            .tmsq-status { font-size: 10px; margin-left: 3px; vertical-align: middle; }

            .tmsq-section-lbl {
                font-size: 12px; font-weight: 700; color: #6cc040;
                text-transform: uppercase; letter-spacing: 0.5px;
                margin-bottom: 6px; padding: 4px 0;
                border-bottom: 1px solid #2a4a1c;
            }

            .tmsq-pos-chip {
                display: inline-block; padding: 1px 6px; border-radius: 4px;
                font-size: 10px; font-weight: 700; letter-spacing: 0.3px;
                line-height: 16px; text-align: center; min-width: 28px;
            }
            .tmsq-bteam-badge {
                display: inline-block; margin-left: 4px; padding: 0 4px;
                font-size: 9px; font-weight: 700; color: #f59e0b;
                background: rgba(245,158,11,0.15); border: 1px solid rgba(245,158,11,0.3);
                border-radius: 3px; vertical-align: middle; line-height: 14px;
            }
            .tmsq-sale-badge {
                display: inline-block; margin-left: 4px; padding: 0 4px;
                font-size: 9px; font-weight: 700; color: #ef4444;
                background: rgba(239,68,68,0.15); border: 1px solid rgba(239,68,68,0.3);
                border-radius: 3px; vertical-align: middle; line-height: 14px;
            }
            .tmsq-trn-dots {
                display: inline-flex; gap: 2px; margin-left: 4px;
            }
            .tmsq-trn-dot {
                display: inline-block; width: 14px; height: 14px;
                border-radius: 3px; text-align: center; line-height: 14px;
                font-size: 9px; font-weight: 700; color: #000;
            }
            .tmsq-card {
                display: inline-block; width: 10px; height: 14px;
                border-radius: 2px; margin-left: 4px; vertical-align: middle;
                box-shadow: 0 1px 2px rgba(0,0,0,0.4);
            }
            .tmsq-card-yellow { background: #eab308; }
            .tmsq-card-red {
                background: #ef4444; position: relative;
                font-size: 8px; font-weight: 700; color: #fff;
                text-align: center; line-height: 14px;
            }


            /* Skill tooltip */
            .tmsq-tip {
                display: none; position: absolute; z-index: 9999;
                background: linear-gradient(135deg, #1a2e14 0%, #243a1a 100%);
                border: 1px solid #4a9030; border-radius: 8px;
                padding: 10px 12px; min-width: 200px; max-width: 280px;
                box-shadow: 0 6px 24px rgba(0,0,0,0.6);
                pointer-events: none; font-size: 11px; color: #c8e0b4;
            }
            .tmsq-tip-header {
                display: flex; align-items: center; gap: 8px;
                margin-bottom: 8px; padding-bottom: 6px;
                border-bottom: 1px solid rgba(74,144,48,0.3);
            }
            .tmsq-tip-name { font-size: 13px; font-weight: 700; color: #e0f0cc; }
            .tmsq-tip-pos { font-size: 10px; color: #8abc78; font-weight: 600; }
            .tmsq-tip-badges { display: flex; gap: 6px; margin-left: auto; }
            .tmsq-tip-badge {
                font-size: 10px; font-weight: 700; padding: 2px 6px;
                border-radius: 4px; background: rgba(0,0,0,0.3);
            }
            .tmsq-tip-skills {
                display: flex; gap: 12px; margin-bottom: 6px;
            }
            .tmsq-tip-skills-col {
                flex: 1; min-width: 0;
            }
            .tmsq-tip-skill {
                display: flex; justify-content: space-between;
                padding: 1px 0; border-bottom: 1px solid rgba(74,144,48,0.12);
            }
            .tmsq-tip-skill-name { color: #8abc78; font-size: 10px; }
            .tmsq-tip-skill-val { font-weight: 700; font-size: 11px; }
            .tmsq-tip-footer {
                display: flex; gap: 8px; justify-content: center;
                padding-top: 6px; border-top: 1px solid rgba(74,144,48,0.3);
            }
            .tmsq-tip-stat { text-align: center; }
            .tmsq-tip-stat-val { font-size: 14px; font-weight: 800; }
            .tmsq-tip-stat-lbl { font-size: 9px; color: #6a9a58; text-transform: uppercase; }
        `;
        document.head.appendChild(style);
    };

    /* ═══════════════════════════════════════════════════════════
       RENDERING
       ═══════════════════════════════════════════════════════════ */
    let sortCol = 'pos', sortDir = 1;

    const getSortVal = (p, col) => {
        if (col === 'age') return p.age * 12 + p.month;
        if (col === 'pos') {
            let v = posSortOrder(p.posIdx) * 100;
            if (p.posList.length > 1) v += 50 + posSortOrder(p.posList[1].idx);
            return v;
        }
        return p[col];
    };

    const sortPlayers = players => {
        players.sort((a, b) => {
            const va = getSortVal(a, sortCol);
            const vb = getSortVal(b, sortCol);
            if (sortCol === 'name') return sortDir * String(va).localeCompare(String(vb));
            return sortDir * ((va || 0) - (vb || 0));
        });
    };

    const statusIcons = p => {
        let s = '';
        if (p.ban === 'g') {
            s += `<span class="tmsq-card tmsq-card-yellow" title="Yellow card accumulation"></span>`;
        } else if (p.ban && p.ban.startsWith('r')) {
            const matches = p.ban.slice(1) || '1';
            s += `<span class="tmsq-card tmsq-card-red" title="Red card (${matches} match${matches==='1'?'':'es'})">${matches}</span>`;
        }
        if (p.injury && p.injury !== '0') {
            s += `<span style="margin-left:4px;color:#ef4444;font-size:12px;font-weight:700;vertical-align:middle" title="Injury: ${p.injury} weeks">✚${p.injury}</span>`;
        }
        if (p.retire && p.retire !== '0') {
            s += `<img src="http://trophymanager.com/pics/icons/retire.gif" style="margin-left:4px;vertical-align:middle;width:14px;height:14px" title="Retiring">`;
        }
        return s;
    };

    // Tooltip element
    let tip;
    const ensureTip = () => {
        if (tip) return;
        tip = document.createElement('div');
        tip.className = 'tmsq-tip';
        document.body.appendChild(tip);
    };

    const showTip = (anchor, p) => {
        ensureTip();
        // Header: name, pos, age, R5 badge
        let h = '<div class="tmsq-tip-header">';
        h += `<div><div class="tmsq-tip-name">${p.name}</div>`;
        const ageDisplay = `${p.age}.${String(p.month).padStart(2,'0')}`;
        h += `<div class="tmsq-tip-pos">${p.pos.toUpperCase()} · #${p.no} · Age ${ageDisplay}</div></div>`;
        h += '<div class="tmsq-tip-badges">';
        h += `<span class="tmsq-tip-badge" style="color:${getColor(p.r5, R5_THRESHOLDS)}">R5 ${p.r5.toFixed(2)}</span>`;
        h += '</div></div>';

        // Skills two-column layout
        const fieldLeft  = [0,1,2,3,4,5,6];
        const fieldRight = [7,8,9,10,11,12,13];
        const gkLeft  = [0,3,1];
        const gkRight = [10,4,5,6,2,7,8,9];
        const leftIdx  = p.isGK ? gkLeft : fieldLeft;
        const rightIdx = p.isGK ? gkRight : fieldRight;

        const renderCol = (indices) => {
            let c = '<div class="tmsq-tip-skills-col">';
            indices.forEach(i => {
                const val = p.skills[i];
                const display = val >= 19 ? '★' : String(val);
                c += '<div class="tmsq-tip-skill">';
                c += `<span class="tmsq-tip-skill-name">${p.labels[i]}</span>`;
                c += `<span class="tmsq-tip-skill-val" style="color:${skillColor(val)}">${display}</span>`;
                c += '</div>';
            });
            c += '</div>';
            return c;
        };
        h += '<div class="tmsq-tip-skills">';
        h += renderCol(leftIdx);
        h += renderCol(rightIdx);
        h += '</div>';

        // Footer: ASI, REC, Routine
        h += '<div class="tmsq-tip-footer">';
        h += `<div class="tmsq-tip-stat"><div class="tmsq-tip-stat-val" style="color:#e0f0cc">${p.asi.toLocaleString()}</div><div class="tmsq-tip-stat-lbl">ASI</div></div>`;
        h += `<div class="tmsq-tip-stat"><div class="tmsq-tip-stat-val" style="color:${getColor(Number(p.rec), REC_THRESHOLDS)}">${Number(p.rec).toFixed(2)}</div><div class="tmsq-tip-stat-lbl">REC</div></div>`;
        h += `<div class="tmsq-tip-stat"><div class="tmsq-tip-stat-val" style="color:#8abc78">${p.routine.toFixed(1)}</div><div class="tmsq-tip-stat-lbl">Routine</div></div>`;
        h += '</div>';

        tip.innerHTML = h;
        tip.style.display = 'block';

        const rect = anchor.getBoundingClientRect();
        let top = rect.bottom + window.scrollY + 4;
        let left = rect.left + window.scrollX;
        tip.style.top = top + 'px';
        tip.style.left = left + 'px';
        requestAnimationFrame(() => {
            const tipRect = tip.getBoundingClientRect();
            if (tipRect.right > window.innerWidth - 10) {
                tip.style.left = (window.innerWidth - tipRect.width - 10) + 'px';
            }
            if (tipRect.bottom > window.innerHeight + window.scrollY - 10) {
                tip.style.top = (rect.top + window.scrollY - tipRect.height - 4) + 'px';
            }
        });
    };

    const hideTip = () => { if (tip) tip.style.display = 'none'; };

    // Column definitions
    const cols = [
        { key: 'no', lbl: '#', align: 'r' },
        { key: 'name', lbl: 'Player' },
        { key: 'pos', lbl: 'Pos', align: 'c' },
        { key: 'age', lbl: 'Age', align: 'r' },
        { key: 'asi', lbl: 'ASI', align: 'r' },
        { key: 'r5', lbl: 'R5', align: 'r' },
        { key: 'rec', lbl: 'REC', align: 'r' },
        { key: 'ti', lbl: 'TI', align: 'r' },
        { key: 'routine', lbl: 'Rtn', align: 'r' },
        { key: 'trn', lbl: 'Trn', align: 'c' },
    ];

    const renderTrainingDots = tc => {
        if (!tc || tc.length !== 6) return '<span style="color:#555">—</span>';
        let h = '<span class="tmsq-trn-dots" title="' + tc.split('').map((d, i) => TRN_LABELS[i] + ': ' + d).join('  ') + '">';
        for (let i = 0; i < 6; i++) {
            const v = parseInt(tc[i]) || 0;
            h += `<span class="tmsq-trn-dot" style="background:${TRN_DOT_COLORS[v]}">${v}</span>`;
        }
        h += '</span>';
        return h;
    };

    const buildSummary = (players) => {
        const n = players.length;
        const avgR5 = n ? players.reduce((s, p) => s + p.r5, 0) / n : 0;
        const avgRec = n ? players.reduce((s, p) => s + Number(p.rec), 0) / n : 0;
        const avgAge = n ? players.reduce((s, p) => s + p.age + p.month / 12, 0) / n : 0;
        const avgASI = n ? players.reduce((s, p) => s + p.asi, 0) / n : 0;
        const tiPlayers = players.filter(p => p.ti !== null);
        const avgTI = tiPlayers.length ? tiPlayers.reduce((s, p) => s + p.ti, 0) / tiPlayers.length : 0;
        const hasTI = tiPlayers.length > 0;
        let h = '<div class="tmsq-summary">';
        h += `<div class="tmsq-sum-item"><span class="tmsq-sum-val">${n}</span><span class="tmsq-sum-lbl">Players</span></div>`;
        h += `<div class="tmsq-sum-item"><span class="tmsq-sum-val" style="color:${getColor(avgR5, R5_THRESHOLDS)}">${avgR5.toFixed(2)}</span><span class="tmsq-sum-lbl">Avg R5</span></div>`;
        h += `<div class="tmsq-sum-item"><span class="tmsq-sum-val" style="color:${getColor(avgRec, REC_THRESHOLDS)}">${avgRec.toFixed(2)}</span><span class="tmsq-sum-lbl">Avg REC</span></div>`;
        if (hasTI) h += `<div class="tmsq-sum-item"><span class="tmsq-sum-val" style="color:${getColor(avgTI, TI_THRESHOLDS)}">${avgTI.toFixed(1)}</span><span class="tmsq-sum-lbl">Avg TI</span></div>`;
        h += `<div class="tmsq-sum-item"><span class="tmsq-sum-val" style="color:${getColor(avgAge, AGE_THRESHOLDS)}">${avgAge.toFixed(1)}</span><span class="tmsq-sum-lbl">Avg Age</span></div>`;
        h += `<div class="tmsq-sum-item"><span class="tmsq-sum-val" style="color:#e0f0cc">${Math.round(avgASI).toLocaleString()}</span><span class="tmsq-sum-lbl">Avg ASI</span></div>`;
        h += '</div>';
        return h;
    };

    const buildTable = (players) => {
        let h = '<div class="tmsq-table-wrap"><table class="tmsq-table"><thead><tr>';
        h += '<th style="width:4px;padding:0"></th>';
        cols.forEach(c => {
            const sorted = sortCol === c.key;
            const arrow = sorted ? (sortDir > 0 ? ' ▲' : ' ▼') : '';
            const cls = [c.align || '', sorted ? 'sorted' : ''].filter(Boolean).join(' ');
            h += `<th data-col="${c.key}"${cls ? ` class="${cls}"` : ''}>${c.lbl}${arrow}</th>`;
        });
        h += '</tr></thead><tbody>';
        players.forEach(p => {
            const ageStr = `${p.age}.${String(p.month).padStart(2, '0')}`;
            const flag = p.country ? `<ib class="flag-img-${p.country} tmsq-flag"></ib>` : '';
            const status = statusIcons(p);
            const posClr = posGroupColor(p.posIdx);
            h += `<tr data-pid="${p.id}">`;
            h += `<td class="pos-bar" style="background:${posClr}"></td>`;
            h += `<td class="r">${p.no}</td>`;
            const bBadge = p.isBTeam ? '<span class="tmsq-bteam-badge">B</span>' : '';
            const saleBadge = onSaleIds.has(String(p.id)) ? '<span class="tmsq-sale-badge">💰</span>' : '';
            h += `<td>${flag}<a href="/players/${p.id}/" class="tmsq-link">${p.name}</a>${bBadge}${saleBadge}${status}</td>`;
            const chipClr = p.posIdx === 9 ? '#4ade80'
                : p.posList.some(pp => pp.idx <= 1) ? '#60a5fa'
                : p.posList.some(pp => pp.idx === 8) ? '#f87171' : '#fbbf24';
            const chipInner = p.posList.map(pp =>
                `<span style="color:${posGroupColor(pp.idx)}">${pp.name}</span>`
            ).join('<span style="color:#6a9a58">, </span>');
            h += `<td class="c"><span class="tmsq-pos-chip" style="background:${chipClr}22;border:1px solid ${chipClr}44">${chipInner}</span></td>`;
            h += `<td class="r" style="color:${getColor(p.age, AGE_THRESHOLDS)}">${ageStr}</td>`;
            h += `<td class="r" style="color:#e0f0cc">${p.asi.toLocaleString()}</td>`;
            h += `<td class="r" style="color:${getColor(p.r5, R5_THRESHOLDS)};font-weight:700">${p.r5.toFixed(2)}</td>`;
            h += `<td class="r" style="color:${getColor(Number(p.rec), REC_THRESHOLDS)};font-weight:700">${Number(p.rec).toFixed(2)}</td>`;
            h += p.ti !== null
                ? `<td class="r" style="color:${getColor(p.ti, TI_THRESHOLDS)}">${p.ti.toFixed(1)}</td>`
                : '<td class="r" style="color:#555">—</td>';
            h += `<td class="r" style="color:${getColor(p.routine, RTN_THRESHOLDS)}">${p.routine.toFixed(1)}</td>`;
            h += `<td class="c">${renderTrainingDots(p.trainingCustom)}</td>`;
            h += '</tr>';
        });
        h += '</tbody></table></div>';
        return h;
    };

    const renderPanel = () => {
        scanForSales();
        const all = [...allPlayers, ...bTeamPlayers];
        sortPlayers(all);

        const seniors = all.filter(p => p.age > 21);
        const youth = all.filter(p => p.age <= 21);

        let panel = document.getElementById('tmsq-panel');
        if (panel) panel.remove();

        panel = document.createElement('div');
        panel.id = 'tmsq-panel';

        let h = '';
        h += '<div class="tmsq-header"><div class="tmsq-title">⭐ Squad Overview</div></div>';

        // Seniors table
        h += `<div class="tmsq-section-lbl">Seniors (${seniors.length})</div>`;
        h += buildSummary(seniors);
        h += buildTable(seniors);

        // Youth table
        h += `<div class="tmsq-section-lbl" style="margin-top:14px">Youth ≤21 (${youth.length})</div>`;
        h += buildSummary(youth);
        h += buildTable(youth);

        panel.innerHTML = h;

        // Replace column2_a content with our panel
        const target = document.querySelector('.column2_a');
        if (target) {
            target.innerHTML = '';
            target.appendChild(panel);
        } else {
            const fallback = document.querySelector('#middle_column') || document.body;
            fallback.insertBefore(panel, fallback.firstChild);
        }
        widenLayout();

        // Sort handlers
        panel.querySelectorAll('th[data-col]').forEach(th => {
            th.addEventListener('click', () => {
                const col = th.dataset.col;
                if (sortCol === col) sortDir *= -1;
                else { sortCol = col; sortDir = (col === 'name' || col === 'pos') ? 1 : -1; }
                renderPanel();
            });
        });

        // Skill tooltip on player name hover
        panel.querySelectorAll('tr[data-pid]').forEach(tr => {
            const nameLink = tr.querySelector('.tmsq-link');
            if (!nameLink) return;
            nameLink.addEventListener('mouseenter', () => {
                const p = allPlayers.find(pl => pl.id === tr.dataset.pid) || bTeamPlayers.find(pl => pl.id === tr.dataset.pid);
                if (p) showTip(nameLink, p);
            });
            nameLink.addEventListener('mouseleave', hideTip);
        });
    };

    /* ═══════════════════════════════════════════════════════════
       XHR HOOK — intercept squad data responses
       ═══════════════════════════════════════════════════════════ */
    const origOpen = XMLHttpRequest.prototype.open;
    XMLHttpRequest.prototype.open = function (method, url, ...args) {
        this._tmsqUrl = url;
        return origOpen.call(this, method, url, ...args);
    };
    const origSend = XMLHttpRequest.prototype.send;
    XMLHttpRequest.prototype.send = function (body) {
        if (this._tmsqUrl && this._tmsqUrl.includes('players_get_select.ajax.php') && !fetchingBTeam) {
            this.addEventListener('load', function () {
                try {
                    const data = typeof this.responseText === 'string'
                        ? JSON.parse(this.responseText) : this.responseText;
                    if (data && data.post && Object.keys(data.post).length) {
                        processData(data);
                    }
                } catch (e) { /* ignore */ }
            });
        }
        return origSend.call(this, body);
    };

    /* ═══════════════════════════════════════════════════════════
       INITIALIZATION
       ═══════════════════════════════════════════════════════════ */
    injectCSS();

    // Remove column3_a and widen column2_a
    const widenLayout = () => {
        const col3 = document.querySelector('.column3_a');
        if (col3) col3.remove();
        const col2 = document.querySelector('.column2_a');
        if (col2) col2.style.width = '790px';
    };

    const tryFetch = () => {
        if (processed) return;
        const clubId = location.pathname.match(/\/club\/(\d+)/)?.[1];
        if (!clubId) return;

        const waitForJQ = setInterval(() => {
            if (typeof $ === 'undefined') return;
            clearInterval(waitForJQ);
            $.post('/ajax/players_get_select.ajax.php', { type: 'change', club_id: clubId })
                .done(res => {
                    try {
                        const data = typeof res === 'string' ? JSON.parse(res) : res;
                        if (data && data.post && Object.keys(data.post).length) {
                            processData(data);
                        }
                    } catch (e) { /* ignore */ }
                });
        }, 200);
    };

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', () => setTimeout(tryFetch, 800));
    } else {
        setTimeout(tryFetch, 800);
    }

})();