`;
}
function rivalRefreshSides() {
for (const side of Object.keys(sides)) {
sides[side] = sides[side].map(old =>
rival.players.find(p => p.id === old.id) ||
rival.players.find(p => p.name === old.name) ||
old
);
}
renderChips();
}
async function rivalLoad() {
if (sport.value !== 'nba') {
rival.loaded = false;
playerCache = [];
rivalStatus.textContent = '● NBA LIVE FEED ONLY';
document.getElementById('waiverResult').textContent =
'This connected feed currently supports NBA only.';
return;
}
rivalStatus.textContent = '● LOADING NBA BOX SCORES…';
try {
const response = await fetch(
'/api/players?sport=nba&days=' + rival.days,
{cache: 'no-store'}
);
const data = await response.json();
if (!response.ok || !Array.isArray(data.players)) {
throw Error(data.details || data.error || 'Invalid response');
}
rival.players = data.players.filter(p => p.id && p.name);
rival.loaded = true;
playerCache = rival.players;
rivalRefreshSides();
rivalStatus.textContent =
`● ESPN NBA · ${data.playerCount ?? rival.players.length}` +
` PLAYERS · ${data.completedGames ?? '?'} GAMES` +
(data.partial ? ' · PARTIAL' : '');
rivalWaivers();
} catch (err) {
rival.loaded = false;
rivalStatus.textContent = '● NBA DATA UNAVAILABLE';
const el = document.getElementById('waiverResult');
el.className = 'notice';
el.textContent = 'Could not load NBA data: ' + err.message;
}
}
function rivalWaivers() {
const el = document.getElementById('waiverResult');
const ranked = rival.players
.filter(p => p.games >= 2 && Number.isFinite(rivalAvg(p)))
.sort((a, b) => rivalAvg(b) - rivalAvg(a))
.slice(0, 15);
el.className = 'result';
el.innerHTML = `
Top recent performers with at least 2 games in this window.
These are NOT verified free agents in your ESPN league.
${ranked.map(rivalPlayerCard).join('')}
`;
}
document.getElementById('evaluate').onclick = () => {
const el = document.getElementById('tradeResult');
if (!sides.send.length || !sides.receive.length) {
el.className = 'empty';
el.textContent = 'Add at least one player on each side.';
return;
}
const all = [...sides.send, ...sides.receive];
const invalid = all.filter(p => !p.recentGames?.length);
if (invalid.length) {
el.className = 'notice';
el.textContent =
'No completed-game data for: ' +
invalid.map(p => p.name).join(', ') +
'. Try a 14-day window. No verdict can be calculated.';
return;
}
const a = sides.send.reduce((n, p) => n + rivalAvg(p), 0);
const b = sides.receive.reduce((n, p) => n + rivalAvg(p), 0);
const delta = b - a;
el.className = 'result';
el.innerHTML = `
RECENT FANTASY PRODUCTION · ${rival.days}-DAY WINDOW
${
Math.abs(delta) < 0.05
? 'Equal combined recent averages'
: delta > 0
? 'Receiving side leads by ' + rivalFmt(delta) + ' FP/game'
: 'Sending side leads by ' + rivalFmt(-delta) + ' FP/game'
}
You send
${rivalFmt(a)}
Combined FP/game
You receive
${rivalFmt(b)}
Combined FP/game
Difference (receive − send)
${delta >= 0 ? '+' : ''}${rivalFmt(delta)}
FP/game
Recent sample only. Multi-player package sums do not
account for roster spots, replacement players, injuries,
future schedules or rest-of-season projections.
This is not a final fairness verdict.
${all.map(rivalPlayerCard).join('')}
`;
};
document.getElementById('scoutBtn').onclick = () => {
const el = document.getElementById('scoutResult');
const q = document.getElementById('scoutInput')
.value.trim().toLowerCase();
const p =
rival.players.find(x => x.name.toLowerCase() === q) ||
rival.players.find(x => x.name.toLowerCase().includes(q));
if (!p) {
el.className = 'empty';
el.textContent =
'No matching NBA player with recent box-score data. ' +
'Try a different spelling or a 14-day window.';
return;
}
el.className = 'result';
el.innerHTML = `
${rivalPlayerCard(p)}