TM Squad Viewer

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

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

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

})();