const CURRENT_MATCH_POLL_INTERVAL_MS = 30 * 1000; const CURRENT_MATCH_KILL_FEED_POLL_INTERVAL_MS = 1500; const CURRENT_MATCH_PLAYER_STATS_POLL_INTERVAL_MS = 3000; const CURRENT_MATCH_SERVERS = Object.freeze({ "comunidad-hispana-01": "Comunidad Hispana #01", "comunidad-hispana-02": "Comunidad Hispana #02", }); const CURRENT_MATCH_SCOREBOARDS = Object.freeze({ "comunidad-hispana-01": "https://scoreboard.comunidadhll.es", "comunidad-hispana-02": "https://scoreboard.comunidadhll.es:5443", }); const CURRENT_MATCH_KILL_FEED_LIMIT = 18; const CURRENT_MATCH_WHITE_WEAPON_ICON_PATH = "./assets/img/weapons/white/"; const CURRENT_MATCH_WHITE_WEAPON_ICON_FILES = Object.freeze([ "bazooka_white.svg", "bren_gun_white.svg", "browing_m1919_white.svg", "colt_1911_white.svg", "dp27_white.svg", "flammenwefer41_white.svg", "gewehr_white.svg", "kar98k_white.svg", "kar98k_x8_white.svg", "lee_enfield_n4_white.svg", "luger_p08_white.svg", "m1903_springfield_white.svg", "m1_carabine_white.svg", "m1_garand_white.svg", "m2_flamethrower_white.svg", "m3_grease_gun_white.svg", "m97_white.svg", "mg34_white.svg", "mg42_white.svg", "mosing_nagant_1891_white.svg", "mosing_nagant_9130_white.svg", "mosing_nagant_m38_white.svg", "mp40_white.svg", "nagant_m1895_white.svg", "panzerchreck_white.svg", "piat_white.svg", "ppsh41_white.svg", "ppsh_41w_drum_white.svg", "ptrs41_white.svg", "scoped_mosin_nagant_9130_white.svg", "scoped_svt40_white.svg", "sten_mk_v_white.svg", "stg44_white.svg", "svt40_white.svg", "thompson_white.svg", "tokarev_tt33_white.svg", "walther_p38_white.svg", "webley_revolver_white.svg", ]); const CURRENT_MATCH_WEAPONS = Object.freeze({ bazooka: currentMatchWeapon("Bazooka", "bazooka_white.svg"), "m1 bazooka": currentMatchWeapon("M1 Bazooka", "bazooka_white.svg"), "us bazooka": currentMatchWeapon("M1 Bazooka", "bazooka_white.svg"), bren: currentMatchWeapon("Bren Gun", "bren_gun_white.svg"), "bren gun": currentMatchWeapon("Bren Gun", "bren_gun_white.svg"), m1919: currentMatchWeapon("Browning M1919", "browing_m1919_white.svg"), "m1919 browning": currentMatchWeapon("Browning M1919", "browing_m1919_white.svg"), "browning m1919": currentMatchWeapon("Browning M1919", "browing_m1919_white.svg"), browning: currentMatchWeapon("Browning M1919", "browing_m1919_white.svg"), "us tank machine gun": currentMatchWeapon("Browning M1919", "browing_m1919_white.svg"), "us coaxial mg": currentMatchWeapon("Browning M1919", "browing_m1919_white.svg"), "us vehicle mg": currentMatchWeapon("Browning M1919", "browing_m1919_white.svg"), "coaxial m1919": currentMatchWeapon("Browning M1919", "browing_m1919_white.svg"), "m1919 coaxial": currentMatchWeapon("Browning M1919", "browing_m1919_white.svg"), colt: currentMatchWeapon("Colt 1911", "colt_1911_white.svg"), "colt 1911": currentMatchWeapon("Colt 1911", "colt_1911_white.svg"), "colt m1911": currentMatchWeapon("Colt 1911", "colt_1911_white.svg"), m1911: currentMatchWeapon("Colt 1911", "colt_1911_white.svg"), "m1911 pistol": currentMatchWeapon("Colt 1911", "colt_1911_white.svg"), dp27: currentMatchWeapon("DP-27", "dp27_white.svg"), "dp 27": currentMatchWeapon("DP-27", "dp27_white.svg"), "dp 27 lmg": currentMatchWeapon("DP-27", "dp27_white.svg"), "flammenwerfer 41": currentMatchWeapon("Flammenwerfer 41", "flammenwefer41_white.svg"), flammenwerfer: currentMatchWeapon("Flammenwerfer 41", "flammenwefer41_white.svg"), flammenwefer41: currentMatchWeapon("Flammenwerfer 41", "flammenwefer41_white.svg"), "german flamethrower": currentMatchWeapon("Flammenwerfer 41", "flammenwefer41_white.svg"), "gewehr 43": currentMatchWeapon("Gewehr 43", "gewehr_white.svg"), gewehr43: currentMatchWeapon("Gewehr 43", "gewehr_white.svg"), g43: currentMatchWeapon("Gewehr 43", "gewehr_white.svg"), kar98k: currentMatchWeapon("Kar98k", "kar98k_white.svg"), "kar 98k": currentMatchWeapon("Kar98k", "kar98k_white.svg"), kar98: currentMatchWeapon("Kar98k", "kar98k_white.svg"), k98: currentMatchWeapon("Kar98k", "kar98k_white.svg"), "k98k": currentMatchWeapon("Kar98k", "kar98k_white.svg"), "scoped kar98k": currentMatchWeapon("Scoped Kar98k", "kar98k_x8_white.svg"), "kar98k x8": currentMatchWeapon("Scoped Kar98k", "kar98k_x8_white.svg"), "kar 98k x8": currentMatchWeapon("Scoped Kar98k", "kar98k_x8_white.svg"), "german sniper kar98k": currentMatchWeapon("Scoped Kar98k", "kar98k_x8_white.svg"), "sniper kar98k": currentMatchWeapon("Scoped Kar98k", "kar98k_x8_white.svg"), "lee enfield no 4": currentMatchWeapon("Lee-Enfield No.4", "lee_enfield_n4_white.svg"), "lee enfield no4": currentMatchWeapon("Lee-Enfield No.4", "lee_enfield_n4_white.svg"), "lee enfield": currentMatchWeapon("Lee-Enfield No.4", "lee_enfield_n4_white.svg"), enfield: currentMatchWeapon("Lee-Enfield No.4", "lee_enfield_n4_white.svg"), luger: currentMatchWeapon("Luger P08", "luger_p08_white.svg"), p08: currentMatchWeapon("Luger P08", "luger_p08_white.svg"), "luger p08": currentMatchWeapon("Luger P08", "luger_p08_white.svg"), "m1903 springfield": currentMatchWeapon("M1903 Springfield", "m1903_springfield_white.svg"), springfield: currentMatchWeapon("M1903 Springfield", "m1903_springfield_white.svg"), "us sniper springfield": currentMatchWeapon("M1903 Springfield", "m1903_springfield_white.svg"), "scoped springfield": currentMatchWeapon("M1903 Springfield", "m1903_springfield_white.svg"), "m1 carbine": currentMatchWeapon("M1 Carbine", "m1_carabine_white.svg"), "m1 carabine": currentMatchWeapon("M1 Carbine", "m1_carabine_white.svg"), "m1 garand": currentMatchWeapon("M1 Garand", "m1_garand_white.svg"), garand: currentMatchWeapon("M1 Garand", "m1_garand_white.svg"), "m2 flamethrower": currentMatchWeapon("M2 Flamethrower", "m2_flamethrower_white.svg"), "us flamethrower": currentMatchWeapon("M2 Flamethrower", "m2_flamethrower_white.svg"), "m3 grease gun": currentMatchWeapon("M3 Grease Gun", "m3_grease_gun_white.svg"), "grease gun": currentMatchWeapon("M3 Grease Gun", "m3_grease_gun_white.svg"), m97: currentMatchWeapon("Winchester M97", "m97_white.svg"), "winchester m97": currentMatchWeapon("Winchester M97", "m97_white.svg"), "trench gun": currentMatchWeapon("Winchester M97", "m97_white.svg"), shotgun: currentMatchWeapon("Winchester M97", "m97_white.svg"), mg34: currentMatchWeapon("MG34", "mg34_white.svg"), "mg 34": currentMatchWeapon("MG34", "mg34_white.svg"), "german tank machine gun": currentMatchWeapon("MG34", "mg34_white.svg"), "german coaxial mg": currentMatchWeapon("MG34", "mg34_white.svg"), "german vehicle mg": currentMatchWeapon("MG34", "mg34_white.svg"), "coaxial mg34": currentMatchWeapon("MG34", "mg34_white.svg"), "mg34 coaxial": currentMatchWeapon("MG34", "mg34_white.svg"), mg42: currentMatchWeapon("MG42", "mg42_white.svg"), "mg 42": currentMatchWeapon("MG42", "mg42_white.svg"), "mosin nagant 1891": currentMatchWeapon("Mosin Nagant 1891", "mosing_nagant_1891_white.svg"), "mosin 1891": currentMatchWeapon("Mosin Nagant 1891", "mosing_nagant_1891_white.svg"), "mosin nagant 91 30": currentMatchWeapon("Mosin Nagant 91/30", "mosing_nagant_9130_white.svg"), "mosin 9130": currentMatchWeapon("Mosin Nagant 91/30", "mosing_nagant_9130_white.svg"), "mosin nagant 9130": currentMatchWeapon("Mosin Nagant 91/30", "mosing_nagant_9130_white.svg"), "mosin nagant m38": currentMatchWeapon("Mosin Nagant M38", "mosing_nagant_m38_white.svg"), "mosin m38": currentMatchWeapon("Mosin Nagant M38", "mosing_nagant_m38_white.svg"), m38: currentMatchWeapon("Mosin Nagant M38", "mosing_nagant_m38_white.svg"), mp40: currentMatchWeapon("MP40", "mp40_white.svg"), "mp 40": currentMatchWeapon("MP40", "mp40_white.svg"), "nagant m1895": currentMatchWeapon("Nagant M1895", "nagant_m1895_white.svg"), "nagant revolver": currentMatchWeapon("Nagant M1895", "nagant_m1895_white.svg"), panzerschreck: currentMatchWeapon("Panzerschreck", "panzerchreck_white.svg"), panzerchreck: currentMatchWeapon("Panzerschreck", "panzerchreck_white.svg"), raketenpanzerbuchse: currentMatchWeapon("Panzerschreck", "panzerchreck_white.svg"), piat: currentMatchWeapon("PIAT", "piat_white.svg"), "ppsh 41": currentMatchWeapon("PPSh-41", "ppsh41_white.svg"), ppsh41: currentMatchWeapon("PPSh-41", "ppsh41_white.svg"), ppsh: currentMatchWeapon("PPSh-41", "ppsh41_white.svg"), "ppsh 41 drum": currentMatchWeapon("PPSh-41 Drum", "ppsh_41w_drum_white.svg"), "ppsh 41 w drum": currentMatchWeapon("PPSh-41 Drum", "ppsh_41w_drum_white.svg"), "ppsh drum": currentMatchWeapon("PPSh-41 Drum", "ppsh_41w_drum_white.svg"), "ppsh41 drum": currentMatchWeapon("PPSh-41 Drum", "ppsh_41w_drum_white.svg"), "ptrs 41": currentMatchWeapon("PTRS-41", "ptrs41_white.svg"), ptrs41: currentMatchWeapon("PTRS-41", "ptrs41_white.svg"), ptrs: currentMatchWeapon("PTRS-41", "ptrs41_white.svg"), "scoped mosin nagant 91 30": currentMatchWeapon("Scoped Mosin Nagant 91/30", "scoped_mosin_nagant_9130_white.svg"), "scoped mosin nagant 9130": currentMatchWeapon("Scoped Mosin Nagant 91/30", "scoped_mosin_nagant_9130_white.svg"), "soviet sniper mosin": currentMatchWeapon("Scoped Mosin Nagant 91/30", "scoped_mosin_nagant_9130_white.svg"), "sniper mosin": currentMatchWeapon("Scoped Mosin Nagant 91/30", "scoped_mosin_nagant_9130_white.svg"), "scoped svt 40": currentMatchWeapon("Scoped SVT-40", "scoped_svt40_white.svg"), "scoped svt40": currentMatchWeapon("Scoped SVT-40", "scoped_svt40_white.svg"), "svt40 scoped": currentMatchWeapon("Scoped SVT-40", "scoped_svt40_white.svg"), "sten mk v": currentMatchWeapon("Sten Mk V", "sten_mk_v_white.svg"), sten: currentMatchWeapon("Sten Mk V", "sten_mk_v_white.svg"), stg44: currentMatchWeapon("StG 44", "stg44_white.svg"), "stg 44": currentMatchWeapon("StG 44", "stg44_white.svg"), "sturmgewehr 44": currentMatchWeapon("StG 44", "stg44_white.svg"), "svt 40": currentMatchWeapon("SVT-40", "svt40_white.svg"), svt40: currentMatchWeapon("SVT-40", "svt40_white.svg"), "m1a1 thompson": currentMatchWeapon("M1A1 Thompson", "thompson_white.svg"), m1a1: currentMatchWeapon("M1A1 Thompson", "thompson_white.svg"), "m1928 thompson": currentMatchWeapon("M1928 Thompson", "thompson_white.svg"), thompson: currentMatchWeapon("Thompson", "thompson_white.svg"), "tokarev tt 33": currentMatchWeapon("Tokarev TT-33", "tokarev_tt33_white.svg"), "tokarev tt33": currentMatchWeapon("Tokarev TT-33", "tokarev_tt33_white.svg"), tt33: currentMatchWeapon("Tokarev TT-33", "tokarev_tt33_white.svg"), "walther p38": currentMatchWeapon("Walther P38", "walther_p38_white.svg"), p38: currentMatchWeapon("Walther P38", "walther_p38_white.svg"), webley: currentMatchWeapon("Webley Revolver", "webley_revolver_white.svg"), "webley revolver": currentMatchWeapon("Webley Revolver", "webley_revolver_white.svg"), unknown: { label: "Arma desconocida", icon: "" }, }); validateCurrentMatchWeaponMapping(); function currentMatchWeapon(label, fileName) { return { label, icon: `${CURRENT_MATCH_WHITE_WEAPON_ICON_PATH}${fileName}`, }; } function validateCurrentMatchWeaponMapping() { const expectedIcons = new Set(CURRENT_MATCH_WHITE_WEAPON_ICON_FILES); const mappedIcons = new Set(); const invalidIcons = []; Object.entries(CURRENT_MATCH_WEAPONS).forEach(([alias, weapon]) => { if (!weapon.icon) { return; } if (!weapon.icon.startsWith(CURRENT_MATCH_WHITE_WEAPON_ICON_PATH)) { invalidIcons.push(`${alias}: ${weapon.icon}`); return; } const fileName = weapon.icon.slice(CURRENT_MATCH_WHITE_WEAPON_ICON_PATH.length); mappedIcons.add(fileName); if (!expectedIcons.has(fileName)) { invalidIcons.push(`${alias}: ${weapon.icon}`); } }); const unmappedIcons = [...expectedIcons].filter((fileName) => !mappedIcons.has(fileName)); if (unmappedIcons.length > 0 || invalidIcons.length > 0) { console.warn("Current match weapon icon mapping needs review.", { unmappedIcons, invalidIcons, }); } } document.addEventListener("DOMContentLoaded", () => { const params = new URLSearchParams(window.location.search); const serverSlug = params.get("server") || ""; const nodes = { title: document.getElementById("current-match-title"), summary: document.getElementById("current-match-summary"), history: document.getElementById("current-match-history"), scoreboard: document.getElementById("current-match-scoreboard"), note: document.getElementById("current-match-note"), state: document.getElementById("current-match-state"), grid: document.getElementById("current-match-grid"), feedTitle: document.getElementById("current-match-feed-title"), playersTitle: document.getElementById("current-match-players-title"), mapHero: document.getElementById("current-match-map-hero"), mapImage: document.getElementById("current-match-map-image"), mapPlaceholder: document.getElementById("current-match-map-placeholder"), }; const backendBaseUrl = document.body.dataset.backendBaseUrl || "http://127.0.0.1:8000"; if (!CURRENT_MATCH_SERVERS[serverSlug]) { renderUnsupportedServer(nodes); return; } nodes.history.href = `./historico.html?server=${encodeURIComponent(serverSlug)}`; const killFeedState = initializeKillFeed(nodes); const playerStatsState = initializePlayerStats(nodes); let currentMatchRefreshInFlight = false; const refreshCurrentMatch = async () => { if (currentMatchRefreshInFlight) { return; } currentMatchRefreshInFlight = true; try { await loadCurrentMatch({ backendBaseUrl, serverSlug, nodes }); } finally { currentMatchRefreshInFlight = false; } }; let killFeedRefreshInFlight = false; const refreshKillFeed = async () => { if (killFeedRefreshInFlight) { return; } killFeedRefreshInFlight = true; try { await loadKillFeed({ backendBaseUrl, serverSlug, nodes, killFeedState }); } finally { killFeedRefreshInFlight = false; } }; let playerStatsRefreshInFlight = false; const refreshPlayerStats = async () => { if (playerStatsRefreshInFlight) { return; } playerStatsRefreshInFlight = true; try { await loadPlayerStats({ backendBaseUrl, serverSlug, nodes, playerStatsState }); } finally { playerStatsRefreshInFlight = false; } }; void refreshCurrentMatch(); void refreshKillFeed(); void refreshPlayerStats(); window.setInterval(() => { void refreshCurrentMatch(); }, CURRENT_MATCH_POLL_INTERVAL_MS); window.setInterval(() => { void refreshKillFeed(); }, CURRENT_MATCH_KILL_FEED_POLL_INTERVAL_MS); window.setInterval(() => { void refreshPlayerStats(); }, CURRENT_MATCH_PLAYER_STATS_POLL_INTERVAL_MS); }); async function loadCurrentMatch({ backendBaseUrl, serverSlug, nodes }) { try { const payload = await fetchJson( `${backendBaseUrl}/api/current-match?server=${encodeURIComponent(serverSlug)}`, ); renderCurrentMatch(payload?.data || {}, nodes); } catch (error) { nodes.note.textContent = "Se conserva el ultimo estado visible si estaba disponible."; setState(nodes.state, "No se pudo actualizar la partida actual.", true); } } async function loadKillFeed({ backendBaseUrl, serverSlug, nodes, killFeedState }) { try { const cursor = killFeedState.latestEventId ? `&since_event_id=${encodeURIComponent(killFeedState.latestEventId)}` : ""; const payload = await fetchJson( `${backendBaseUrl}/api/current-match/kills?server=${encodeURIComponent(serverSlug)}&limit=${CURRENT_MATCH_KILL_FEED_LIMIT}${cursor}`, ); renderKillFeed(payload?.data || {}, nodes, killFeedState); } catch (error) { setState(nodes.feedState, "No se pudo actualizar el feed de combate.", true); } } async function loadPlayerStats({ backendBaseUrl, serverSlug, nodes, playerStatsState }) { try { const payload = await fetchJson( `${backendBaseUrl}/api/current-match/players?server=${encodeURIComponent(serverSlug)}`, ); renderPlayerStats(payload?.data || {}, nodes, playerStatsState); } catch (error) { setState( nodes.playerStatsState, "Todavía no hay estadísticas fiables de jugadores para esta partida.", true, ); } } function renderCurrentMatch(data, nodes) { const rawServerName = data.server_name || data.server_slug || "Servidor no disponible"; const serverName = formatServerDisplayName(data, rawServerName); const mapName = data.map_pretty_name || data.map || "Mapa no disponible"; const scoreboardUrl = resolveTrustedScoreboardUrl(data); nodes.title.textContent = mapName; nodes.summary.textContent = serverName; nodes.note.textContent = data.found ? "Lectura en vivo recibida. El feed de bajas se actualiza en tiempo casi real." : "Todavia no hay snapshot live disponible para este servidor."; nodes.scoreboard.href = scoreboardUrl || "./index.html"; nodes.scoreboard.hidden = !scoreboardUrl; renderMapHero(data, mapName, nodes); nodes.grid.innerHTML = renderLiveScoreboard(data, { mapName, serverName }); nodes.state.hidden = true; nodes.grid.hidden = false; } function renderUnsupportedServer(nodes) { nodes.title.textContent = "Servidor no soportado"; nodes.summary.textContent = "Abre esta vista desde una tarjeta activa de Comunidad Hispana."; nodes.note.textContent = ""; nodes.scoreboard.hidden = true; nodes.grid.hidden = true; renderMapHero({}, "Mapa no disponible", nodes); setState(nodes.state, "No se puede consultar la partida solicitada.", true); } function initializeKillFeed(nodes) { const feedShell = nodes.feedTitle?.closest(".panel__shell"); if (feedShell) { feedShell.insertAdjacentHTML( "beforeend", `
Cargando feed de combate...
Cargando estadisticas en vivo...
`, ); } nodes.playerStatsState = document.getElementById("current-match-player-stats-state"); nodes.playerCount = document.getElementById("current-match-player-count"); nodes.playerStatsShell = document.getElementById("current-match-player-stats-shell"); return { visibleSignature: "", }; } function renderKillFeed(data, nodes, state) { const incoming = Array.isArray(data.items) ? data.items : []; if (data.scope === "no-current-match-events") { state.byId.clear(); state.latestEventId = ""; } incoming.forEach((event) => { if (event?.event_id) { state.byId.set(event.event_id, event); } }); const events = [...state.byId.values()] .sort(compareKillFeedEvents) .slice(-CURRENT_MATCH_KILL_FEED_LIMIT); state.byId = new Map(events.map((event) => [event.event_id, event])); state.latestEventId = events[events.length - 1]?.event_id || state.latestEventId; if (events.length === 0) { nodes.feedList.innerHTML = ""; state.visibleSignature = ""; setState(nodes.feedState, "Todavía no se han detectado bajas en esta partida."); return; } const visualEvents = events; const visibleSignature = visualEvents.map((event) => event.event_id).join("|"); if (visibleSignature !== state.visibleSignature) { nodes.feedList.innerHTML = renderKillFeedColumns(visualEvents); state.visibleSignature = visibleSignature; } nodes.feedState.textContent = formatKillFeedCoverage(data.scope); nodes.feedState.classList.remove("historical-state--error"); } function compareKillFeedEvents(left, right) { const leftTime = Number(left.server_time); const rightTime = Number(right.server_time); if (Number.isFinite(leftTime) && Number.isFinite(rightTime) && leftTime !== rightTime) { return leftTime - rightTime; } return ( String(left.event_timestamp || "").localeCompare(String(right.event_timestamp || "")) || String(left.event_id || "").localeCompare(String(right.event_id || "")) ); } function renderKillFeedColumns(events) { const splitIndex = Math.ceil(events.length / 2); return [events.slice(0, splitIndex), events.slice(splitIndex)] .map( (columnEvents) => `| Jugador | Equipo | Bajas | Muertes | TK | Muertes TK | Arma frecuente |
|---|