Greasy Fork is available in English.
Enhanced transfer market with smart filters, TI calculation and skill analysis
// ==UserScript==
// @name TM Transfer Scanner
// @namespace https://trophymanager.com
// @version 1.0.0
// @description Enhanced transfer market with smart filters, TI calculation and skill analysis
// @match https://trophymanager.com/transfer*
// @grant none
// @run-at document-end
// ==/UserScript==
(function () {
'use strict';
const $ = window.jQuery;
if (!$) return;
// Only match the main transfer page, not /transfer/bids etc.
if (!/^\/transfer\/?$/.test(location.pathname)) return;
// ═══════════════════════════════════════════════════════════════════
// CONSTANTS
// ═══════════════════════════════════════════════════════════════════
const K_ASI = Math.pow(2, 9) * Math.pow(5, 4) * Math.pow(7, 7); // 263,533,760,000
const K_ASI_GK = 48717927500;
const WAGE_RATE = 15.8079;
const TRAINING1 = new Date('2023-01-16T23:00:00Z');
const SEASON_DAYS = 84;
// Color scale (same as tm-player.user.js)
const COLOR_LEVELS = [
'#ff4c4c', '#ff8c00', '#ffd700', '#90ee90', '#00cfcf', '#5b9bff', '#cc88ff'
];
const TI_THRESHOLDS = [12, 9, 6, 4, 2, 1, -Infinity];
const REC_THRESHOLDS = [5.5, 5, 4, 3, 2, 1, 0];
const R5_THRESHOLDS = [110, 100, 90, 80, 70, 60, -Infinity];
// GK skill order for R5/Rec weight tables (after tooltip reorder)
const GK_WEIGHT_ORDER = ['str','pac','jum','sta','one','ref','ari','com','kic','thr','han'];
// Position multipliers for R5 bonuses (outfield posIdx 0-8)
const R5_POS_MULT = [0.3, 0.3, 0.9, 0.6, 1.5, 0.9, 0.9, 0.6, 0.3];
const SAVED_FILTERS_KEY = 'tms_saved_filters';
// 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
];
// Tooltip skill name → transfer key
const SKILL_NAME_TO_KEY = {
'Strength':'str','Stamina':'sta','Pace':'pac','Marking':'mar','Tackling':'tac',
'Workrate':'wor','Positioning':'pos','Passing':'pas','Crossing':'cro',
'Technique':'tec','Heading':'hea','Finishing':'fin','Longshots':'lon','Set Pieces':'set',
'Handling':'han','One on ones':'one','Reflexes':'ref','Aerial Ability':'ari',
'Jumping':'jum','Communication':'com','Kicking':'kic','Throwing':'thr',
};
const OUTFIELD_SKILLS = ['str','sta','pac','mar','tac','wor','pos','pas','cro','tec','hea','fin','lon','set'];
const GK_SKILLS = ['str','sta','pac','han','one','ref','ari','jum','com','kic','thr'];
const ALL_SKILLS = ['str','sta','pac','mar','tac','wor','pos','pas','cro','tec','hea','fin','lon','set','han','one','ref','ari','jum','com','kic','thr'];
const SKILL_NAMES = {
str:'Str',sta:'Sta',pac:'Pac',mar:'Mar',tac:'Tac',wor:'Wor',
pos:'Pos',pas:'Pas',cro:'Cro',tec:'Tec',hea:'Hea',fin:'Fin',
lon:'Lon',set:'Set',han:'Han',one:'One',ref:'Ref',ari:'Aer',
jum:'Jum',com:'Com',kic:'Kic',thr:'Thr',
};
// Position colors matching tm-player.user.js posGroupColor palette
const POS_COLOR = { g:'#4ade80', d:'#60a5fa', dm:'#fbbf24', m:'#fbbf24', mf:'#fbbf24', om:'#fbbf24', f:'#f87171' };
// Individual formation position → TM API group + side
const FP_MAP = {
gk: { group: 'gk', side: null },
dl: { group: 'de', side: 'le' },
dc: { group: 'de', side: 'ce' },
dr: { group: 'de', side: 'ri' },
dml: { group: 'dm', side: 'le' },
dmc: { group: 'dm', side: 'ce' },
dmr: { group: 'dm', side: 'ri' },
ml: { group: 'mf', side: 'le' },
mc: { group: 'mf', side: 'ce' },
mr: { group: 'mf', side: 'ri' },
oml: { group: 'om', side: 'le' },
omc: { group: 'om', side: 'ce' },
omr: { group: 'om', side: 'ri' },
fc: { group: 'fw', side: null },
};
// ═══════════════════════════════════════════════════════════════════
// STATE
// ═══════════════════════════════════════════════════════════════════
let allPlayers = [];
let sortKey = 'time';
let sortAsc = true;
let expandedId = null;
let playerTipEl = null;
let playerTipTimer = null;
let isLoading = false;
let skillsMode = false;
let tooltipCache = {}; // pid → { recSort, ti, skills }
let tooltipFetchAbort = false;
let findAllRunning = false;
let findAllAbort = false;
// ═══════════════════════════════════════════════════════════════════
// CALCULATION HELPERS
// ═══════════════════════════════════════════════════════════════════
function parseAge(ageFloat) {
const years = Math.floor(ageFloat);
const months = Math.round((ageFloat - years) * 100);
return { years, months, totalMonths: years * 12 + months, decimal: years + months / 12 };
}
function playerIsGK(p) {
return p.fp && p.fp[0] === 'gk';
}
function starSum(p, gk) {
const skills = gk ? GK_SKILLS : OUTFIELD_SKILLS;
let sum = 0, count = 0;
for (const s of skills) {
if (p[s] > 0) { sum += p[s]; count++; }
}
return { sum, count, total: skills.length, max: skills.length * 20 };
}
function fmtNum(n) {
if (n == null || isNaN(n) || n === 0) return '-';
if (n >= 1e9) return (n / 1e9).toFixed(2) + 'B';
if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
if (n >= 1e3) return (n / 1e3).toFixed(0) + 'k';
return String(Math.round(n));
}
function 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 CURRENT_SESSION = getCurrentSession();
function calculateTI(asi, wage, isGK) {
if (!asi || !wage || wage <= 30000) return null;
const w = isGK ? K_ASI_GK : K_ASI;
const { pow, log, round } = Math;
const log27 = log(pow(2, 7));
return round((pow(2, log(w * asi) / log27) - pow(2, log(w * wage / WAGE_RATE) / log27)) * 10);
}
function getColor(value, thresholds) {
for (let i = 0; i < thresholds.length; i++) {
if (value >= thresholds[i]) return COLOR_LEVELS[i];
}
return COLOR_LEVELS[COLOR_LEVELS.length - 1];
}
function fmtRec(val) {
if (val == null || val === '') return '<span style="color:#4a5a40">—</span>';
const num = parseFloat(val);
const disp = Number.isInteger(num) ? String(num) : num.toFixed(2);
const clr = getColor(num, REC_THRESHOLDS);
return `<span class="tms-rec" style="background:rgba(0,0,0,0.25);border:1px solid ${clr}44;color:${clr}">${disp}</span>`;
}
function tiHtml(ti) {
if (ti === null || ti === undefined) return '<span style="color:#4a5a40">—</span>';
const clr = getColor(ti, TI_THRESHOLDS);
return `<span style="color:${clr};font-weight:700">${ti.toFixed(1)}</span>`;
}
function fmtR5(r5) {
if (r5 == null) return '<span class="tms-tip-pending">…</span>';
const clr = getColor(r5, R5_THRESHOLDS);
return `<span style="color:${clr};font-weight:700">${r5.toFixed(1)}</span>`;
}
function fmtAge(ageFloat) {
const { years, months } = parseAge(ageFloat);
return `<span class="tms-age-y">${years}.${months}</span>`;
}
function fmtPos(fp) {
if (!fp || !fp.length) return '-';
// Sort positions same as squad: DC before DMC etc.
const sorted = [...fp].sort((a, b) => getPosIndex(a) - getPosIndex(b));
const labelOf = str => {
if (!str) return { label: '', color: '#aaa' };
const side = str.slice(-1);
const pos = str.slice(0, str.length - 1);
const color = POS_COLOR[pos] || POS_COLOR[str] || '#aaa';
const sideLabel = { l:'L', c:'C', r:'R', k:'' }[side] || '';
const label = (str === 'gk') ? 'GK' : (pos.toUpperCase() + sideLabel);
return { label, color };
};
if (sorted.length === 1) {
const { label, color } = labelOf(sorted[0]);
return `<span style="color:${color};font-weight:700">${label}</span>`;
}
const firstColor = (() => {
const str = sorted[0];
if (str === 'gk') return '#4ade80';
const pos = str.replace(/[lcrk]$/, '');
return POS_COLOR[pos] || POS_COLOR[str] || '#fbbf24';
})();
const inner = sorted.map(str => {
const { label, color } = labelOf(str);
if(!label.trim()) return '';
return ` <span style="color:${color}">${label}</span>`;
}).filter(Boolean);
return `<span class="tms-pos-chip" style="background:${firstColor}22;border:1px solid ${firstColor}44">${inner}</span>`;
}
function skillColor(val) {
if (!val || val <= 0) return '#2a3a28';
if (val >= 20) return '#d4af37';
if (val >= 19) return '#c0c0c0';
if (val >= 16) return '#66dd44';
if (val >= 12) return '#cccc00';
if (val >= 8) return '#ee9900';
return '#ee6633';
}
function skillCell(val) {
if (!val || val <= 0) return `<td class="tms-skill tms-skill0">-</td>`;
const pct = (val / 20) * 100;
const clr = skillColor(val);
return `<td class="tms-skill"><div class="tms-bar-wrap"><div class="tms-bar" style="width:${pct}%;background:${clr}"></div><span>${val}</span></div></td>`;
}
function processPlayer(p) {
const gk = playerIsGK(p);
const ss = starSum(p, gk);
const ageP = parseAge(p.age);
return { ...p, _gk: gk, _ss: ss, _ageP: ageP };
}
const fix2 = v => (Math.round(v * 100) / 100).toFixed(2);
function getPosIndex(favposition) {
if (!favposition) return 0;
const pos = favposition.split(',')[0].toLowerCase().trim();
switch (pos) {
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;
default: return 8; // fc / f
}
}
function skillsToArray(skillsObj, posIdx) {
const order = posIdx === 9 ? GK_WEIGHT_ORDER : OUTFIELD_SKILLS;
return order.map(k => skillsObj[k] || 0);
}
function computeRemainders(posIdx, skills, asi) {
const weight = posIdx === 9 ? K_ASI_GK : K_ASI;
const skillSum = skills.reduce((s, v) => s + v, 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 = parseFloat(fix2((rec + remainder * remainderW1 / not20 - 2) / 3));
return { remainder, remainderW2, not20, ratingR, rec };
}
function computeR5(posIdx, skills, asi, routine) {
const r = computeRemainders(posIdx, skills, asi);
const { pow, E } = Math;
const rou = routine || 0;
const routineBonus = (3 / 100) * (100 - 100 * pow(E, -rou * 0.035));
let rating = parseFloat(fix2(r.ratingR + (r.remainder * r.remainderW2 / r.not20) + routineBonus * 5));
const rou2 = routineBonus;
const goldstar = skills.filter(s => s === 20).length;
const denom = skills.length - goldstar || 1;
const skillsB = skills.map(s => s === 20 ? 20 : s + r.remainder / denom);
const sr = skillsB.map((s, i) => i === 1 ? s : s + rou2);
if (skills.length !== 11) { // outfield (14 skills)
const headerBonus = sr[10] > 12
? parseFloat(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 = parseFloat(fix2(pow(E, (sr[13] + sr[12] + sr[9] * 0.5) ** 2 * 0.002) / 327.92526));
const ckBonus = parseFloat(fix2(pow(E, (sr[13] + sr[8] + sr[9] * 0.5) ** 2 * 0.002) / 983.65770));
const pkBonus = parseFloat(fix2(pow(E, (sr[13] + sr[11] + sr[9] * 0.5) ** 2 * 0.002) / 1967.31409));
const allBonus = headerBonus + fkBonus + ckBonus + pkBonus;
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 = parseFloat(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 = parseFloat(fix2(offSkillsSq / 6 / 22.9**2));
const m = R5_POS_MULT[posIdx];
return parseFloat(fix2(rating + allBonus + gainBase * m + keepBase * m));
}
return parseFloat(fix2(rating));
}
// Estimate R5 range from transfer-data skills with assumed routine [0 … 4.2*(age-15)]
function estimatePlayerR5(p) {
const asi = p.asi || 0;
if (!asi) return null;
const gk = p._gk;
const skillKeys = gk ? GK_WEIGHT_ORDER : OUTFIELD_SKILLS;
const skills = skillKeys.map(k => p[k] || 0);
if (skills.every(s => s === 0)) return null;
const positions = [...(p.fp || [])].sort((a, b) => getPosIndex(a) - getPosIndex(b));
if (!positions.length) return null;
const ageYears = p._ageP ? p._ageP.years : Math.floor(parseFloat(p.age) || 20);
const routineMax = Math.max(0, 4.2 * (ageYears - 15));
let r5Lo = null, r5Hi = null, recCalc = null;
for (const pos of positions) {
const pi = getPosIndex(pos);
const lo = computeR5(pi, skills, asi, 0);
const hi = computeR5(pi, skills, asi, routineMax);
const rm = computeRemainders(pi, skills, asi);
if (r5Lo === null || lo > r5Lo) r5Lo = lo;
if (r5Hi === null || hi > r5Hi) r5Hi = hi;
if (recCalc === null || rm.rec > recCalc) recCalc = rm.rec;
}
return { r5Lo, r5Hi, recCalc, routineMax };
}
function fmtR5Range(lo, hi) {
if (lo == null || hi == null) return '<span class="tms-tip-pending">…</span>';
const loFixed = lo.toFixed(1), hiFixed = hi.toFixed(1);
const clrLo = getColor(lo, R5_THRESHOLDS);
const clrHi = getColor(hi, R5_THRESHOLDS);
if (loFixed === hiFixed)
return `<span style="color:${clrHi};font-weight:700;opacity:0.75">${hiFixed}</span>`;
return `<span style="opacity:0.75">` +
`<span style="color:${clrLo};font-weight:700;font-size:10px">${loFixed}</span>` +
`<span style="color:#4a6a38;font-size:9px">–</span>` +
`<span style="color:${clrHi};font-weight:700;font-size:10px">${hiFixed}</span></span>`;
}
// Pre-populate tooltipCache with R5 estimates for players not yet fully fetched
function computeAllEstimates(players) {
for (const p of players) {
if (tooltipCache[p.id] && !tooltipCache[p.id].estimated) continue;
const est = estimatePlayerR5(p);
if (est) {
console.log(`[TMS] ${p.name_js || p.name} | age ${p.age} | routineMax ${est.routineMax.toFixed(1)} | R5: ${est.r5Lo != null ? est.r5Lo.toFixed(1) : '?'}-${est.r5Hi != null ? est.r5Hi.toFixed(1) : '?'} | Rec: ${est.recCalc != null ? est.recCalc.toFixed(2) : '?'}`);
tooltipCache[p.id] = {
estimated: true,
r5Lo: est.r5Lo, r5Hi: est.r5Hi,
recCalc: est.recCalc,
r5: null, recSort: null, ti: null, skills: null,
};
}
}
}
// ═══════════════════════════════════════════════════════════════════
// CSS
// ═══════════════════════════════════════════════════════════════════
function injectStyles() {
if (document.getElementById('tms-style')) return;
const css = `
/* ─── Root layout ─── */
#tms-root {
display: flex;
gap: 0;
align-items: flex-start;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
color: #c8e0b4;
}
/* ─── Sidebar ─── */
#tms-sidebar {
width: 250px;
min-width: 250px;
background: transparent;
box-sizing: border-box;
position: sticky;
top: 8px;
max-height: calc(100vh - 20px);
overflow-y: auto;
}
#tms-sidebar::-webkit-scrollbar { width: 4px; }
#tms-sidebar::-webkit-scrollbar-track { background: #111; }
#tms-sidebar::-webkit-scrollbar-thumb { background: #3d6828; border-radius: 2px; }
/* Card-style sections (matching tm-player widget style) */
.tms-sb-section {
background: #1c3410;
border: 1px solid #3d6828;
border-radius: 8px;
overflow: hidden;
margin-bottom: 8px;
}
.tms-sb-head {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 10px;
font-weight: 700;
color: #6a9a58;
text-transform: uppercase;
letter-spacing: 0.5px;
padding: 8px 12px 6px;
border-bottom: 1px solid rgba(61,104,40,0.3);
}
.tms-for-inline {
display: flex; align-items: center; gap: 4px;
font-size: 10px; font-weight: 600; color: #90b878;
text-transform: none; letter-spacing: 0; cursor: pointer;
}
.tms-for-inline input[type=checkbox] { accent-color: #6cc040; cursor: pointer; margin: 0; }
.tms-sb-body {
padding: 8px 10px;
}
.tms-pos-formation { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 3px; }
.tms-pos-formation-empty { pointer-events: none; }
.tms-more-toggle {
display: flex; align-items: center; justify-content: space-between;
width: 100%; padding: 6px 10px; margin: 16px 0;
background: rgba(42,74,28,0.25); border: 1px solid #2a4a1c;
border-radius: 6px; color: #6a9a58; font-size: 10px; font-weight: 700;
text-transform: uppercase; letter-spacing: 0.5px;
cursor: pointer; user-select: none;
}
.tms-more-toggle:hover { background: rgba(42,74,28,0.5); color: #c8e0b4; }
.tms-more-toggle .tms-more-arrow { font-size: 9px; transition: transform .2s; }
.tms-more-toggle.open .tms-more-arrow { transform: rotate(180deg); }
.tms-more-body { display: none; }
.tms-more-body.open { display: block; }
.tms-filter-btn {
padding: 5px 5px;
border-radius: 5px;
font-size: 11px;
font-weight: 700;
border: 1px solid rgba(61,104,40,0.45);
background: rgba(0,0,0,0.15);
color: #90b878;
cursor: pointer;
text-align: center;
transition: all 0.12s;
user-select: none;
}
.tms-filter-btn.active { background: #3d6828; color: #e8f5d8; border-color: #6cc040; }
.tms-filter-btn:hover { background: #2a4a1c; }
.tms-filter-btn.tms-gk { color: #4ade80; }
.tms-filter-btn.tms-de { color: #60a5fa; }
.tms-filter-btn.tms-dm { color: #fbbf24; }
.tms-filter-btn.tms-mf { color: #fbbf24; }
.tms-filter-btn.tms-om { color: #fb923c; }
.tms-filter-btn.tms-fw { color: #f87171; }
.tms-row { display: flex; align-items: center; gap: 6px; margin-bottom: 5px; }
.tms-row:last-child { margin-bottom: 0; }
.tms-range-row { display: flex; align-items: center; gap: 4px; }
.tms-range-row .tms-num { flex: 1; min-width: 0; }
.tms-range-sep { font-size: 10px; color: #5a7a48; flex-shrink: 0; }
.tms-lbl { font-size: 10px; color: #8aac72; font-weight: 600; min-width: 30px; letter-spacing: 0.3px; text-transform: uppercase; }
.tms-sel {
flex: 1;
background: rgba(0,0,0,0.25);
border: 1px solid rgba(42,74,28,0.6);
border-radius: 4px;
color: #e8f5d8;
font-size: 12px;
font-weight: 600;
padding: 5px 8px;
outline: none;
cursor: pointer;
font-family: inherit;
transition: border-color 0.15s;
}
.tms-sel:focus { border-color: #6cc040; }
.tms-num { -moz-appearance: textfield; }
.tms-num::-webkit-inner-spin-button,
.tms-num::-webkit-outer-spin-button { opacity: 1; filter: invert(0.6); }
.tms-num::placeholder { color: #5a7a48; }
.tms-check-row { display: flex; align-items: center; gap: 6px; }
.tms-check-row label { font-size: 11px; color: #90b878; cursor: pointer; }
.tms-check-row input[type=checkbox] { accent-color: #6cc040; cursor: pointer; }
.tms-skill-row { display: grid; grid-template-columns: 1fr auto; gap: 4px; margin-bottom: 4px; }
.tms-skill-row:last-child { margin-bottom: 0; }
.tms-skill-row .tms-sel { font-size: 10px; }
.tms-post-note {
font-size: 9px;
font-weight: 400;
color: #4a7a38;
text-transform: none;
letter-spacing: 0;
margin-left: 4px;
}
#tms-search-btn {
width: 100%;
padding: 9px;
border-radius: 7px;
border: none;
background: #3d6828;
color: #e8f5d8;
font-size: 12px;
font-weight: 700;
cursor: pointer;
transition: background 0.15s;
letter-spacing: 0.3px;
font-family: inherit;
margin-bottom: 6px;
}
#tms-search-btn:hover { background: #4d8030; }
#tms-findall-btn {
width: 100%;
padding: 8px;
border-radius: 7px;
border: 1px solid #3d6828;
background: rgba(61,104,40,0.12);
color: #6cc040;
font-size: 11px;
font-weight: 700;
cursor: pointer;
transition: background 0.15s;
letter-spacing: 0.3px;
font-family: inherit;
}
#tms-findall-btn:hover { background: rgba(61,104,40,0.3); }
#tms-filter-box {
background: #162e0e;
border: 1px solid #3d6828;
border-radius: 8px;
padding: 8px;
margin-bottom: 8px;
}
#tms-filter-box .tms-sb-section { margin-bottom: 6px; }
#tms-filter-box .tms-sb-section:last-of-type { margin-bottom: 8px; }
#tms-filter-box #tms-search-btn { margin-bottom: 5px; }
#tms-filter-box #tms-findall-btn { margin-bottom: 0; }
/* ─── Main content ─── */
#tms-main { flex: 1; min-width: 0; padding-left: 12px; position: relative; }
.tms-spacer { flex: 1; }
#tms-toolbar {
position: absolute;
top: 4px; right: 4px;
z-index: 5;
display: flex;
align-items: center;
gap: 4px;
font-size: 11px;
background: rgba(22,46,14,0.92);
padding: 2px 8px;
border-radius: 4px;
pointer-events: none;
}
#tms-hits {
font-size: 12px;
font-weight: 800;
color: #80e048;
font-variant-numeric: tabular-nums;
}
#tms-toolbar .tms-toolbar-label {
font-size: 11px;
color: #6a9a58;
}
/* ─── Table ─── */
.tms-table-wrap { overflow-x: auto; border-radius: 8px; border: 1px solid #2a4a1c; }
.tms-table-wrap::-webkit-scrollbar { height: 4px; }
.tms-table-wrap::-webkit-scrollbar-track { background: #111; }
.tms-table-wrap::-webkit-scrollbar-thumb { background: #3d6828; border-radius: 2px; }
#tms-table {
width: 100%;
border-collapse: collapse;
font-size: 11px;
color: #c8e0b4;
}
#tms-table thead tr { border-bottom: 1px solid #2a4a1c;background: rgba(0,0,0,0.2); }
#tms-table th {
background: #162e0e;
color: #6a9a58;
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.4px;
padding: 6px 8px;
white-space: nowrap;
cursor: pointer;
user-select: none;
position: sticky;
top: 0;
z-index: 2;
background: #162e0e;
}
#tms-table th:hover { color: #c8e0b4; background: #243d18; }
#tms-table th.sort-asc::after { content: ' ▲'; color: #6cc040; }
#tms-table th.sort-desc::after { content: ' ▼'; color: #6cc040; }
#tms-table td {
padding: 4px 7px;
border-bottom: 1px solid rgba(42,74,28,.4);
vertical-align: middle;
white-space: nowrap;
}
#tms-table .tms-player-row { background: #1c3410; }
#tms-table tbody .tms-player-row:nth-child(odd) { background: #1c3410; }
#tms-table tbody .tms-player-row:nth-child(even) { background: #162e0e; }
#tms-table .tms-player-row:hover { background: #243d18 !important; cursor: pointer; }
#tms-table .tms-player-row.tms-expanded { background: rgba(255,255,255,.07); }
/* Column-specific */
.tms-col-flag { width: 24px; text-align: center; }
.tms-col-name { max-width: 220px; overflow: hidden; text-overflow: ellipsis; }
.tms-col-name a { color: #80e048; text-decoration: none; font-weight: 600; }
.tms-col-name a:hover { color: #c8e0b4; text-decoration: underline; }
.tms-note-icon {
display: inline-block;
margin-left: 5px;
font-size: 11px;
cursor: default;
opacity: 0.75;
vertical-align: middle;
position: relative;
}
.tms-note-icon:hover { opacity: 1; }
.tms-note-icon::after {
content: attr(data-note);
display: none;
position: absolute;
left: 50%;
transform: translateX(-50%);
top: calc(100% + 5px);
background: #1a2e14;
border: 1px solid #4a9030;
border-radius: 5px;
padding: 5px 8px;
font-size: 11px;
color: #c8e0b4;
white-space: pre-wrap;
max-width: 260px;
min-width: 100px;
word-break: break-word;
z-index: 100002;
box-shadow: 0 4px 14px rgba(0,0,0,0.6);
pointer-events: none;
line-height: 1.5;
}
.tms-note-icon:hover::after { display: block; }
.tms-col-age { text-align: center; white-space: nowrap; }
.tms-col-r { text-align: right; font-variant-numeric: tabular-nums; }
.tms-col-c { text-align: center; }
.tms-age-y { font-size: 13px; font-weight: 700; color: #e8f5d8; }
.tms-age-mo { font-size: 10px; color: #8aac72; margin-left: 1px; }
.tms-pos {
font-size: 10px;
font-weight: 700;
padding: 1px 3px;
border-radius: 3px;
display: inline-block;
}
.tms-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;
}
.tms-pos-bar { width: 3px; padding: 0 !important; border-radius: 2px; }
.tms-col-posbar { width: 4px; padding: 0 !important; }
.tms-rec {
display: inline-block;
padding: 1px 6px;
border-radius: 8px;
font-size: 10px;
font-weight: 700;
}
.tms-bid-btn {
padding: 3px 8px;
border-radius: 3px;
border: 1px solid #3d6828;
background: rgba(61,104,40,0.25);
color: #6cc040;
font-size: 10px;
font-weight: 700;
cursor: pointer;
transition: all 0.12s;
}
.tms-bid-btn:hover { background: #3d6828; color: #e8f5d8; }
.tms-reload-btn {
padding: 2px 6px;
border-radius: 3px;
border: 1px solid #2a4a1c;
background: transparent;
color: #4a7a38;
font-size: 13px;
line-height: 1;
cursor: pointer;
transition: color 0.12s, border-color 0.12s;
margin-right: 3px;
vertical-align: middle;
}
.tms-reload-btn:hover { color: #6cc040; border-color: #4a8030; }
.tms-reload-btn.tms-reloading { animation: tms-spin 0.7s linear infinite; pointer-events: none; color: #6cc040; }
/* Pending tooltip indicator */
.tms-tip-pending {
color: #4a5a40;
font-size: 10px;
animation: tms-pending-blink 1.2s ease-in-out infinite;
}
@keyframes tms-pending-blink { 0%,100% { opacity: 0.3; } 50% { opacity: 1; } }
/* Skill columns (skills mode) */
.tms-skill { text-align: center; padding: 4px 2px !important; }
.tms-skill0 { color: #4a5a40; font-size: 10px; }
.tms-bar-wrap { display: flex; align-items: center; gap: 3px; min-width: 38px; }
.tms-bar { height: 8px; border-radius: 2px; min-width: 2px; flex-shrink: 0; }
.tms-bar-wrap span { font-size: 10px; min-width: 12px; }
/* ─── Expanded row ─── */
tr.tms-expand-row td { padding: 12px 10px !important; background: #1c3410 !important; cursor: default; }
.tms-expand-inner { display: flex; gap: 20px; flex-wrap: wrap; }
.tms-expand-skills { flex: 1; min-width: 240px; }
.tms-expand-analysis { width: 215px; min-width: 190px; }
.tms-exp-head {
font-size: 9px;
font-weight: 700;
color: #6a9a58;
text-transform: uppercase;
letter-spacing: 0.6px;
margin-bottom: 8px;
}
.tms-skill-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 4px; }
.tms-skill-cell {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
padding: 5px 2px;
background: rgba(0,0,0,0.25);
border-radius: 4px;
border: 1px solid rgba(61,104,40,0.3);
}
.tms-sk-name { font-size: 9px; color: #6a9a58; text-transform: uppercase; }
.tms-sk-bar { width: 100%; height: 5px; background: rgba(0,0,0,0.3); border-radius: 2px; overflow: hidden; }
.tms-sk-fill { height: 100%; border-radius: 2px; }
.tms-sk-val { font-size: 12px; font-weight: 700; }
.tms-an-row {
display: flex;
justify-content: space-between;
padding: 4px 0;
border-bottom: 1px solid rgba(61,104,40,0.2);
font-size: 11px;
}
.tms-an-row:last-child { border-bottom: none; }
.tms-an-lbl { color: #6a9a58; font-weight: 600; }
.tms-an-val { color: #c8e0b4; font-weight: 700; font-variant-numeric: tabular-nums; }
/* ─── Loading / empty ─── */
#tms-loading { text-align: center; padding: 50px 20px; color: #6a9a58; font-size: 13px; }
.tms-spinner {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid #3d6828;
border-top-color: #6cc040;
border-radius: 50%;
animation: tms-spin 0.7s linear infinite;
margin-right: 8px;
vertical-align: middle;
}
@keyframes tms-spin { to { transform: rotate(360deg); } }
/* ─── Player row tooltip ─── */
.tms-player-tip {
position: fixed; z-index: 100001;
background: linear-gradient(135deg, #1a2e14 0%, #243a1a 100%);
border: 1px solid #4a9030; border-radius: 8px;
padding: 10px 12px; min-width: 220px; max-width: 280px;
box-shadow: 0 6px 24px rgba(0,0,0,0.6);
pointer-events: none; font-size: 11px; color: #c8e0b4;
opacity: 0; transition: opacity .15s ease;
}
.tms-player-tip.visible { opacity: 1; }
.tms-player-tip-header {
display: flex; align-items: flex-start; gap: 8px;
margin-bottom: 8px; padding-bottom: 6px;
border-bottom: 1px solid rgba(74,144,48,0.3);
}
.tms-player-tip-name { font-size: 13px; font-weight: 700; color: #e0f0cc; }
.tms-player-tip-pos { font-size: 10px; color: #8abc78; font-weight: 600; margin-top: 2px; }
.tms-player-tip-badges { display: flex; flex-direction: column; gap: 3px; margin-left: auto; align-items: flex-end; }
.tms-player-tip-badge { font-size: 10px; font-weight: 700; padding: 1px 5px; border-radius: 4px; background: rgba(0,0,0,0.3); }
.tms-player-tip-skills { display: flex; gap: 12px; margin-bottom: 6px; }
.tms-player-tip-skills-col { flex: 1; min-width: 0; }
.tms-player-tip-skill {
display: flex; justify-content: space-between;
padding: 1px 0; border-bottom: 1px solid rgba(74,144,48,0.12);
}
.tms-player-tip-skill-name { color: #8abc78; font-size: 10px; }
.tms-player-tip-skill-val { font-weight: 700; font-size: 11px; }
.tms-player-tip-footer {
display: flex; gap: 6px; justify-content: center;
padding-top: 6px; border-top: 1px solid rgba(74,144,48,0.3);
}
.tms-player-tip-stat { text-align: center; }
.tms-player-tip-stat-val { font-size: 13px; font-weight: 800; }
.tms-player-tip-stat-lbl { font-size: 9px; color: #6a9a58; text-transform: uppercase; letter-spacing: 0.3px; }
/* ─── Websocket-compatible watched rows ─── */
#tms-table tr.tms-bump td { background: rgba(255,200,40,0.10) !important; }
#tms-table tr.tms-bump a { color: #ffe680; }
#tms-table tr.watched-player td { background: rgba(108,192,64,0.18) !important; }
#tms-table tr.watched-player-currentbid td{ background: rgba(0,220,110,0.25) !important; box-shadow: inset 0 0 0 1px #00e676; }
#tms-table tr.watched-player-outbid td { background: rgba(255,60,40,0.2) !important; box-shadow: inset 0 0 0 1px #ff4c4c; }
#tms-table tr.watched-player a { color: #e8f5d8; }
/* ─── Time cell ─── */
.tms-time-cell { position: relative; text-align: right; }
.tms-time-cell::after {
content: '';
background: url(/pics/ultra2/clock2.png) no-repeat center;
background-size: contain;
display: inline-block;
width: 13px; height: 13px;
vertical-align: text-bottom;
margin-left: 2px;
}
.tms-time-cell .countdown-split-seconds,
.tms-time-cell .countdown-split-minutes,
.tms-time-cell .countdown-split-hours {
width: 18px; text-align: left; padding-left: 2px;
}
/* ─── Hide TM's page content, our UI lives directly on body ─── */
#right_col, .column3_a, .column3_b, .column2_a { display: none !important; }
.column1_d{display: none !important;}
.main_center{padding-top: 0!important;padding-bottom: 0 !important;}
/* ─── Our outer wrapper, full-width, directly on body ─── */
#tms-outer {
display: block;
width: calc(100% - 20px);
max-width: 1400px;
margin: 10px auto 0;
font-family: Arial, sans-serif;
font-size: 12px;
color: #c8ddb8;
box-sizing: border-box;
}
#tms-root { width: 100%; }
/* ─── Custom modal ─── */
#tms-modal-overlay {
position: fixed; inset: 0; z-index: 200000;
background: rgba(0,0,0,0.78);
display: flex; align-items: center; justify-content: center;
backdrop-filter: blur(3px);
}
.tms-modal {
background: linear-gradient(160deg, #1a2e14 0%, #0e1e0a 100%);
border: 1px solid #4a9030;
border-radius: 12px;
padding: 28px 24px 20px;
max-width: 440px;
width: calc(100% - 40px);
box-shadow: 0 20px 60px rgba(0,0,0,0.9), 0 0 0 1px rgba(74,144,48,0.15);
color: #c8e0b4;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
.tms-modal-icon { font-size: 30px; margin-bottom: 10px; line-height: 1; }
.tms-modal-title { font-size: 15px; font-weight: 800; color: #e0f0cc; margin-bottom: 8px; }
.tms-modal-msg { font-size: 12px; color: #90b878; line-height: 1.65; margin-bottom: 22px; }
.tms-modal-btns { display: flex; flex-direction: column; gap: 8px; }
.tms-modal-btn {
padding: 10px 16px; border-radius: 7px;
font-size: 12px; font-weight: 700;
cursor: pointer; border: none;
transition: all 0.14s; font-family: inherit;
text-align: left;
}
.tms-modal-btn-primary { background: #3d6828; color: #e8f5d8; border: 1px solid #6cc040; }
.tms-modal-btn-primary:hover { background: #4d8030; }
.tms-modal-btn-secondary { background: rgba(61,104,40,0.15); color: #80c050; border: 1px solid #3d6828; }
.tms-modal-btn-secondary:hover { background: rgba(61,104,40,0.3); }
.tms-modal-btn-danger { background: rgba(60,15,5,0.3); color: #a05040; border: 1px solid #5a2a1a; }
.tms-modal-btn-danger:hover { background: rgba(80,20,5,0.5); color: #c06050; }
.tms-modal-btn-sub { font-size: 10px; font-weight: 400; opacity: 0.7; display: block; margin-top: 2px; }
/* ─── Saved filters ─── */
.tms-filter-action-btn {
flex: 1;
padding: 5px 6px;
border-radius: 5px;
font-size: 10px;
font-weight: 700;
border: 1px solid rgba(61,104,40,0.45);
background: rgba(0,0,0,0.15);
color: #90b878;
cursor: pointer;
transition: all 0.12s;
font-family: inherit;
}
.tms-filter-action-btn:hover { background: #2a4a1c; color: #c8e0b4; }
.tms-filter-action-btn.tms-filter-del { color: #a05040; border-color: rgba(90,42,26,0.45); }
.tms-filter-action-btn.tms-filter-del:hover { background: rgba(80,20,5,0.4); color: #c06050; }
`;
const el = document.createElement('style');
el.id = 'tms-style';
el.textContent = css;
document.head.appendChild(el);
}
// ═══════════════════════════════════════════════════════════════════
// SIDEBAR
// ═══════════════════════════════════════════════════════════════════
function optRange(from, to, def) {
let s = '';
for (let i = from; i <= to; i++) s += `<option value="${i}"${i === def ? ' selected' : ''}>${i}</option>`;
return s;
}
function optList(arr, def) {
return arr.map(([v, l]) => `<option value="${v}"${v === def ? ' selected' : ''}>${l}</option>`).join('');
}
function skillSelectOpts(withNone = true) {
const combined = [...OUTFIELD_SKILLS, ...GK_SKILLS.filter(s => !OUTFIELD_SKILLS.includes(s))];
let s = withNone ? '<option value="0">—</option>' : '';
for (const sk of combined) s += `<option value="${sk}">${SKILL_NAMES[sk]}</option>`;
return s;
}
function decRecToTM(val) {
return Math.min(10, Math.max(0, Math.floor(parseFloat(val) * 2)));
}
function buildSidebar() {
const valOpts = `<option value="0">≥</option>${[...Array(20)].map((_,i)=>`<option value="${i+1}">${i+1}</option>`).join('')}`;
return `
<div id="tms-sidebar">
<div id="tms-filter-box">
<div class="tms-sb-section">
<div class="tms-sb-head">Age Range
<label class="tms-for-inline"><input type="checkbox" id="tms-for" checked /> Foreigners</label>
</div>
<div class="tms-sb-body">
<div class="tms-range-row">
<input type="number" id="tms-amin" class="tms-sel tms-num" min="18" max="37" value="18" placeholder="Min" />
<span class="tms-range-sep">–</span>
<input type="number" id="tms-amax" class="tms-sel tms-num" min="18" max="37" value="37" placeholder="Max" />
</div>
</div>
</div>
<div class="tms-sb-section">
<div class="tms-sb-head">Recommendation</div>
<div class="tms-sb-body">
<div class="tms-range-row">
<input type="number" id="tms-rmin" class="tms-sel tms-num" min="0" max="5" step="0.01" value="0" placeholder="Min" />
<span class="tms-range-sep">–</span>
<input type="number" id="tms-rmax" class="tms-sel tms-num" min="0" max="5" step="0.01" value="5" placeholder="Max" />
</div>
</div>
</div>
<div class="tms-sb-section">
<div class="tms-sb-head">R5 <span class="tms-post-note">post-filter</span></div>
<div class="tms-sb-body">
<div class="tms-range-row">
<input type="number" id="tms-r5min" class="tms-sel tms-num" min="0" max="200" step="0.1" placeholder="Min" />
<span class="tms-range-sep">–</span>
<input type="number" id="tms-r5max" class="tms-sel tms-num" min="0" max="200" step="0.1" placeholder="Max" />
</div>
</div>
</div>
<div class="tms-sb-section">
<div class="tms-sb-head">TI <span class="tms-post-note">post-filter</span></div>
<div class="tms-sb-body">
<div class="tms-range-row">
<input type="number" id="tms-timin" class="tms-sel tms-num" min="-100" max="200" step="0.1" placeholder="Min" />
<span class="tms-range-sep">–</span>
<input type="number" id="tms-timax" class="tms-sel tms-num" min="-100" max="200" step="0.1" placeholder="Max" />
</div>
</div>
</div>
<div class="tms-sb-section">
<div class="tms-sb-body">
<div class="tms-pos-formation">
<div class="tms-pos-formation-empty"></div>
<div class="tms-filter-btn tms-gk" data-fp="gk">GK</div>
<div class="tms-pos-formation-empty"></div>
<div class="tms-filter-btn tms-de" data-fp="dl">DL</div>
<div class="tms-filter-btn tms-de" data-fp="dc">DC</div>
<div class="tms-filter-btn tms-de" data-fp="dr">DR</div>
<div class="tms-filter-btn tms-dm" data-fp="dml">DML</div>
<div class="tms-filter-btn tms-dm" data-fp="dmc">DMC</div>
<div class="tms-filter-btn tms-dm" data-fp="dmr">DMR</div>
<div class="tms-filter-btn tms-mf" data-fp="ml">ML</div>
<div class="tms-filter-btn tms-mf" data-fp="mc">MC</div>
<div class="tms-filter-btn tms-mf" data-fp="mr">MR</div>
<div class="tms-filter-btn tms-om" data-fp="oml">OML</div>
<div class="tms-filter-btn tms-om" data-fp="omc">OMC</div>
<div class="tms-filter-btn tms-om" data-fp="omr">OMR</div>
<div class="tms-pos-formation-empty"></div>
<div class="tms-filter-btn tms-fw" data-fp="fc">FC</div>
<div class="tms-pos-formation-empty"></div>
</div>
</div>
</div>
<button id="tms-search-btn">🔍 Search 100</button>
<button id="tms-findall-btn">⬇️ Find All</button>
<div class="tms-sb-section" style="margin-top:6px">
<div class="tms-sb-head">Saved Filters</div>
<div class="tms-sb-body">
<select id="tms-saved-filters-sel" class="tms-sel" style="width:100%;margin-bottom:6px"><option value="">— no saved filters —</option></select>
<div style="display:flex;gap:4px">
<button id="tms-filter-load-btn" class="tms-filter-action-btn">📂 Load</button>
<button id="tms-filter-save-btn" class="tms-filter-action-btn" style="flex:2">💾 Save Current</button>
<button id="tms-filter-del-btn" class="tms-filter-action-btn tms-filter-del">🗑</button>
</div>
</div>
</div>
<button class="tms-more-toggle" id="tms-more-toggle"><span>More Filters</span><span class="tms-more-arrow">▼</span></button>
<div class="tms-more-body" id="tms-more-body">
<div class="tms-sb-section">
<div class="tms-sb-head">Max Price</div>
<div class="tms-sb-body">
<div class="tms-row">
<select id="tms-cost" class="tms-sel">
<option value="0" selected>Any</option>
<option value="aff">Affordable</option>
<option value="5">5 Mil</option>
<option value="25">25 Mil</option>
<option value="50">50 Mil</option>
<option value="100">100 Mil</option>
<option value="250">250 Mil</option>
<option value="500">500 Mil</option>
</select>
</div>
</div>
</div>
<div class="tms-sb-section">
<div class="tms-sb-head">Time Left</div>
<div class="tms-sb-body">
<div class="tms-row">
<select id="tms-time" class="tms-sel">
<option value="0" selected>Any</option>
<option value="1">15 Minutes</option>
<option value="2">1 Hour</option>
<option value="3">6 Hours</option>
<option value="4">1 Day</option>
<option value="5">2 Days</option>
<option value="6">4 Days</option>
</select>
</div>
</div>
</div>
<div class="tms-sb-section">
<div class="tms-sb-head">Skill Filters</div>
<div class="tms-sb-body">
<div class="tms-skill-row">
<select class="tms-sel" id="tms-sf-s0">${skillSelectOpts()}</select>
<select class="tms-sel" id="tms-sf-v0" style="width:46px">${valOpts}</select>
</div>
<div class="tms-skill-row">
<select class="tms-sel" id="tms-sf-s1">${skillSelectOpts()}</select>
<select class="tms-sel" id="tms-sf-v1" style="width:46px">${valOpts}</select>
</div>
<div class="tms-skill-row">
<select class="tms-sel" id="tms-sf-s2">${skillSelectOpts()}</select>
<select class="tms-sel" id="tms-sf-v2" style="width:46px">${valOpts}</select>
</div>
</div>
</div>
</div>
</div>
</div>`;
}
// ═══════════════════════════════════════════════════════════════════
// TABLE RENDER
// ═══════════════════════════════════════════════════════════════════
const BREAKDOWN_COLS = [
{ key: 'posbar', label: '', sort: false, cls: 'tms-col-posbar' },
{ key: 'flag', label: '', sort: false, cls: 'tms-col-flag' },
{ key: 'name', label: 'Name', sort: true, cls: 'tms-col-name' },
{ key: 'age', label: 'Age', sort: true, cls: 'tms-col-age' },
{ key: 'fp', label: 'Pos', sort: false, cls: 'tms-col-c' },
{ key: 'r5', label: 'R5', sort: true, cls: 'tms-col-r' },
{ key: 'rec', label: 'Rec', sort: true, cls: 'tms-col-c' },
{ key: 'ti', label: 'TI', sort: true, cls: 'tms-col-r' },
{ key: 'asi', label: 'ASI', sort: true, cls: 'tms-col-r' },
{ key: 'bid', label: 'Bid', sort: true, cls: 'tms-col-r' },
{ key: 'time', label: 'Time', sort: true, cls: 'tms-col-r' },
{ key: 'act', label: '', sort: false, cls: '' },
];
function thSortClass(key) {
if (sortKey !== key) return '';
return sortAsc ? ' sort-asc' : ' sort-desc';
}
function buildBidBtn(p) {
const nameJs = (p.name_js || p.name || '').replace(/\\/g, '\\\\').replace(/'/g, "\\'");
const fetched = tooltipCache[p.id] && !tooltipCache[p.id].estimated;
const reloadBtn = fetched ? '' : `<button class="tms-reload-btn" data-pid="${p.id}" title="Fetch stats">↻</button>`;
return `${reloadBtn}<button class="tms-bid-btn" onclick="event.stopPropagation();tlpop_pop_transfer_bid('${p.next_bid||0}',${p.pro||0},'${p.id}','${nameJs}')" title="Place Bid">Bid</button>`;
}
function buildPlayerRow(p) {
const nameLink = `<a href="/players/${p.id}/" target="_blank" onclick="event.stopPropagation()">${p.name || p.id}</a>`;
const timeId = `tms-td-${p.id}`;
const timeTd = p.time > 0
? `<span id="${timeId}" class="tms-time-cell"></span>`
: '—';
const bidCls = `bid_${p.id}`;
const cachedTip = tooltipCache[p.id];
const recHtml = cachedTip
? fmtRec(cachedTip.recCalc != null ? cachedTip.recCalc : cachedTip.recSort)
: fmtRec(p.rec);
const barClr = p.fp && p.fp.length
? (() => {
const str = p.fp[0];
if (str === 'gk') return '#4ade80';
const pos = str.replace(/[lcrk]$/, '');
return POS_COLOR[pos] || POS_COLOR[str] || '#4a5a40';
})()
: '#4a5a40';
const noteIcon = p.txt ? `<span class="tms-note-icon" data-note="${p.txt.replace(/"/g, '"')}">📋</span>` : '';
return `<tr class="tms-player-row${p.bump ? ' tms-bump' : ''}" id="player_row_${p.id}" data-pid="${p.id}">
<td class="tms-pos-bar" style="background:${barClr}"></td>
<td class="tms-col-flag">${p.flag || ''}</td>
<td class="tms-col-name">${nameLink}</td>
<td class="tms-col-age">${fmtAge(p.age)}</td>
<td class="tms-col-c">${fmtPos(p.fp)}</td>
<td class="tms-col-r" id="tms-r5-${p.id}">${
cachedTip && cachedTip.r5 != null ? fmtR5(cachedTip.r5)
: cachedTip && (cachedTip.r5Lo != null || cachedTip.r5Hi != null) ? fmtR5Range(cachedTip.r5Lo, cachedTip.r5Hi)
: '<span class="tms-tip-pending">…</span>'
}</td>
<td class="tms-col-c" id="tms-rec-${p.id}">${recHtml}</td>
<td class="tms-col-r" id="tms-ti-${p.id}">${cachedTip ? tiHtml(cachedTip.ti) : '<span class="tms-tip-pending">…</span>'}</td>
<td class="tms-col-r" style="color:#e0f0cc">${p.asi ? fmtNum(p.asi) : '—'}</td>
<td class="tms-col-r ${bidCls}">${fmtNum(p.bid) || '—'}</td>
<td class="tms-col-r">${timeTd}</td>
<td>${buildBidBtn(p)}${noteIcon}</td>
</tr>`;
}
function buildExpandRow(p, colCount) {
const gk = p._gk;
const skills = gk ? GK_SKILLS : OUTFIELD_SKILLS;
const ss = p._ss;
const ageP = p._ageP;
const tip = tooltipCache[p.id];
const skillCells = skills.map(s => {
// Prefer tooltip integer skills over star ratings from transfer list
const val = (tip && tip.skills && tip.skills[s] != null) ? tip.skills[s] : (p[s] || 0);
const pct = (val / 20) * 100;
const clr = skillColor(val);
return `<div class="tms-skill-cell">
<span class="tms-sk-name">${SKILL_NAMES[s]}</span>
<div class="tms-sk-bar"><div class="tms-sk-fill" style="width:${pct}%;background:${clr}"></div></div>
<span class="tms-sk-val" style="color:${clr}">${val || '—'}</span>
</div>`;
}).join('');
const bidN = fmtNum(p.bid);
const recDisp = tip
? fmtRec(tip.recCalc != null ? tip.recCalc : tip.recSort)
: fmtRec(p.rec);
const r5Disp = tip
? (tip.r5 != null ? fmtR5(tip.r5) : fmtR5Range(tip.r5Lo, tip.r5Hi))
: '<span style="color:#4a5a40">Loading…</span>';
const tiDisp = tip ? tiHtml(tip.ti) : '<span style="color:#4a5a40">Loading…</span>';
const skillNote = tip ? `(from tooltip)` : `(transfer list stars)`;
return `<tr class="tms-expand-row">
<td colspan="${colCount}">
<div class="tms-expand-inner">
<div class="tms-expand-skills">
<div class="tms-exp-head">Skills — ${ss.count}/${ss.total} scouted <span style="font-weight:400;color:#4a5a40">${skillNote}</span></div>
<div class="tms-skill-grid">${skillCells}</div>
</div>
<div class="tms-expand-analysis">
<div class="tms-exp-head">Analysis</div>
<div class="tms-an-row"><span class="tms-an-lbl">Age</span><span class="tms-an-val">${ageP.years}.${ageP.months}</span></div>
<div class="tms-an-row"><span class="tms-an-lbl">ASI</span><span class="tms-an-val">${p.asi ? fmtNum(p.asi) : '—'}</span></div>
<div class="tms-an-row"><span class="tms-an-lbl">Rec</span><span class="tms-an-val">${recDisp}</span></div>
<div class="tms-an-row"><span class="tms-an-lbl">R5</span><span class="tms-an-val">${r5Disp}</span></div>
<div class="tms-an-row"><span class="tms-an-lbl">TI / session</span><span class="tms-an-val">${tiDisp}</span></div>
<div class="tms-an-row"><span class="tms-an-lbl">Current Bid</span><span class="tms-an-val">${bidN}</span></div>
<div class="tms-an-row"><span class="tms-an-lbl">Position</span><span class="tms-an-val">${(p.fp || []).join(', ')}</span></div>
<div class="tms-an-row"><span class="tms-an-lbl">Type</span><span class="tms-an-val">${gk ? 'Goalkeeper' : 'Outfield'}</span></div>
</div>
</div>
</td>
</tr>`;
}
// ═══════════════════════════════════════════════════════════════════
// FILTER + SORT + REFRESH
// ═══════════════════════════════════════════════════════════════════
function getPostFilters() {
const r5min = $('#tms-r5min').val(); const r5max = $('#tms-r5max').val();
const timin = $('#tms-timin').val(); const timax = $('#tms-timax').val();
return {
r5min: r5min !== '' ? parseFloat(r5min) : null,
r5max: r5max !== '' ? parseFloat(r5max) : null,
timin: timin !== '' ? parseFloat(timin) : null,
timax: timax !== '' ? parseFloat(timax) : null,
};
}
function passesPostFilters(p, pf) {
const tip = tooltipCache[p.id];
if (!tip) return true; // no tooltip yet — show until we know
if (tip.r5 != null) {
if (pf.r5min !== null && tip.r5 < pf.r5min) return false;
if (pf.r5max !== null && tip.r5 > pf.r5max) return false;
} else {
if (pf.r5min !== null && tip.r5Hi != null && tip.r5Hi < pf.r5min) return false;
if (pf.r5max !== null && tip.r5Lo != null && tip.r5Lo > pf.r5max) return false;
}
if (pf.timin !== null && tip.ti != null && tip.ti < pf.timin) return false;
if (pf.timax !== null && tip.ti != null && tip.ti > pf.timax) return false;
return true;
}
function getVisible() {
const pf = getPostFilters();
let arr = allPlayers.filter(p => passesPostFilters(p, pf));
arr.sort((a, b) => {
// Bump players always float to the top
if (a.bump && !b.bump) return -1;
if (!a.bump && b.bump) return 1;
let av, bv;
switch (sortKey) {
case 'name': av = (a.name||'').toLowerCase(); bv = (b.name||'').toLowerCase(); break;
case 'age': av = a.age||0; bv = b.age||0; break;
case 'rec': {
const ta = tooltipCache[a.id]; const tb = tooltipCache[b.id];
av = ta && ta.recCalc != null ? ta.recCalc : (ta && ta.recSort != null ? ta.recSort : (a.rec||0));
bv = tb && tb.recCalc != null ? tb.recCalc : (tb && tb.recSort != null ? tb.recSort : (b.rec||0));
break;
}
case 'r5': {
const ta = tooltipCache[a.id]; const tb = tooltipCache[b.id];
av = ta ? (ta.r5 != null ? ta.r5 : (ta.r5Hi != null ? ta.r5Hi : -9999)) : -9999;
bv = tb ? (tb.r5 != null ? tb.r5 : (tb.r5Hi != null ? tb.r5Hi : -9999)) : -9999;
break;
}
case 'ti': {
const ta = tooltipCache[a.id]; const tb = tooltipCache[b.id];
av = ta && ta.ti != null ? ta.ti : -9999;
bv = tb && tb.ti != null ? tb.ti : -9999;
break;
}
case 'asi': av = a.asi||0; bv = b.asi||0; break;
case 'bid': av = a.bid||0; bv = b.bid||0; break;
default:
case 'time': av = a.time||0; bv = b.time||0; break;
}
if (typeof av === 'string') return sortAsc ? av.localeCompare(bv) : bv.localeCompare(av);
return sortAsc ? av - bv : bv - av;
});
return arr;
}
function startCountdowns(arr) {
arr.forEach(p => {
if (!p.time || p.time <= 0) return;
const $span = $(`#tms-td-${p.id}`);
if (!$span.length) return;
// Reuse existing countdown if alive
if (window.countDowns && window.countDowns[p.id]) {
try { $span.html(window.countDowns[p.id].getJQ()); return; } catch(e) {}
}
if (!window.Countdown) { $span.text(p.time + 's'); return; }
const cd = new window.Countdown(p.time, '', 'highest', true, (cntdwn) => {
if (!cntdwn || !cntdwn.getJQ) return;
const $row = cntdwn.getJQ().closest('tr');
if (!$row.length) return;
if ($row.hasClass('watched-player-currentbid')) {
$row.find('.tms-time-cell').text('Won').removeClass('tms-time-cell');
} else if ($row.hasClass('watched-player-outbid')) {
$row.find('.tms-time-cell').text('Lost').removeClass('tms-time-cell');
} else {
$row.find('td').css({ color: '#ff3636' });
$row.find('.tms-bid-btn').remove();
cntdwn.getJQ().closest('td').html('—');
setTimeout(() => $row.fadeOut(800, () => $row.remove()), 3000);
}
});
if (window.countDowns) window.countDowns[p.id] = cd;
try { $span.html(cd.getJQ()); } catch(e) { $span.text(p.time + 's'); }
});
}
function removePlayerTip() {
clearTimeout(playerTipTimer);
if (playerTipEl) { playerTipEl.remove(); playerTipEl = null; }
}
function buildPlayerTip(p) {
const tip = tooltipCache[p.id];
const gk = p._gk;
const skillList = gk ? GK_SKILLS : OUTFIELD_SKILLS;
const SKILL_LONG = {
str:'Strength', sta:'Stamina', pac:'Pace', mar:'Marking', tac:'Tackling',
wor:'Workrate', pos:'Positioning',pas:'Passing', cro:'Crossing', tec:'Technique',
hea:'Heading', fin:'Finishing', lon:'Longshots', set:'Set Pieces',
han:'Handling', one:'One on ones',ref:'Reflexes', ari:'Aerial',
jum:'Jumping', com:'Communication', kic:'Kicking', thr:'Throwing',
};
const ageP = p._ageP;
const recVal = tip ? (tip.recCalc != null ? tip.recCalc : tip.recSort) : null;
const r5 = tip ? tip.r5 : null;
const r5Lo = tip ? tip.r5Lo : null;
const r5Hi = tip ? tip.r5Hi : null;
const ti = tip ? tip.ti : null;
const posStr = (p.fp && p.fp.length)
? p.fp.map(s => {
const side = s.slice(-1);
const base = s.slice(0, s.length - 1);
const sl = { l:'L', c:'C', r:'R', k:'' }[side] || '';
return s === 'gk' ? 'GK' : (base.toUpperCase() + sl);
}).join(', ')
: '—';
let h = '<div class="tms-player-tip-header">';
h += `<div><div class="tms-player-tip-name">${p.name || p.id}</div>`;
h += `<div class="tms-player-tip-pos">${posStr} · Age ${ageP.years}.${String(ageP.months).padStart(2,'0')}</div></div>`;
h += '<div class="tms-player-tip-badges">';
if (r5 != null) h += `<span class="tms-player-tip-badge" style="color:${getColor(r5, R5_THRESHOLDS)}">R5 ${r5.toFixed(1)}</span>`;
else if (r5Hi != null) h += `<span class="tms-player-tip-badge" style="color:${getColor(r5Hi, R5_THRESHOLDS)}">R5 ${r5Lo != null && r5Lo.toFixed(1) !== r5Hi.toFixed(1) ? r5Lo.toFixed(1) + '–' : ''}${r5Hi.toFixed(1)}</span>`;
if (recVal != null) h += `<span class="tms-player-tip-badge" style="color:${getColor(recVal, REC_THRESHOLDS)}">Rec ${recVal.toFixed(2)}</span>`;
h += '</div></div>';
const leftIdx = gk ? [0,1,2,7] : [0,1,2,3,4,5,6];
const rightIdx = gk ? [3,4,5,6,8,9,10] : [7,8,9,10,11,12,13];
const renderCol = (indices) => {
let c = '<div class="tms-player-tip-skills-col">';
indices.forEach(i => {
const key = skillList[i];
if (!key) return;
const val = (tip && tip.skills && tip.skills[key] != null) ? tip.skills[key] : null;
const clr = val != null ? skillColor(val) : '#4a5a40';
const disp = val == null ? '—' : (val >= 20 ? '★' : String(val));
c += '<div class="tms-player-tip-skill">';
c += `<span class="tms-player-tip-skill-name">${SKILL_LONG[key] || key}</span>`;
c += `<span class="tms-player-tip-skill-val" style="color:${clr}">${disp}</span>`;
c += '</div>';
});
c += '</div>';
return c;
};
h += '<div class="tms-player-tip-skills">';
h += renderCol(leftIdx);
h += renderCol(rightIdx);
h += '</div>';
h += '<div class="tms-player-tip-footer">';
const r5FooterVal = r5 != null ? r5 : r5Hi;
const r5FooterDisp = r5 != null ? r5.toFixed(1)
: (r5Hi != null ? (r5Lo != null && r5Lo.toFixed(1) !== r5Hi.toFixed(1) ? r5Lo.toFixed(1) + '–' + r5Hi.toFixed(1) : r5Hi.toFixed(1)) : '…');
h += `<div class="tms-player-tip-stat"><div class="tms-player-tip-stat-val" style="color:${r5FooterVal != null ? getColor(r5FooterVal, R5_THRESHOLDS) : '#6a9a58'}">${r5FooterDisp}</div><div class="tms-player-tip-stat-lbl">R5</div></div>`;
h += `<div class="tms-player-tip-stat"><div class="tms-player-tip-stat-val" style="color:${recVal != null ? getColor(recVal, REC_THRESHOLDS) : '#6a9a58'}">${recVal != null ? recVal.toFixed(2) : '…'}</div><div class="tms-player-tip-stat-lbl">Rec</div></div>`;
h += `<div class="tms-player-tip-stat"><div class="tms-player-tip-stat-val" style="color:${ti != null ? getColor(ti, TI_THRESHOLDS) : '#6a9a58'}">${ti != null ? ti.toFixed(1) : '…'}</div><div class="tms-player-tip-stat-lbl">TI</div></div>`;
h += `<div class="tms-player-tip-stat"><div class="tms-player-tip-stat-val" style="color:#e0f0cc">${p.asi ? fmtNum(p.asi) : '—'}</div><div class="tms-player-tip-stat-lbl">ASI</div></div>`;
h += `<div class="tms-player-tip-stat"><div class="tms-player-tip-stat-val" style="color:#c8e0b4">${fmtNum(p.bid) || '—'}</div><div class="tms-player-tip-stat-lbl">Bid</div></div>`;
h += '</div>';
if (p.txt) h += `<div style="margin-top:7px;padding-top:6px;border-top:1px solid rgba(74,144,48,0.25);font-size:10px;color:#90b878;line-height:1.5">📋 ${p.txt}</div>`;
return h;
}
function refreshDisplay() {
const $wrap = $('#tms-table-wrap');
if (!$wrap.length) return;
const arr = getVisible();
$('#tms-hits').text(arr.length);
if (!arr.length) {
$wrap.html('<div id="tms-loading">No players found. Try adjusting your filters.</div>');
return;
}
let html;
if (skillsMode) {
const colCount = 5 + ALL_SKILLS.length + 2;
const thSkills = ALL_SKILLS.map(s => `<th>${SKILL_NAMES[s]}</th>`).join('');
html = `<div class="tms-table-wrap"><table id="tms-table">
<thead><tr>
<th class="tms-col-posbar"></th>
<th class="tms-col-flag"></th>
<th data-sort="name" class="${thSortClass('name')}">Name</th>
<th data-sort="age" class="${thSortClass('age')}">Age</th>
<th>Pos</th>
${thSkills}
<th data-sort="time" class="${thSortClass('time')}">Time</th>
<th></th>
</tr></thead>
</table></div>`;
$wrap.html(html);
startCountdowns(arr);
} else {
const thCols = BREAKDOWN_COLS.map(c => {
if (!c.sort) return `<th class="${c.cls||''}">${c.label}</th>`;
return `<th data-sort="${c.key}" class="${c.cls||''} ${thSortClass(c.key)}">${c.label}</th>`;
}).join('');
html = `<div class="tms-table-wrap"><table id="tms-table">
<thead><tr>${thCols}</tr></thead>
<tbody>${arr.map(p => buildPlayerRow(p)).join('')}</tbody>
</table></div>`;
$wrap.html(html);
startCountdowns(arr);
}
// Sort headers
$('#tms-table th[data-sort]').on('click', function () {
const k = $(this).data('sort');
if (sortKey === k) sortAsc = !sortAsc;
else { sortKey = k; sortAsc = (k === 'time'); }
expandedId = null;
refreshDisplay();
});
// Row hover tooltip
removePlayerTip();
$('#tms-table tbody')
.on('mouseenter', '.tms-player-row', function () {
const pid = $(this).data('pid');
const player = allPlayers.find(x => x.id == pid);
if (!player) return;
removePlayerTip();
playerTipEl = $('<div class="tms-player-tip"></div>').appendTo('body');
const $tip = playerTipEl;
$tip.html(buildPlayerTip(player));
const nameCell = this.querySelector('.tms-col-name') || this;
const rect = nameCell.getBoundingClientRect();
$tip.css({ left: rect.left + 'px', top: (rect.bottom + 6) + 'px' });
playerTipTimer = setTimeout(() => $tip.addClass('visible'), 50);
requestAnimationFrame(() => {
if (!playerTipEl) return;
const tr = $tip[0].getBoundingClientRect();
if (tr.right > window.innerWidth - 10)
$tip.css('left', Math.max(4, window.innerWidth - tr.width - 10) + 'px');
if (tr.bottom > window.innerHeight - 10)
$tip.css('top', Math.max(4, rect.top - tr.height - 6) + 'px');
});
// Fetch tooltip on hover if not yet loaded
const cachedTip = tooltipCache[pid];
if (!cachedTip || cachedTip.estimated) {
fetchOnePlayer(player).then(() => {
if (playerTipEl === $tip) $tip.html(buildPlayerTip(player));
});
}
})
.on('mouseleave', '.tms-player-row', removePlayerTip);
// Compatibility hooks
if (typeof window.make_highlighted_rows === 'function') {
try { window.make_highlighted_rows(); } catch(e) {}
}
}
// ═══════════════════════════════════════════════════════════════════
// TOOLTIP BACKGROUND FETCHER
// ═══════════════════════════════════════════════════════════════════
function updateTooltipCells(pid, tip) {
const recVal = tip.recCalc != null ? tip.recCalc : tip.recSort;
// Post-filters: remove rows that no longer pass after tooltip data arrives
const pf = getPostFilters();
const failsRec = (() => {
if (recVal == null) return false;
const decMin = parseFloat($('#tms-rmin').val()) || 0;
const decMax = parseFloat($('#tms-rmax').val());
const maxVal = isNaN(decMax) ? 5 : decMax;
return (decMin > 0 || maxVal < 5) && (recVal < decMin || recVal > maxVal);
})();
const failsR5 = tip.r5 != null
? ((pf.r5min !== null && tip.r5 < pf.r5min) || (pf.r5max !== null && tip.r5 > pf.r5max))
: ((pf.r5min !== null && tip.r5Hi != null && tip.r5Hi < pf.r5min) ||
(pf.r5max !== null && tip.r5Lo != null && tip.r5Lo > pf.r5max));
const failsTI = (pf.timin !== null && tip.ti != null && tip.ti < pf.timin) ||
(pf.timax !== null && tip.ti != null && tip.ti > pf.timax);
if (failsRec || failsR5 || failsTI) {
const $row = $(`#player_row_${pid}`);
$row.next('.tms-expand-row').remove();
$row.remove();
allPlayers = allPlayers.filter(p => String(p.id) !== String(pid));
const parts = ($('#tms-hits').text() || '').split('/');
const shown = parseInt(parts[0]) || 0;
const total = parseInt(parts[1]) || 0;
$('#tms-hits').text((shown > 0 ? shown - 1 : 0) + ' / ' + (total > 0 ? total - 1 : 0));
return;
}
// Remove reload button now that we have full data
$(`#player_row_${pid} .tms-reload-btn`).remove();
const $rec = $(`#tms-rec-${pid}`);
if ($rec.length && recVal != null) $rec.html(fmtRec(recVal));
const $r5 = $(`#tms-r5-${pid}`);
if ($r5.length) {
if (tip.r5 != null) $r5.html(fmtR5(tip.r5));
else if (tip.r5Lo != null || tip.r5Hi != null) $r5.html(fmtR5Range(tip.r5Lo, tip.r5Hi));
}
const $ti = $(`#tms-ti-${pid}`);
if ($ti.length) $ti.html(tiHtml(tip.ti));
// Refresh open expand row for this player (legacy — no-op when no expand rows)
const $expRow = $(`#player_row_${pid}`).next('.tms-expand-row');
if ($expRow.length) {
const player = allPlayers.find(x => x.id == pid);
if (player) {
const colCount = skillsMode ? (5 + ALL_SKILLS.length + 2) : BREAKDOWN_COLS.length;
$expRow.replaceWith(buildExpandRow(player, colCount));
}
}
// Refresh hover tooltip if it's showing this player
if (playerTipEl) {
const $hovRow = $(`#player_row_${pid}`);
if ($hovRow.length && $hovRow.is(':hover')) {
const player = allPlayers.find(x => x.id == pid);
if (player) playerTipEl.html(buildPlayerTip(player));
}
}
}
async function fetchOnePlayer(p) {
const data = await new Promise(resolve => {
$.post('/ajax/tooltip.ajax.php', { player_id: p.id }, res => {
try { resolve(typeof res === 'object' ? res : JSON.parse(res)); }
catch(e) { resolve(null); }
}).fail(() => resolve(null));
});
if (!data || !data.player) return;
const tp = data.player;
// rec_sort (decimal recommendation)
const recSort = tp.rec_sort !== undefined ? parseFloat(tp.rec_sort) : null;
// TI from ASI + wage
const wageNum = parseInt((tp.wage || '').toString().replace(/[^0-9]/g, '')) || 0;
const asiNum = p.asi || parseInt((tp.asi || tp.skill_index || '').toString().replace(/[^0-9]/g, '')) || 0;
const favpos = tp.favposition || '';
const isGK = favpos.split(',')[0].toLowerCase() === 'gk';
let ti = null;
if (asiNum && wageNum) {
const tiRaw = calculateTI(asiNum, wageNum, isGK);
if (tiRaw !== null && CURRENT_SESSION > 0) {
ti = Number((tiRaw / CURRENT_SESSION).toFixed(1));
}
}
// Integer skills from tooltip
let skills = null;
if (tp.skills && Array.isArray(tp.skills)) {
skills = {};
for (const sk of tp.skills) {
const key = SKILL_NAME_TO_KEY[sk.name];
if (!key) continue;
const v = sk.value;
if (typeof v === 'string') {
if (v.includes('star_silver')) skills[key] = 19;
else if (v.includes('star')) skills[key] = 20;
else skills[key] = parseInt(v) || 0;
} else {
skills[key] = parseInt(v) || 0;
}
}
}
// Exact routine from tooltip (null if not scouted)
const tooltipRoutine = tp.routine != null ? parseFloat(tp.routine) : null;
// Compute R5 with exact skills + real routine; keep lo-hi range as fallback
let recCalc = null, r5 = null, r5Lo = null, r5Hi = null;
if (skills && asiNum) {
const positions = favpos.split(',').map(s => s.trim()).filter(Boolean);
if (positions.length) {
const ageYears = p._ageP ? p._ageP.years : Math.floor(parseFloat(p.age) || 20);
const routineMax = Math.max(0, 4.2 * (ageYears - 15));
const exactRou = tooltipRoutine !== null ? tooltipRoutine : null;
for (const pos of positions) {
const pix = getPosIndex(pos);
const sax = skillsToArray(skills, pix);
const rmx = computeRemainders(pix, sax, asiNum);
const lo = computeR5(pix, sax, asiNum, 0);
const hi = computeR5(pix, sax, asiNum, routineMax);
if (recCalc === null || rmx.rec > recCalc) recCalc = rmx.rec;
if (r5Lo === null || lo > r5Lo) r5Lo = lo;
if (r5Hi === null || hi > r5Hi) r5Hi = hi;
if (exactRou !== null) {
const exact = computeR5(pix, sax, asiNum, exactRou);
if (r5 === null || exact > r5) r5 = exact;
}
}
}
}
const tip = { recSort, recCalc, r5, r5Lo, r5Hi, ti, skills };
tooltipCache[p.id] = tip;
updateTooltipCells(p.id, tip);
}
async function startTooltipFetch(players) {
tooltipFetchAbort = false;
const uncached = players.filter(p => !tooltipCache[p.id] || tooltipCache[p.id].estimated);
// Fire all in parallel — browser caps at ~6 concurrent naturally
await Promise.all(uncached.map(async p => {
if (!tooltipFetchAbort) await fetchOnePlayer(p);
}));
}
// ═══════════════════════════════════════════════════════════════════
// SEARCH
// ═══════════════════════════════════════════════════════════════════
function buildHash() {
let h = '/';
const activeFps = $('[data-fp].active').map(function () { return $(this).data('fp'); }).get();
if (activeFps.length) {
const groups = new Set(), sides = new Set();
for (const fp of activeFps) {
const m = FP_MAP[fp]; if (!m) continue;
groups.add(m.group);
if (m.side) sides.add(m.side);
}
for (const g of groups) h += g + '/';
for (const s of sides) h += s + '/';
}
if ($('#tms-for').is(':checked')) h += 'for/';
const amin = $('#tms-amin').val(), amax = $('#tms-amax').val();
if (amin !== '18') h += `amin/${amin}/`;
if (amax !== '37') h += `amax/${amax}/`;
const recMin = parseFloat($('#tms-rmin').val()) || 0;
const recMax = parseFloat($('#tms-rmax').val());
const tmRmin = decRecToTM(recMin);
const tmRmax = decRecToTM(isNaN(recMax) ? 5 : recMax);
if (tmRmin > 0) h += `rmin/${tmRmin}/`;
if (tmRmax < 10) h += `rmax/${tmRmax}/`;
const cost = $('#tms-cost').val();
if (cost && cost !== '0') h += `cost/${cost}/`;
const time = $('#tms-time').val();
if (time && time !== '0') h += `time/${time}/`;
for (let i = 0; i < 3; i++) {
const sk = $(`#tms-sf-s${i}`).val();
const sv = $(`#tms-sf-v${i}`).val();
if (sk && sk !== '0' && sv && sv !== '0') h += `${sk}/${sv}/`;
}
return h;
}
function doSearch() {
if (isLoading) return;
isLoading = true;
expandedId = null;
tooltipFetchAbort = true; // cancel in-flight tooltip fetches
findAllAbort = true; // cancel in-flight findAll scan
tooltipCache = {}; // fresh cache for new search results
$('#tms-table-wrap').html('<div id="tms-loading"><span class="tms-spinner"></span> Searching transfer market…</div>');
// Clear old countdowns
if (window.countDowns) {
for (const id in window.countDowns) { window.countDowns[id] = null; }
window.countDowns = {};
}
const hash = buildHash();
const clubId = window.SESSION ? window.SESSION.id : 0;
$.post('/ajax/transfer.ajax.php', { search: hash, club_id: clubId }, function (data) {
isLoading = false;
if (!data) {
$('#tms-table-wrap').html('<div id="tms-loading" style="color:#ff7373">No data received. Please try again.</div>');
return;
}
if (data.refresh) { location.reload(); return; }
const raw = Array.isArray(data.list) ? data.list : [];
window.transfer_info_ar = raw;
allPlayers = raw.map(processPlayer);
computeAllEstimates(allPlayers);
refreshDisplay();
// Start background tooltip enrichment
tooltipFetchAbort = true; // abort any previous fetch
setTimeout(() => startTooltipFetch(allPlayers), 300);
}, 'json').fail(function () {
isLoading = false;
$('#tms-table-wrap').html('<div id="tms-loading" style="color:#ff7373">Network error. Please try again.</div>');
});
}
function resetFilters(silent) {
$('[data-fp]').removeClass('active');
$('#tms-for').prop('checked', false);
$('#tms-amin').val('18');
$('#tms-amax').val('37');
$('#tms-rmin').val('0');
$('#tms-rmax').val('5');
$('#tms-cost').val('0');
$('#tms-time').val('0');
for (let i = 0; i < 3; i++) {
$(`#tms-sf-s${i}`).val('0');
$(`#tms-sf-v${i}`).val('0');
}
$('#tms-r5min').val('');
$('#tms-r5max').val('');
$('#tms-timin').val('');
$('#tms-timax').val('');
if (!silent) doSearch();
}
// ═══════════════════════════════════════════════════════════════════
// SAVED FILTERS
// ═══════════════════════════════════════════════════════════════════
function readCurrentFilterState() {
const positions = $('[data-fp].active').map(function () { return $(this).data('fp'); }).get();
const skills = [];
for (let i = 0; i < 3; i++) {
skills.push([$(`#tms-sf-s${i}`).val() || '0', $(`#tms-sf-v${i}`).val() || '0']);
}
return {
positions,
foreigners: $('#tms-for').is(':checked'),
amin: $('#tms-amin').val(),
amax: $('#tms-amax').val(),
rmin: $('#tms-rmin').val(),
rmax: $('#tms-rmax').val(),
cost: $('#tms-cost').val(),
time: $('#tms-time').val(),
skills,
r5min: $('#tms-r5min').val(),
r5max: $('#tms-r5max').val(),
timin: $('#tms-timin').val(),
timax: $('#tms-timax').val(),
};
}
function applyFilterState(state) {
if (!state) return;
$('[data-fp]').removeClass('active');
(state.positions || []).forEach(fp => $(`[data-fp="${fp}"]`).addClass('active'));
$('#tms-for').prop('checked', !!state.foreigners);
$('#tms-amin').val(state.amin != null ? state.amin : '18');
$('#tms-amax').val(state.amax != null ? state.amax : '37');
$('#tms-rmin').val(state.rmin != null ? state.rmin : '0');
$('#tms-rmax').val(state.rmax != null ? state.rmax : '5');
$('#tms-cost').val(state.cost || '0');
$('#tms-time').val(state.time || '0');
const skills = state.skills || [];
for (let i = 0; i < 3; i++) {
const [sk, sv] = skills[i] || ['0', '0'];
$(`#tms-sf-s${i}`).val(sk);
$(`#tms-sf-v${i}`).val(sv);
}
$('#tms-r5min').val(state.r5min || '');
$('#tms-r5max').val(state.r5max || '');
$('#tms-timin').val(state.timin || '');
$('#tms-timax').val(state.timax || '');
// Open "More Filters" if any of those fields are active
const hasMore = (state.cost && state.cost !== '0') ||
(state.time && state.time !== '0') ||
(skills.some(([sk]) => sk && sk !== '0'));
if (hasMore) {
$('#tms-more-toggle').addClass('open');
$('#tms-more-body').addClass('open');
}
}
function getSavedFilters() {
try {
return JSON.parse(localStorage.getItem(SAVED_FILTERS_KEY) || '{}');
} catch (e) { return {}; }
}
function saveNamedFilter(name, state) {
const filters = getSavedFilters();
filters[name] = state;
localStorage.setItem(SAVED_FILTERS_KEY, JSON.stringify(filters));
}
function deleteNamedFilter(name) {
const filters = getSavedFilters();
delete filters[name];
localStorage.setItem(SAVED_FILTERS_KEY, JSON.stringify(filters));
}
function populateSavedFiltersDropdown() {
const filters = getSavedFilters();
const names = Object.keys(filters);
const $sel = $('#tms-saved-filters-sel');
if (!$sel.length) return;
const current = $sel.val();
$sel.empty();
if (names.length === 0) {
$sel.append('<option value="">— no saved filters —</option>');
} else {
$sel.append('<option value="">— select filter —</option>');
for (const name of names) {
$sel.append(`<option value="${name}"${name === current ? ' selected' : ''}>${name}</option>`);
}
}
}
// ═══════════════════════════════════════════════════════════════════
// FIND ALL PLAYERS (exhaustive cartesian scan: pos × age × rec)
// ═══════════════════════════════════════════════════════════════════
function readBaseFilters() {
return {
foreigners: $('#tms-for').is(':checked'),
amin: $('#tms-amin').val() || '18',
amax: $('#tms-amax').val() || '37',
rmin: $('#tms-rmin').val() || '0',
rmax: $('#tms-rmax').val() || '5',
cost: $('#tms-cost').val() || '0',
time: $('#tms-time').val() || '0',
skills: [0, 1, 2].map(i => [$(`#tms-sf-s${i}`).val() || '0', $(`#tms-sf-v${i}`).val() || '0']),
};
}
function buildHashRaw({ positions = [], sides = [], foreigners, amin, amax, rmin, rmax, cost, time, skills = [] }) {
let h = '/';
for (const p of positions) h += p + '/';
for (const s of sides) h += s + '/';
if (foreigners) h += 'for/';
if (amin && amin !== '18') h += `amin/${amin}/`;
if (amax && amax !== '37') h += `amax/${amax}/`;
if (rmin && rmin !== '0') h += `rmin/${rmin}/`;
if (rmax && rmax !== '10') h += `rmax/${rmax}/`;
if (cost && cost !== '0') h += `cost/${cost}/`;
if (time && time !== '0') h += `time/${time}/`;
for (const [sk, sv] of skills) {
if (sk && sk !== '0' && sv && sv !== '0') h += `${sk}/${sv}/`;
}
return h;
}
function fetchWithHash(hash) {
return new Promise(resolve => {
const clubId = window.SESSION ? window.SESSION.id : 0;
$.post('/ajax/transfer.ajax.php', { search: hash, club_id: clubId }, function (data) {
resolve(Array.isArray(data && data.list) ? data.list : []);
}, 'json').fail(() => resolve([]));
});
}
function showModal({ icon, title, message, buttons }) {
return new Promise(resolve => {
const overlay = document.createElement('div');
overlay.id = 'tms-modal-overlay';
overlay.innerHTML =
`<div class="tms-modal">` +
`<div class="tms-modal-icon">${icon || ''}</div>` +
`<div class="tms-modal-title">${title}</div>` +
`<div class="tms-modal-msg">${message}</div>` +
`<div class="tms-modal-btns">${buttons.map(b =>
`<button class="tms-modal-btn tms-modal-btn-${b.style || 'secondary'}" data-val="${b.value}">` +
`${b.label}${b.sub ? `<span class="tms-modal-btn-sub">${b.sub}</span>` : ''}` +
`</button>`
).join('')}</div></div>`;
const closeWith = val => { overlay.remove(); resolve(val); };
const onKey = e => {
if (e.key === 'Escape') { document.removeEventListener('keydown', onKey); closeWith('cancel'); }
};
overlay.addEventListener('click', e => {
if (e.target === overlay) { document.removeEventListener('keydown', onKey); closeWith('cancel'); }
});
document.addEventListener('keydown', onKey);
overlay.querySelectorAll('.tms-modal-btn').forEach(btn =>
btn.addEventListener('click', () => {
document.removeEventListener('keydown', onKey);
closeWith(btn.dataset.val);
})
);
document.body.appendChild(overlay);
});
}
function promptModal({ icon, title, placeholder, defaultValue }) {
return new Promise(resolve => {
const overlay = document.createElement('div');
overlay.id = 'tms-modal-overlay';
const esc = s => (s || '').replace(/"/g, '"');
overlay.innerHTML =
`<div class="tms-modal">` +
`<div class="tms-modal-icon">${icon || ''}</div>` +
`<div class="tms-modal-title">${title}</div>` +
`<input type="text" id="tms-prompt-input" class="tms-sel" style="width:100%;box-sizing:border-box;margin-bottom:14px" placeholder="${esc(placeholder)}" value="${esc(defaultValue)}" />` +
`<div class="tms-modal-btns">` +
`<button class="tms-modal-btn tms-modal-btn-primary" data-val="ok">💾 Save</button>` +
`<button class="tms-modal-btn tms-modal-btn-danger" data-val="cancel">Cancel</button>` +
`</div></div>`;
const getVal = () => overlay.querySelector('#tms-prompt-input').value.trim();
const closeWith = val => { overlay.remove(); resolve(val); };
const onKey = e => {
if (e.key === 'Escape') { document.removeEventListener('keydown', onKey); closeWith(null); }
if (e.key === 'Enter') { document.removeEventListener('keydown', onKey); closeWith(getVal() || null); }
};
overlay.addEventListener('click', e => {
if (e.target === overlay) { document.removeEventListener('keydown', onKey); closeWith(null); }
});
document.addEventListener('keydown', onKey);
overlay.querySelector('[data-val="ok"]').addEventListener('click', () => {
document.removeEventListener('keydown', onKey);
closeWith(getVal() || null);
});
overlay.querySelector('[data-val="cancel"]').addEventListener('click', () => {
document.removeEventListener('keydown', onKey);
closeWith(null);
});
document.body.appendChild(overlay);
setTimeout(() => overlay.querySelector('#tms-prompt-input').focus(), 50);
});
}
async function findAllPlayers() {
if (isLoading || findAllRunning) return;
// Warn if age range > 3 years and no position/rec filter is set
const _amin = Math.max(18, parseInt($('#tms-amin').val()) || 18);
const _amax = Math.min(37, parseInt($('#tms-amax').val()) || 37);
const _hasPos = $('[data-fp].active').length > 0;
const _rmin = parseFloat($('#tms-rmin').val()) || 0;
const _rmax = parseFloat($('#tms-rmax').val());
const _hasRec = _rmin > 0 || (!isNaN(_rmax) && _rmax < 5);
if ((_amax - _amin) > 3 && !_hasPos && !_hasRec) {
const choice = await showModal({
icon: '⚠️',
title: 'This scan may take a long time',
message: 'A wide age range is selected and no <strong style="color:#c8e0b4">position</strong> or ' +
'<strong style="color:#c8e0b4">recommendation</strong> filter is active.<br><br>' +
'Consider adding one to speed things up significantly.',
buttons: [
{ label: 'Proceed Anyway', value: 'ok', style: 'secondary' },
{ label: 'Cancel', value: 'cancel', style: 'danger' },
],
});
if (choice !== 'ok') return;
}
findAllRunning = true;
findAllAbort = false;
tooltipFetchAbort = true;
tooltipCache = {};
expandedId = null;
if (window.countDowns) {
for (const id in window.countDowns) window.countDowns[id] = null;
window.countDowns = {};
}
const base = readBaseFilters();
const rminNum = Math.max(0, decRecToTM(base.rmin));
const rmaxNum = Math.min(10, decRecToTM(parseFloat(base.rmax) || 5));
const aminNum = Math.max(18, parseInt(base.amin) || 18);
const amaxNum = Math.min(37, parseInt(base.amax) || 37);
// ── 1. Position combos: respect user's sidebar selection ──────────
const activeFps = $('[data-fp].active').map(function () { return $(this).data('fp'); }).get();
const fpKeys = activeFps.length ? activeFps : Object.keys(FP_MAP);
const posCombos = fpKeys.map(fp => {
const m = FP_MAP[fp];
return { positions: [m.group], sides: m.side ? [m.side] : [] };
});
// ── 2. Age: one step per year ─────────────────────────────────────
const ages = [];
for (let a = aminNum; a <= amaxNum; a++) ages.push(a);
// ── 3. Rec: one step per unit ─────────────────────────────────────
const recRanges = [];
for (let r = rminNum; r < rmaxNum; r++) recRanges.push([r, r + 1]);
if (recRanges.length === 0) recRanges.push([rminNum, rmaxNum]);
// ── 4. Full cartesian product ─────────────────────────────────────
const tasks = [];
for (const pos of posCombos) {
for (const age of ages) {
for (const [lo, hi] of recRanges) {
tasks.push({ pos, age, recLo: lo, recHi: hi });
}
}
}
const collected = new Map();
const total = tasks.length;
let done = 0;
const updateProgress = () => {
const pct = total > 0 ? Math.round((done / total) * 100) : 0;
$('#tms-table-wrap').html(
`<div id="tms-loading"><span class="tms-spinner"></span>` +
`Scanning… <strong style="color:#c8e0b4">${done}/${total}</strong> (${pct}%) — ` +
`<span style="color:#80e048">${collected.size}</span> players found…</div>`
);
};
updateProgress();
// Fire all tasks in parallel; browser naturally caps at ~6 concurrent
await Promise.all(tasks.map(async (task) => {
if (findAllAbort) return;
const hash = buildHashRaw({
...base,
positions: task.pos.positions,
sides: task.pos.sides,
amin: String(task.age),
amax: String(task.age),
rmin: String(task.recLo),
rmax: String(task.recHi),
});
const result = await fetchWithHash(hash);
if (!findAllAbort) {
for (const p of result) collected.set(p.id, p);
}
done++;
updateProgress();
}));
findAllRunning = false;
if (findAllAbort) return;
const foundCount = collected.size;
let fetchTooltips = true;
if (foundCount > 600) {
const choice = await showModal({
icon: '📊',
title: `${foundCount} players found`,
message: 'Fetching full stats (R5, Rec, TI) for this many players may take several minutes.' +
'<br><br>Or get an <strong style="color:#c8e0b4">instant R5 range estimate</strong> ' +
'based on transfer-data skills and assumed routine ' +
'<span style="color:#80e048">0 – 4.2 × (age − 15)</span>, with no API calls.',
buttons: [
{ label: 'Full Analysis', value: 'full', style: 'primary', sub: 'Fetches R5 · Rec · TI via tooltip API — slower' },
{ label: 'Quick Estimate', value: 'estimate', style: 'secondary', sub: 'Shows R5 range instantly, no extra API calls' },
{ label: 'Cancel', value: 'cancel', style: 'danger' },
],
});
if (choice === 'cancel') return;
fetchTooltips = (choice === 'full');
}
allPlayers = [...collected.values()].map(processPlayer);
computeAllEstimates(allPlayers);
refreshDisplay();
if (fetchTooltips) setTimeout(() => startTooltipFetch(allPlayers), 300);
}
// ═══════════════════════════════════════════════════════════════════
// LAYOUT
// ═══════════════════════════════════════════════════════════════════
function buildLayout() {
console.log('Building layout');
if ($('#tms-outer').length) return;
const $outer = $('<div id="tms-outer"></div>');
$outer.html(`
<div id="tms-root">
${buildSidebar()}
<div id="tms-main">
<div id="tms-toolbar">
<span id="tms-hits">0</span>
<span class="tms-toolbar-label"> players</span>
</div>
<div id="tms-table-wrap">
<div id="tms-loading"><span class="tms-spinner"></span> Loading transfer market…</div>
</div>
</div>
</div>
<div id="transfer_list" style="display:none"></div>
`);
$('.main_center').last().after($outer);
}
// ═══════════════════════════════════════════════════════════════════
// EVENT BINDING
// ═══════════════════════════════════════════════════════════════════
function bindEvents() {
$(document).on('click', '#tms-search-btn', doSearch);
$(document).on('keydown', '#tms-sidebar', function (e) {
if (e.key === 'Enter') doSearch();
});
$(document).on('click', '.tms-reload-btn', function (e) {
e.stopPropagation();
const pid = $(this).data('pid');
const player = allPlayers.find(x => x.id == pid);
if (!player) return;
$(this).addClass('tms-reloading');
fetchOnePlayer(player);
});
$(document).on('click', '[data-fp]', function () { $(this).toggleClass('active'); });
$(document).on('click', '#tms-findall-btn', findAllPlayers);
$(document).on('click', '#tms-more-toggle', function () {
$(this).toggleClass('open');
$('#tms-more-body').toggleClass('open');
});
$(document).on('click', '#tms-filter-save-btn', async function () {
const currentSel = $('#tms-saved-filters-sel').val();
const name = await promptModal({
icon: '💾',
title: 'Save Current Filter',
placeholder: 'Enter filter name…',
defaultValue: currentSel || '',
});
if (!name) return;
saveNamedFilter(name, readCurrentFilterState());
populateSavedFiltersDropdown();
$('#tms-saved-filters-sel').val(name);
});
$(document).on('click', '#tms-filter-load-btn', function () {
const name = $('#tms-saved-filters-sel').val();
if (!name) return;
const state = getSavedFilters()[name];
if (!state) return;
applyFilterState(state);
doSearch();
});
$(document).on('click', '#tms-filter-del-btn', async function () {
const name = $('#tms-saved-filters-sel').val();
if (!name) return;
const confirmed = await showModal({
icon: '🗑️',
title: 'Delete saved filter',
message: `Delete "<strong style="color:#c8e0b4">${name}</strong>"?`,
buttons: [
{ label: 'Delete', value: 'ok', style: 'danger' },
{ label: 'Cancel', value: 'cancel', style: 'secondary' },
],
});
if (confirmed !== 'ok') return;
deleteNamedFilter(name);
populateSavedFiltersDropdown();
});
$(document).on('input', '#tms-r5min, #tms-r5max, #tms-timin, #tms-timax', function () {
expandedId = null;
refreshDisplay();
});
$(document).on('click', '#tms-mode-bd', function () {
skillsMode = false;
$(this).addClass('active');
$('#tms-mode-sk').removeClass('active');
expandedId = null;
refreshDisplay();
});
$(document).on('click', '#tms-mode-sk', function () {
skillsMode = true;
$(this).addClass('active');
$('#tms-mode-bd').removeClass('active');
expandedId = null;
refreshDisplay();
});
}
// ═══════════════════════════════════════════════════════════════════
// NEUTRALIZE TM'S OWN RENDER PIPELINE
// ═══════════════════════════════════════════════════════════════════
function neutralizeTM() {
console.log('Neutralizing TM rendering');
// Stop the hash-check interval TM registered on document.ready
if (window.hashCheck) {
clearInterval(window.hashCheck);
window.hashCheck = null;
}
// Make TM's rendering functions no-ops so their AJAX callback
// doesn't overwrite our UI (they still populate transfer_info_ar)
window.makeTable = function (arr) { if (arr) window.transfer_info_ar = arr; };
window.sort_it = function () {};
window.startSearch = function () {};
window.check_hash = function () {};
window.popFilterImages = function () {};
}
// ═══════════════════════════════════════════════════════════════════
// INIT
// ═══════════════════════════════════════════════════════════════════
function init() {
neutralizeTM();
injectStyles();
buildLayout();
bindEvents();
populateSavedFiltersDropdown();
// Tiny delay gives TM's own DOM-ready callbacks time to clear,
// then we kick off our first search
setTimeout(doSearch, 150);
console.log('Transfer Market Scanner initialized');
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();