diff --git a/ai/tasks/pending/TASK-085-monthly-mvp-v2-ui.md b/ai/tasks/done/TASK-085-monthly-mvp-v2-ui.md similarity index 100% rename from ai/tasks/pending/TASK-085-monthly-mvp-v2-ui.md rename to ai/tasks/done/TASK-085-monthly-mvp-v2-ui.md diff --git a/ai/tasks/pending/TASK-086-mvp-v1-v2-comparison-panel.md b/ai/tasks/done/TASK-086-mvp-v1-v2-comparison-panel.md similarity index 100% rename from ai/tasks/pending/TASK-086-mvp-v1-v2-comparison-panel.md rename to ai/tasks/done/TASK-086-mvp-v1-v2-comparison-panel.md diff --git a/frontend/assets/css/historico.css b/frontend/assets/css/historico.css index 28fa988..75bd8ee 100644 --- a/frontend/assets/css/historico.css +++ b/frontend/assets/css/historico.css @@ -200,6 +200,13 @@ gap: 16px; } +.historical-mvp-card--v2 { + border-color: rgba(187, 143, 72, 0.26); + background: + radial-gradient(circle at top right, rgba(187, 143, 72, 0.12), transparent 40%), + linear-gradient(180deg, rgba(31, 27, 18, 0.96), rgba(12, 15, 11, 0.98)); +} + .historical-mvp-card--rank-1 { border-color: rgba(210, 182, 118, 0.34); background: @@ -235,6 +242,20 @@ line-height: 1.3; } +.historical-mvp-card__version { + display: inline-flex; + align-items: center; + min-height: 28px; + padding: 0 10px; + border: 1px solid rgba(187, 143, 72, 0.28); + border-radius: 999px; + color: var(--accent-warm); + font-size: 0.68rem; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; +} + .historical-mvp-card__score-label { margin-bottom: 6px; color: var(--muted); @@ -272,6 +293,39 @@ line-height: 1.5; } +.historical-mvp-card__signals { + display: grid; + gap: 10px; +} + +.historical-mvp-card__signal-summary { + color: var(--text-soft); + font-size: 0.9rem; + line-height: 1.5; +} + +.historical-mvp-card__signal-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; +} + +.historical-mvp-card__signal-grid article { + padding: 10px 12px; + border: 1px solid rgba(187, 143, 72, 0.14); + border-radius: 14px; + background: rgba(13, 17, 12, 0.42); +} + +.historical-mvp-card__signal-grid span { + display: block; + margin-bottom: 6px; + color: var(--muted); + font-size: 0.68rem; + letter-spacing: 0.08em; + text-transform: uppercase; +} + .historical-state { margin: 0 0 16px; padding: 14px 16px; @@ -333,6 +387,81 @@ gap: 14px; } +.historical-comparison-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr)); + gap: 14px; +} + +.historical-comparison-card { + display: grid; + gap: 14px; + padding: 18px; + border: 1px solid rgba(159, 168, 141, 0.16); + border-radius: 18px; + background: + radial-gradient(circle at top right, rgba(183, 201, 125, 0.1), transparent 42%), + linear-gradient(180deg, rgba(28, 34, 25, 0.94), rgba(12, 15, 11, 0.98)); + box-shadow: var(--shadow-soft); +} + +.historical-comparison-card__top, +.historical-comparison-card__scores { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; +} + +.historical-comparison-card__eyebrow, +.historical-comparison-card__delta-label, +.historical-comparison-card__meta span { + margin: 0 0 6px; + color: var(--muted); + font-size: 0.7rem; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.historical-comparison-card__title { + margin: 0; + font-size: 1.08rem; + line-height: 1.35; +} + +.historical-comparison-card__delta-value { + color: var(--accent-strong); + font-size: 1.35rem; + line-height: 1; +} + +.historical-comparison-card__score-block strong { + display: block; + font-size: 1rem; +} + +.historical-comparison-card__meta { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; +} + +.historical-comparison-card__meta article { + padding: 10px 12px; + border: 1px solid rgba(159, 168, 141, 0.14); + border-radius: 14px; + background: rgba(13, 17, 12, 0.42); +} + +.historical-comparison-card__summary { + margin: 0; + padding-top: 12px; + border-top: 1px solid rgba(159, 168, 141, 0.12); + color: var(--text-soft); + font-size: 0.9rem; + line-height: 1.5; +} + .historical-match-card { display: grid; gap: 14px; @@ -401,6 +530,19 @@ grid-template-columns: 1fr; } + .historical-mvp-card__signal-grid { + grid-template-columns: 1fr; + } + + .historical-comparison-card__top, + .historical-comparison-card__scores { + flex-direction: column; + } + + .historical-comparison-card__meta { + grid-template-columns: 1fr; + } + .historical-tab { width: 100%; } diff --git a/frontend/assets/js/historico.js b/frontend/assets/js/historico.js index e94dc54..a516b76 100644 --- a/frontend/assets/js/historico.js +++ b/frontend/assets/js/historico.js @@ -100,6 +100,16 @@ document.addEventListener("DOMContentLoaded", () => { const monthlyMvpSnapshotMetaNode = document.getElementById( "monthly-mvp-snapshot-meta", ); + const monthlyMvpV2TitleNode = document.getElementById("monthly-mvp-v2-title"); + const monthlyMvpV2StateNode = document.getElementById("monthly-mvp-v2-state"); + const monthlyMvpV2ListNode = document.getElementById("monthly-mvp-v2-list"); + const monthlyMvpV2NoteNode = document.getElementById("monthly-mvp-v2-note"); + const monthlyMvpV2SnapshotMetaNode = document.getElementById( + "monthly-mvp-v2-snapshot-meta", + ); + const comparisonStateNode = document.getElementById("mvp-comparison-state"); + const comparisonListNode = document.getElementById("mvp-comparison-list"); + const comparisonNoteNode = document.getElementById("mvp-comparison-note"); const weeklyValueHeadingNode = document.getElementById("weekly-leaderboard-value-heading"); const weeklyWindowNoteNode = document.getElementById("weekly-window-note"); const weeklySnapshotMetaNode = document.getElementById( @@ -123,7 +133,10 @@ document.addEventListener("DOMContentLoaded", () => { const recentMatchesCache = new Map(); const leaderboardCache = new Map(); const monthlyMvpCache = new Map(); + const monthlyMvpV2Cache = new Map(); const pendingRequestCache = new Map(); + let latestMonthlyMvpResult = null; + let latestMonthlyMvpV2Result = null; const getSummarySnapshot = (serverSlug) => getCachedJson( @@ -157,6 +170,14 @@ document.addEventListener("DOMContentLoaded", () => { `${backendBaseUrl}/api/historical/snapshots/monthly-mvp?server=${encodeURIComponent(serverSlug)}&limit=3`, ); + const getMonthlyMvpV2Snapshot = (serverSlug) => + getCachedJson( + monthlyMvpV2Cache, + pendingRequestCache, + buildMonthlyMvpV2SnapshotKey(serverSlug), + `${backendBaseUrl}/api/historical/snapshots/monthly-mvp-v2?server=${encodeURIComponent(serverSlug)}&limit=3`, + ); + const refreshServerContent = async () => { const requestId = activeServerRequestId + 1; const leaderboardRequestId = activeLeaderboardRequestId + 1; @@ -184,14 +205,28 @@ document.addEventListener("DOMContentLoaded", () => { summaryNoteNode.textContent = `La vista esta leyendo snapshots precalculados del historico local para ${activeServerLabel}.`; setSnapshotMeta(summarySnapshotMetaNode, "Cargando snapshot de resumen..."); renderSummaryLoading(summaryNode); - monthlyMvpTitleNode.textContent = `Top 3 MVP mensual - ${activeServerLabel}`; - monthlyMvpNoteNode.textContent = "Cargando periodo del MVP mensual..."; - setState(monthlyMvpStateNode, "Cargando Top 3 MVP mensual..."); + monthlyMvpTitleNode.textContent = `Top 3 MVP mensual V1 - ${activeServerLabel}`; + monthlyMvpNoteNode.textContent = "Cargando periodo del MVP mensual V1..."; + setState(monthlyMvpStateNode, "Cargando Top 3 MVP mensual V1..."); monthlyMvpListNode.innerHTML = ""; setSnapshotMeta( monthlyMvpSnapshotMetaNode, - "Cargando snapshot del MVP mensual...", + "Cargando snapshot del MVP mensual V1...", ); + monthlyMvpV2TitleNode.textContent = `Top 3 MVP mensual V2 - ${activeServerLabel}`; + monthlyMvpV2NoteNode.textContent = "Cargando lectura avanzada del MVP mensual V2..."; + setState(monthlyMvpV2StateNode, "Cargando Top 3 MVP mensual V2..."); + monthlyMvpV2ListNode.innerHTML = ""; + setSnapshotMeta( + monthlyMvpV2SnapshotMetaNode, + "Cargando snapshot del MVP mensual V2...", + ); + latestMonthlyMvpResult = null; + latestMonthlyMvpV2Result = null; + comparisonListNode.innerHTML = ""; + comparisonNoteNode.textContent = + "Cargando comparativa entre los rankings mensuales V1 y V2..."; + setState(comparisonStateNode, "Preparando comparativa V1 vs V2..."); weeklyWindowNoteNode.textContent = "Cargando snapshot del ranking activo..."; setSnapshotMeta( weeklySnapshotMetaNode, @@ -221,6 +256,7 @@ document.addEventListener("DOMContentLoaded", () => { buildMonthlyMvpSnapshotKey(activeServerSlug), ); if (cachedMonthlyMvpPayload) { + latestMonthlyMvpResult = { status: "fulfilled", value: cachedMonthlyMvpPayload }; hydrateMonthlyMvp( { status: "fulfilled", value: cachedMonthlyMvpPayload }, monthlyMvpStateNode, @@ -229,6 +265,36 @@ document.addEventListener("DOMContentLoaded", () => { monthlyMvpNoteNode, monthlyMvpSnapshotMetaNode, ); + hydrateMvpComparison( + latestMonthlyMvpResult, + latestMonthlyMvpV2Result, + comparisonStateNode, + comparisonListNode, + comparisonNoteNode, + ); + } + + const cachedMonthlyMvpV2Payload = readCachedPayload( + monthlyMvpV2Cache, + buildMonthlyMvpV2SnapshotKey(activeServerSlug), + ); + if (cachedMonthlyMvpV2Payload) { + latestMonthlyMvpV2Result = { status: "fulfilled", value: cachedMonthlyMvpV2Payload }; + hydrateMonthlyMvpV2( + { status: "fulfilled", value: cachedMonthlyMvpV2Payload }, + monthlyMvpV2StateNode, + monthlyMvpV2ListNode, + monthlyMvpV2TitleNode, + monthlyMvpV2NoteNode, + monthlyMvpV2SnapshotMetaNode, + ); + hydrateMvpComparison( + latestMonthlyMvpResult, + latestMonthlyMvpV2Result, + comparisonStateNode, + comparisonListNode, + comparisonNoteNode, + ); } const cachedLeaderboardPayload = readCachedPayload( @@ -358,6 +424,7 @@ document.addEventListener("DOMContentLoaded", () => { return; } + latestMonthlyMvpResult = monthlyMvpResult; hydrateMonthlyMvp( monthlyMvpResult, monthlyMvpStateNode, @@ -366,6 +433,43 @@ document.addEventListener("DOMContentLoaded", () => { monthlyMvpNoteNode, monthlyMvpSnapshotMetaNode, ); + hydrateMvpComparison( + latestMonthlyMvpResult, + latestMonthlyMvpV2Result, + comparisonStateNode, + comparisonListNode, + comparisonNoteNode, + ); + }); + + void settlePromise(getMonthlyMvpV2Snapshot(targetServerSlug)).then((monthlyMvpV2Result) => { + if ( + !isActiveServerRequest( + requestId, + targetServerSlug, + targetTimeframe, + targetMetric, + ) + ) { + return; + } + + latestMonthlyMvpV2Result = monthlyMvpV2Result; + hydrateMonthlyMvpV2( + monthlyMvpV2Result, + monthlyMvpV2StateNode, + monthlyMvpV2ListNode, + monthlyMvpV2TitleNode, + monthlyMvpV2NoteNode, + monthlyMvpV2SnapshotMetaNode, + ); + hydrateMvpComparison( + latestMonthlyMvpResult, + latestMonthlyMvpV2Result, + comparisonStateNode, + comparisonListNode, + comparisonNoteNode, + ); }); }; @@ -722,28 +826,28 @@ function hydrateMonthlyMvp( snapshotMetaNode, ) { if (result.status !== "fulfilled") { - titleNode.textContent = `Top 3 MVP mensual - ${getHistoricalServerLabel(activeServerSlug)}`; - noteNode.textContent = "No se pudo leer el snapshot mensual del MVP."; - setSnapshotMeta(snapshotMetaNode, "Error al leer el snapshot del MVP mensual."); - setState(stateNode, "No se pudo cargar el Top 3 MVP mensual.", true); + titleNode.textContent = `Top 3 MVP mensual V1 - ${getHistoricalServerLabel(activeServerSlug)}`; + noteNode.textContent = "No se pudo leer el snapshot mensual del MVP V1."; + setSnapshotMeta(snapshotMetaNode, "Error al leer el snapshot del MVP mensual V1."); + setState(stateNode, "No se pudo cargar el Top 3 MVP mensual V1.", true); listNode.innerHTML = ""; return; } const payload = result.value?.data; - titleNode.textContent = `Top 3 MVP mensual - ${getHistoricalServerLabel( + titleNode.textContent = `Top 3 MVP mensual V1 - ${getHistoricalServerLabel( payload?.server_slug || activeServerSlug, )}`; noteNode.textContent = buildMonthlyMvpNote(payload); setSnapshotMeta( snapshotMetaNode, - buildSnapshotMetaText(payload, "Snapshot del MVP mensual pendiente de generacion."), + buildSnapshotMetaText(payload, "Snapshot del MVP mensual V1 pendiente de generacion."), ); if (!payload?.found) { setState( stateNode, - "Todavia no hay un Top 3 MVP mensual listo para el alcance activo.", + "Todavia no hay un Top 3 MVP mensual V1 listo para el alcance activo.", ); listNode.innerHTML = ""; return; @@ -763,6 +867,107 @@ function hydrateMonthlyMvp( stateNode.hidden = true; } +function hydrateMonthlyMvpV2( + result, + stateNode, + listNode, + titleNode, + noteNode, + snapshotMetaNode, +) { + if (result.status !== "fulfilled") { + titleNode.textContent = `Top 3 MVP mensual V2 - ${getHistoricalServerLabel(activeServerSlug)}`; + noteNode.textContent = "No se pudo leer el snapshot mensual del MVP V2."; + setSnapshotMeta(snapshotMetaNode, "Error al leer el snapshot del MVP mensual V2."); + setState(stateNode, "No se pudo cargar el Top 3 MVP mensual V2.", true); + listNode.innerHTML = ""; + return; + } + + const payload = result.value?.data; + titleNode.textContent = `Top 3 MVP mensual V2 - ${getHistoricalServerLabel( + payload?.server_slug || activeServerSlug, + )}`; + noteNode.textContent = buildMonthlyMvpV2Note(payload); + setSnapshotMeta( + snapshotMetaNode, + buildSnapshotMetaText(payload, "Snapshot del MVP mensual V2 pendiente de generacion."), + ); + + if (!payload?.found) { + setState( + stateNode, + "Todavia no hay un Top 3 MVP mensual V2 listo para el alcance activo.", + ); + listNode.innerHTML = ""; + return; + } + + const items = payload?.items; + if (!Array.isArray(items) || items.length === 0) { + setState( + stateNode, + "No hay jugadores elegibles para el MVP mensual V2 en el periodo activo.", + ); + listNode.innerHTML = ""; + return; + } + + listNode.innerHTML = items.map((item) => renderMonthlyMvpV2Card(item, payload)).join(""); + stateNode.hidden = true; +} + +function hydrateMvpComparison( + monthlyMvpResult, + monthlyMvpV2Result, + stateNode, + listNode, + noteNode, +) { + if (!monthlyMvpResult || !monthlyMvpV2Result) { + setState(stateNode, "Preparando comparativa V1 vs V2..."); + return; + } + + if ( + monthlyMvpResult.status !== "fulfilled" || + monthlyMvpV2Result.status !== "fulfilled" + ) { + noteNode.textContent = "No se pudo completar la lectura conjunta de V1 y V2."; + setState(stateNode, "No se pudo cargar la comparativa V1 vs V2.", true); + listNode.innerHTML = ""; + return; + } + + const v1Payload = monthlyMvpResult.value?.data; + const v2Payload = monthlyMvpV2Result.value?.data; + const v1Items = Array.isArray(v1Payload?.items) ? v1Payload.items : []; + const v2Items = Array.isArray(v2Payload?.items) ? v2Payload.items : []; + + if (!v1Payload?.found || !v2Payload?.found || !v1Items.length || !v2Items.length) { + noteNode.textContent = + "La comparativa se activara cuando existan rankings V1 y V2 listos para el mismo alcance."; + setState(stateNode, "Todavia no hay una comparativa V1 vs V2 lista para este alcance."); + listNode.innerHTML = ""; + return; + } + + const comparisonItems = buildMvpComparisonItems(v1Items, v2Items); + if (!comparisonItems.length) { + noteNode.textContent = + "No se encontraron jugadores coincidentes o relevantes para comparar entre V1 y V2."; + setState(stateNode, "Sin diferencias comparables entre V1 y V2 para el alcance activo."); + listNode.innerHTML = ""; + return; + } + + noteNode.textContent = buildMvpComparisonNote(v1Payload, v2Payload, comparisonItems.length); + listNode.innerHTML = comparisonItems + .map((item) => renderMvpComparisonCard(item)) + .join(""); + stateNode.hidden = true; +} + function renderRecentMatchCard(item) { const mapName = item.map?.pretty_name || item.map?.name || "Mapa no disponible"; return ` @@ -862,6 +1067,105 @@ function renderMonthlyMvpCard(item, payload) { `; } +function renderMonthlyMvpV2Card(item, payload) { + const scoreValue = Number(item?.mvp_v2_score); + return ` +
+
+
+ #${escapeHtml(item?.ranking_position || "-")} +
+
+

Puntuacion MVP V2

+ ${escapeHtml( + Number.isFinite(scoreValue) ? scoreValue.toFixed(1) : "0.0", + )} +
+
+
+ V2 avanzado +
+
+ ${escapeHtml( + item?.player?.name || "Jugador no identificado", + )} +
+
+

${escapeHtml( + buildMonthlyMvpV2SignalSummary(item), + )}

+
+
+ Penalty TK + ${escapeHtml(formatDecimal(item?.teamkill_penalty_v2, 2))} +
+
+ Confidence + ${escapeHtml(formatPercent(item?.advanced_confidence))} +
+
+ Most killed + ${escapeHtml(formatNumber(item?.advanced?.most_killed_count))} +
+
+ Duel control + ${escapeHtml(formatNumber(item?.advanced?.duel_control_raw))} +
+
+
+ +
+ `; +} + +function renderMvpComparisonCard(item) { + return ` +
+
+
+

Jugador comparado

+

${escapeHtml(item.playerName)}

+
+
+

Delta puesto

+ ${escapeHtml(item.positionDeltaLabel)} +
+
+
+
+

Score V1

+ ${escapeHtml(item.v1ScoreLabel)} +
+
+

Score V2

+ ${escapeHtml(item.v2ScoreLabel)} +
+
+
+
+ Posicion V1 + ${escapeHtml(item.v1PositionLabel)} +
+
+ Posicion V2 + ${escapeHtml(item.v2PositionLabel)} +
+
+ Delta score + ${escapeHtml(item.scoreDeltaLabel)} +
+
+ Penalty TK + ${escapeHtml(item.teamkillPenaltyLabel)} +
+
+

${escapeHtml(item.summary)}

+
+ `; +} + function setState(node, message, isError = false) { node.textContent = message; node.hidden = false; @@ -967,6 +1271,10 @@ function buildMonthlyMvpSnapshotKey(serverSlug) { return `monthly-mvp:${serverSlug}`; } +function buildMonthlyMvpV2SnapshotKey(serverSlug) { + return `monthly-mvp-v2:${serverSlug}`; +} + function buildRangeLabel(start, end) { if (!start && !end) { return ""; @@ -1061,14 +1369,14 @@ function buildRecentMatchesNote(serverSlug) { function buildMonthlyMvpNote(payload) { if (!payload?.found) { - return "El Top 3 mensual aparecera cuando exista un snapshot MVP listo para este alcance."; + return "El Top 3 mensual V1 aparecera cuando exista un snapshot MVP listo para este alcance."; } const periodLabel = payload?.window_label && payload?.month_key ? `${payload.window_label} (${formatMonthKey(payload.month_key)})` : formatMonthKey(payload?.month_key); const eligiblePlayers = formatNumber(payload?.eligible_players_count); - return `${periodLabel || "Periodo mensual activo"}. ${eligiblePlayers} jugadores cumplen los umbrales de elegibilidad.`; + return `${periodLabel || "Periodo mensual activo"}. ${eligiblePlayers} jugadores cumplen los umbrales base de la version V1.`; } function buildMonthlyMvpFooter(item, payload) { @@ -1079,6 +1387,130 @@ function buildMonthlyMvpFooter(item, payload) { )} partidas · ${formatDecimal(hoursPlayed, 1)} h jugadas`; } +function buildMonthlyMvpV2Note(payload) { + if (!payload?.found) { + return "El Top 3 mensual V2 aparecera cuando exista un snapshot alineado con la cobertura de eventos."; + } + const periodLabel = + payload?.window_label && payload?.month_key + ? `${payload.window_label} (${formatMonthKey(payload.month_key)})` + : formatMonthKey(payload?.month_key); + const eligiblePlayers = formatNumber(payload?.eligible_players_count); + const eventCount = formatNumber(payload?.event_coverage?.event_count); + return `${periodLabel || "Periodo mensual activo"}. ${eligiblePlayers} jugadores elegibles y ${eventCount} eventos V2 cubiertos para este alcance.`; +} + +function buildMonthlyMvpV2SignalSummary(item) { + const rivalryEdge = formatNumber(item?.advanced?.rivalry_edge_raw); + const deathBy = formatNumber(item?.advanced?.death_by_count); + return `Ventaja de rivalidad ${rivalryEdge} y ${deathBy} muertes frente a su rival mas repetido.`; +} + +function buildMonthlyMvpV2Footer(item, payload) { + const hoursPlayed = Number(item?.totals?.time_seconds) / 3600; + const monthLabel = formatMonthKey(payload?.month_key); + return `${monthLabel || "Mes activo"} · ${formatNumber( + item?.matches_considered, + )} partidas · ${formatDecimal(hoursPlayed, 1)} h jugadas`; +} + +function buildMvpComparisonItems(v1Items, v2Items) { + const v1TopItems = v1Items.slice(0, 3); + const v2TopItems = v2Items.slice(0, 3); + const v1Index = new Map( + v1Items.map((item) => [item?.player?.stable_player_key, item]), + ); + const v2Index = new Map( + v2Items.map((item) => [item?.player?.stable_player_key, item]), + ); + const comparisonKeys = []; + + [...v1TopItems, ...v2TopItems].forEach((item) => { + const stableKey = item?.player?.stable_player_key; + if (!stableKey || comparisonKeys.includes(stableKey)) { + return; + } + comparisonKeys.push(stableKey); + }); + + return comparisonKeys.map((stableKey) => { + const v1Item = v1Index.get(stableKey); + const v2Item = v2Index.get(stableKey); + const v1Position = Number(v1Item?.ranking_position); + const v2Position = Number(v2Item?.ranking_position); + const v1Score = Number(v1Item?.mvp_score); + const v2Score = Number(v2Item?.mvp_v2_score); + const scoreDelta = Number.isFinite(v1Score) && Number.isFinite(v2Score) + ? v2Score - v1Score + : null; + return { + playerName: + v2Item?.player?.name || + v1Item?.player?.name || + "Jugador no identificado", + v1PositionLabel: Number.isFinite(v1Position) ? `#${v1Position}` : "Fuera del Top V1", + v2PositionLabel: Number.isFinite(v2Position) ? `#${v2Position}` : "Fuera del Top V2", + positionDeltaLabel: buildPositionDeltaLabel(v1Position, v2Position), + v1ScoreLabel: Number.isFinite(v1Score) ? formatDecimal(v1Score, 1) : "Sin entrada", + v2ScoreLabel: Number.isFinite(v2Score) ? formatDecimal(v2Score, 1) : "Sin entrada", + scoreDeltaLabel: buildScoreDeltaLabel(scoreDelta), + teamkillPenaltyLabel: buildTeamkillPenaltyComparisonLabel(v1Item, v2Item), + summary: buildMvpComparisonSummary(v1Item, v2Item, scoreDelta), + }; + }); +} + +function buildMvpComparisonNote(v1Payload, v2Payload, itemCount) { + const monthLabel = formatMonthKey(v2Payload?.month_key || v1Payload?.month_key); + return `${monthLabel || "Periodo mensual activo"}. Comparativa ligera de ${formatNumber(itemCount)} jugadores visibles entre el Top V1 y el Top V2 para validar cambios antes de converger rankings.`; +} + +function buildPositionDeltaLabel(v1Position, v2Position) { + if (Number.isFinite(v1Position) && Number.isFinite(v2Position)) { + const delta = v1Position - v2Position; + if (delta > 0) { + return `Sube ${delta}`; + } + if (delta < 0) { + return `Baja ${Math.abs(delta)}`; + } + return "Sin cambio"; + } + if (Number.isFinite(v2Position)) { + return "Entra en V2"; + } + if (Number.isFinite(v1Position)) { + return "Sale en V2"; + } + return "Sin cruce"; +} + +function buildScoreDeltaLabel(scoreDelta) { + if (!Number.isFinite(scoreDelta)) { + return "Sin cruce"; + } + const prefix = scoreDelta > 0 ? "+" : ""; + return `${prefix}${formatDecimal(scoreDelta, 1)}`; +} + +function buildTeamkillPenaltyComparisonLabel(v1Item, v2Item) { + const v1Penalty = Number(v1Item?.teamkill_penalty); + const v2Penalty = Number(v2Item?.teamkill_penalty_v2); + if (!Number.isFinite(v1Penalty) && !Number.isFinite(v2Penalty)) { + return "Sin dato"; + } + return `${formatDecimal(v1Penalty, 1)} -> ${formatDecimal(v2Penalty, 1)}`; +} + +function buildMvpComparisonSummary(v1Item, v2Item, scoreDelta) { + const deltaLabel = Number.isFinite(scoreDelta) + ? `${scoreDelta > 0 ? "mejora" : scoreDelta < 0 ? "cae" : "mantiene"} ${buildScoreDeltaLabel(scoreDelta)}` + : "no tiene cruce completo"; + const mostKilled = formatNumber(v2Item?.advanced?.most_killed_count); + const duelControl = formatNumber(v2Item?.advanced?.duel_control_raw); + return `En V2 ${deltaLabel}. Senales avanzadas visibles: most killed ${mostKilled} y duel control ${duelControl}.`; +} + function buildSnapshotMetaText(payload, missingMessage) { if (!payload?.generated_at) { return missingMessage; @@ -1169,6 +1601,17 @@ function formatDecimal(value, fractionDigits = 1) { }).format(parsedValue); } +function formatPercent(value) { + const parsedValue = Number(value); + if (!Number.isFinite(parsedValue)) { + return "0 %"; + } + + return `${new Intl.NumberFormat("es-ES", { + maximumFractionDigits: 0, + }).format(parsedValue * 100)} %`; +} + function formatMonthKey(monthKey) { if (!monthKey) { return ""; diff --git a/frontend/historico.html b/frontend/historico.html index d7c6b0c..51a5fcb 100644 --- a/frontend/historico.html +++ b/frontend/historico.html @@ -111,23 +111,62 @@
-

MVP mensual

-

Top 3 del alcance activo

+

MVP mensual V1

+

Top 3 V1 del alcance activo

- Cargando periodo del MVP mensual... + Cargando baseline del MVP mensual V1...

- Cargando snapshot del MVP mensual... + Cargando snapshot del MVP mensual V1...

- Cargando Top 3 MVP mensual... + Cargando Top 3 MVP mensual V1...

+
+
+
+
+

MVP mensual V2

+

Top 3 V2 del alcance activo

+

+ Cargando lectura avanzada del MVP mensual V2... +

+

+ Cargando snapshot del MVP mensual V2... +

+
+
+

+ Cargando Top 3 MVP mensual V2... +

+
+
+
+ +
+
+
+
+

Comparativa V1 vs V2

+

Lectura de validacion del alcance activo

+

+ Cargando comparativa entre los rankings mensuales V1 y V2... +

+
+
+

+ Preparando comparativa V1 vs V2... +

+
+
+
+