Add historical monthly MVP V2 UI and comparison panel
This commit is contained in:
@@ -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%;
|
||||
}
|
||||
|
||||
@@ -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 `
|
||||
<article class="historical-mvp-card historical-mvp-card--v2 historical-mvp-card--rank-${escapeHtml(item?.ranking_position || "x")}">
|
||||
<div class="historical-mvp-card__top">
|
||||
<div>
|
||||
<span class="historical-mvp-card__rank">#${escapeHtml(item?.ranking_position || "-")}</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="historical-mvp-card__score-label">Puntuacion MVP V2</p>
|
||||
<strong class="historical-mvp-card__score-value">${escapeHtml(
|
||||
Number.isFinite(scoreValue) ? scoreValue.toFixed(1) : "0.0",
|
||||
)}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="historical-mvp-card__version">V2 avanzado</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong class="historical-mvp-card__player">${escapeHtml(
|
||||
item?.player?.name || "Jugador no identificado",
|
||||
)}</strong>
|
||||
</div>
|
||||
<div class="historical-mvp-card__signals">
|
||||
<p class="historical-mvp-card__signal-summary">${escapeHtml(
|
||||
buildMonthlyMvpV2SignalSummary(item),
|
||||
)}</p>
|
||||
<div class="historical-mvp-card__signal-grid">
|
||||
<article>
|
||||
<span>Penalty TK</span>
|
||||
<strong>${escapeHtml(formatDecimal(item?.teamkill_penalty_v2, 2))}</strong>
|
||||
</article>
|
||||
<article>
|
||||
<span>Confidence</span>
|
||||
<strong>${escapeHtml(formatPercent(item?.advanced_confidence))}</strong>
|
||||
</article>
|
||||
<article>
|
||||
<span>Most killed</span>
|
||||
<strong>${escapeHtml(formatNumber(item?.advanced?.most_killed_count))}</strong>
|
||||
</article>
|
||||
<article>
|
||||
<span>Duel control</span>
|
||||
<strong>${escapeHtml(formatNumber(item?.advanced?.duel_control_raw))}</strong>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
<p class="historical-mvp-card__footer">
|
||||
${escapeHtml(buildMonthlyMvpV2Footer(item, payload))}
|
||||
</p>
|
||||
</article>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderMvpComparisonCard(item) {
|
||||
return `
|
||||
<article class="historical-comparison-card">
|
||||
<div class="historical-comparison-card__top">
|
||||
<div>
|
||||
<p class="historical-comparison-card__eyebrow">Jugador comparado</p>
|
||||
<h3 class="historical-comparison-card__title">${escapeHtml(item.playerName)}</h3>
|
||||
</div>
|
||||
<div>
|
||||
<p class="historical-comparison-card__delta-label">Delta puesto</p>
|
||||
<strong class="historical-comparison-card__delta-value">${escapeHtml(item.positionDeltaLabel)}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="historical-comparison-card__scores">
|
||||
<div class="historical-comparison-card__score-block">
|
||||
<p class="historical-comparison-card__delta-label">Score V1</p>
|
||||
<strong>${escapeHtml(item.v1ScoreLabel)}</strong>
|
||||
</div>
|
||||
<div class="historical-comparison-card__score-block">
|
||||
<p class="historical-comparison-card__delta-label">Score V2</p>
|
||||
<strong>${escapeHtml(item.v2ScoreLabel)}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="historical-comparison-card__meta">
|
||||
<article>
|
||||
<span>Posicion V1</span>
|
||||
<strong>${escapeHtml(item.v1PositionLabel)}</strong>
|
||||
</article>
|
||||
<article>
|
||||
<span>Posicion V2</span>
|
||||
<strong>${escapeHtml(item.v2PositionLabel)}</strong>
|
||||
</article>
|
||||
<article>
|
||||
<span>Delta score</span>
|
||||
<strong>${escapeHtml(item.scoreDeltaLabel)}</strong>
|
||||
</article>
|
||||
<article>
|
||||
<span>Penalty TK</span>
|
||||
<strong>${escapeHtml(item.teamkillPenaltyLabel)}</strong>
|
||||
</article>
|
||||
</div>
|
||||
<p class="historical-comparison-card__summary">${escapeHtml(item.summary)}</p>
|
||||
</article>
|
||||
`;
|
||||
}
|
||||
|
||||
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 "";
|
||||
|
||||
Reference in New Issue
Block a user