LeaderBoard creado v1
This commit is contained in:
@@ -87,35 +87,66 @@ function buildCsvByRoom(): string {
|
|||||||
return lines.join('\n') + '\n';
|
return lines.join('\n') + '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildCsvByUuid(): string {
|
async function buildCsvByUuid(): Promise<string> {
|
||||||
const headers = [
|
const headers = [
|
||||||
'roomId', 'variant', 'round', 'status',
|
'roomId', 'variant', 'round', 'status',
|
||||||
'uuid', 'name', 'role', 'pavo', 'elote', 'shame'
|
'uuid', 'name', 'role', 'pavo', 'elote', 'shame', 'events_made'
|
||||||
];
|
];
|
||||||
const lines: string[] = [headers.join(',')];
|
const lines: string[] = [headers.join(',')];
|
||||||
|
|
||||||
gameRooms.value.forEach(room => {
|
const apiBase = (import.meta as any).env?.VITE_API_URL || `${window.location.protocol}//${window.location.host}/api`;
|
||||||
|
const historyCache = new Map<string, any[]>();
|
||||||
|
|
||||||
|
for (const room of gameRooms.value) {
|
||||||
const det = (props.roomDetails[room.roomId] || {}) as any;
|
const det = (props.roomDetails[room.roomId] || {}) as any;
|
||||||
const status = det.gameStatus || room?.metadata?.gameStatus || 'waiting';
|
const status = det.gameStatus || room?.metadata?.gameStatus || 'waiting';
|
||||||
const variant = det.variant || room?.metadata?.currentVariant || 'G1';
|
const variant = det.variant || room?.metadata?.currentVariant || 'G1';
|
||||||
const round = det.round || room?.metadata?.currentRound || 1;
|
const round = det.round || room?.metadata?.currentRound || 1;
|
||||||
const players = (det.players || []) as PlayerRow[];
|
const players = (det.players || []) as PlayerRow[];
|
||||||
players.forEach(p => {
|
|
||||||
|
for (const p of players) {
|
||||||
|
const uuid = (p?.uuid || '').toString();
|
||||||
|
let history = uuid ? historyCache.get(uuid) : [];
|
||||||
|
if (uuid && !history) {
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`${apiBase}/players/${uuid}/history`);
|
||||||
|
const data = await resp.json();
|
||||||
|
history = Array.isArray(data?.history) ? data.history : [];
|
||||||
|
} catch {
|
||||||
|
history = [];
|
||||||
|
}
|
||||||
|
historyCache.set(uuid, history);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build events_made generically:
|
||||||
|
// - If kind starts with p1_ or p2_, include only when event.role matches P1/P2
|
||||||
|
// - Otherwise include by default (system/agnostic events)
|
||||||
|
const events = (history || []).filter((h: any) => {
|
||||||
|
const kind = (h?.kind || '').toString();
|
||||||
|
if (!kind) return false;
|
||||||
|
const evRole = kind.startsWith('p1_') ? 'P1' : (kind.startsWith('p2_') ? 'P2' : null);
|
||||||
|
if (!evRole) return true; // role-agnostic
|
||||||
|
const r = ((h?.role || '') as string).toUpperCase();
|
||||||
|
return r === evRole;
|
||||||
|
}).map((h: any) => h.kind);
|
||||||
|
|
||||||
|
const eventsHistory = events.join('|');
|
||||||
const row = [
|
const row = [
|
||||||
room.roomId,
|
room.roomId,
|
||||||
variant,
|
variant,
|
||||||
round,
|
round,
|
||||||
status,
|
status,
|
||||||
p?.uuid || '',
|
uuid,
|
||||||
p?.name || '',
|
p?.name || '',
|
||||||
p?.role || '',
|
p?.role || '',
|
||||||
p?.pavoTokens ?? 0,
|
p?.pavoTokens ?? 0,
|
||||||
p?.eloteTokens ?? 0,
|
p?.eloteTokens ?? 0,
|
||||||
p?.shameTokens ?? 0
|
p?.shameTokens ?? 0,
|
||||||
|
eventsHistory
|
||||||
].map(csvEscape).join(',');
|
].map(csvEscape).join(',');
|
||||||
lines.push(row);
|
lines.push(row);
|
||||||
});
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
return lines.join('\n') + '\n';
|
return lines.join('\n') + '\n';
|
||||||
}
|
}
|
||||||
@@ -137,7 +168,7 @@ function triggerDownload(csv: string, suffix: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function downloadCsvByRoom() { triggerDownload(buildCsvByRoom(), 'by-room'); }
|
function downloadCsvByRoom() { triggerDownload(buildCsvByRoom(), 'by-room'); }
|
||||||
function downloadCsvByUuid() { triggerDownload(buildCsvByUuid(), 'by-uuid'); }
|
async function downloadCsvByUuid() { const csv = await buildCsvByUuid(); triggerDownload(csv, 'by-uuid'); }
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import Game from '../views/Game.vue';
|
|||||||
import Dashboard from '../views/Dashboard.vue';
|
import Dashboard from '../views/Dashboard.vue';
|
||||||
import DemoGame from '../views/DemoGame.vue';
|
import DemoGame from '../views/DemoGame.vue';
|
||||||
import UuidSelector from '../views/UuidSelector.vue';
|
import UuidSelector from '../views/UuidSelector.vue';
|
||||||
|
import Leaderboard from '../views/Leaderboard.vue';
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(),
|
history: createWebHistory(),
|
||||||
@@ -28,6 +29,11 @@ const router = createRouter({
|
|||||||
name: 'Dashboard',
|
name: 'Dashboard',
|
||||||
component: Dashboard
|
component: Dashboard
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/leaderboard',
|
||||||
|
name: 'Leaderboard',
|
||||||
|
component: Leaderboard
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
name: 'UuidSelector',
|
name: 'UuidSelector',
|
||||||
|
|||||||
223
client/src/views/Leaderboard.vue
Normal file
223
client/src/views/Leaderboard.vue
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
<template>
|
||||||
|
<div class="leaderboard">
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>📈 Leaderboard</h1>
|
||||||
|
<div class="actions">
|
||||||
|
<button class="btn" @click="refreshAll" :disabled="loading">{{ loading ? 'Actualizando…' : 'Actualizar' }}</button>
|
||||||
|
<button class="btn toggle" :class="{ active: showPercent }" @click="showPercent = !showPercent" :disabled="loading">
|
||||||
|
{{ showPercent ? 'Ver conteos' : 'Ver %' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2 class="section-title">Eventos globales (detalle por tipo)</h2>
|
||||||
|
<div v-if="loading" class="placeholder">Cargando datos…</div>
|
||||||
|
<div v-else class="bars">
|
||||||
|
<div v-for="k in EVENTS" :key="k" class="bar-row">
|
||||||
|
<span class="label">{{ friendlyKind(k) }}</span>
|
||||||
|
<div class="bar">
|
||||||
|
<div class="bar-fill p1" :style="{ width: globalBarWidth(k) + '%' }"></div>
|
||||||
|
</div>
|
||||||
|
<span class="value">{{ globalValueLabel(k) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="hint small">Nota: basado en mensajes disponibles por sala.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2 class="section-title">Estadísticas por jugador</h2>
|
||||||
|
<div class="player-filter">
|
||||||
|
<label>Jugador:</label>
|
||||||
|
<select v-model="selectedUuid" @change="loadPlayerHistory" class="select">
|
||||||
|
<option value="">— Seleccionar —</option>
|
||||||
|
<option v-for="p in players" :key="p.uuid" :value="p.uuid">{{ p.name }} ({{ p.uuid.slice(0,8) }})</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div v-if="playerLoading" class="placeholder">Cargando historial del jugador…</div>
|
||||||
|
<div v-else-if="!selectedUuid" class="placeholder">Selecciona un jugador para ver sus eventos.</div>
|
||||||
|
<div v-else class="player-stats">
|
||||||
|
<div class="bars">
|
||||||
|
<div class="bar-row" v-for="k in EVENTS" :key="'p-' + k">
|
||||||
|
<span class="label">{{ friendlyKind(k) }}</span>
|
||||||
|
<div class="bar"><div class="bar-fill p2" :style="{ width: playerBarWidth(k) + '%' }"></div></div>
|
||||||
|
<span class="value">{{ playerValueLabel(k) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted } from 'vue';
|
||||||
|
|
||||||
|
interface RoomInfo { roomId: string; metadata?: any; }
|
||||||
|
interface RoomState { players?: any[]; systemMessages?: { kind: string }[] }
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const EVENTS = [
|
||||||
|
'p1_propose', 'p1_no_offer',
|
||||||
|
'p2_snatch', 'p2_accept', 'p2_force', 'p2_no_force', 'p2_reject',
|
||||||
|
'p1_shame', 'p1_no_shame', 'p1_report', 'p1_no_report'
|
||||||
|
];
|
||||||
|
const globalEventCounts = ref<Record<string, number>>((Object.fromEntries(EVENTS.map(k => [k, 0])) as Record<string, number>));
|
||||||
|
const showPercent = ref(false);
|
||||||
|
const globalMax = computed(() => {
|
||||||
|
const vals = EVENTS.map(k => globalEventCounts.value[k] || 0);
|
||||||
|
const m = Math.max(0, ...vals);
|
||||||
|
return m || 1;
|
||||||
|
});
|
||||||
|
const globalTotal = computed(() => EVENTS.reduce((acc, k) => acc + (globalEventCounts.value[k] || 0), 0) || 1);
|
||||||
|
function globalBarWidth(k: string) {
|
||||||
|
const v = globalEventCounts.value[k] || 0;
|
||||||
|
return Math.round((v / (showPercent.value ? globalTotal.value : globalMax.value)) * 100);
|
||||||
|
}
|
||||||
|
function globalValueLabel(k: string) {
|
||||||
|
const v = globalEventCounts.value[k] || 0;
|
||||||
|
return showPercent.value ? `${Math.round((v / globalTotal.value) * 100)}%` : String(v);
|
||||||
|
}
|
||||||
|
const rooms = ref<RoomInfo[]>([]);
|
||||||
|
const players = ref<{ uuid: string; name: string }[]>([]);
|
||||||
|
|
||||||
|
const selectedUuid = ref('');
|
||||||
|
const playerLoading = ref(false);
|
||||||
|
const playerEventCounts = ref<Record<string, number>>((Object.fromEntries(EVENTS.map(k => [k, 0])) as Record<string, number>));
|
||||||
|
const playerMax = computed(() => {
|
||||||
|
const vals = EVENTS.map(k => playerEventCounts.value[k] || 0);
|
||||||
|
const m = Math.max(0, ...vals);
|
||||||
|
return m || 1;
|
||||||
|
});
|
||||||
|
const playerTotal = computed(() => EVENTS.reduce((acc, k) => acc + (playerEventCounts.value[k] || 0), 0) || 1);
|
||||||
|
function playerBarWidth(k: string) {
|
||||||
|
const v = playerEventCounts.value[k] || 0;
|
||||||
|
return Math.round((v / (showPercent.value ? playerTotal.value : playerMax.value)) * 100);
|
||||||
|
}
|
||||||
|
function playerValueLabel(k: string) {
|
||||||
|
const v = playerEventCounts.value[k] || 0;
|
||||||
|
return showPercent.value ? `${Math.round((v / playerTotal.value) * 100)}%` : String(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiBase = (import.meta as any).env?.VITE_API_URL || `${window.location.protocol}//${window.location.host}/api`;
|
||||||
|
|
||||||
|
async function fetchRooms(): Promise<RoomInfo[]> {
|
||||||
|
const res = await fetch(`${apiBase}/rooms`);
|
||||||
|
return await res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchRoomStats(roomId: string): Promise<RoomState> {
|
||||||
|
const res = await fetch(`${apiBase}/rooms/${roomId}/stats`);
|
||||||
|
return await res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshAll() {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const list = await fetchRooms();
|
||||||
|
rooms.value = (list || []).filter((r: any) => r?.name === 'game');
|
||||||
|
// reset counts
|
||||||
|
globalEventCounts.value = Object.fromEntries(EVENTS.map(k => [k, 0])) as Record<string, number>;
|
||||||
|
const playerMap = new Map<string, string>();
|
||||||
|
|
||||||
|
for (const r of rooms.value) {
|
||||||
|
try {
|
||||||
|
const s = await fetchRoomStats(r.roomId);
|
||||||
|
const msgs = (s?.systemMessages || []) as Array<{ kind: string }>;
|
||||||
|
msgs.forEach(m => {
|
||||||
|
const k = (m?.kind || '').toString();
|
||||||
|
if (EVENTS.includes(k)) {
|
||||||
|
globalEventCounts.value[k] = (globalEventCounts.value[k] || 0) + 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// collect players for filter
|
||||||
|
(s?.players || []).forEach((p: any) => {
|
||||||
|
const uuid = (p?.uuid || p?.sessionId || '').toString();
|
||||||
|
if (uuid && !playerMap.has(uuid)) playerMap.set(uuid, (p?.name || 'player'));
|
||||||
|
});
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
players.value = Array.from(playerMap.entries()).map(([uuid, name]) => ({ uuid, name }));
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadPlayerHistory() {
|
||||||
|
playerEventCounts.value = Object.fromEntries(EVENTS.map(k => [k, 0])) as Record<string, number>;
|
||||||
|
if (!selectedUuid.value) return;
|
||||||
|
playerLoading.value = true;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${apiBase}/players/${selectedUuid.value}/history`);
|
||||||
|
const data = await res.json();
|
||||||
|
const history = Array.isArray(data?.history) ? data.history : [];
|
||||||
|
const made = history.filter((h: any) => {
|
||||||
|
const kind = (h?.kind || '').toString();
|
||||||
|
if (!kind) return false;
|
||||||
|
const evRole = kind.startsWith('p1_') ? 'P1' : (kind.startsWith('p2_') ? 'P2' : null);
|
||||||
|
if (!evRole) return true; // system/agnostic
|
||||||
|
return ((h?.role || '').toUpperCase() === evRole);
|
||||||
|
});
|
||||||
|
made.forEach((h: any) => {
|
||||||
|
const k = (h?.kind || '').toString();
|
||||||
|
if (EVENTS.includes(k)) {
|
||||||
|
playerEventCounts.value[k] = (playerEventCounts.value[k] || 0) + 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
playerEventCounts.value = Object.fromEntries(EVENTS.map(k => [k, 0])) as Record<string, number>;
|
||||||
|
} finally {
|
||||||
|
playerLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(refreshAll);
|
||||||
|
|
||||||
|
function friendlyKind(kind: string): string {
|
||||||
|
const k = (kind || '').toString();
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
p1_propose: 'Ofrecer',
|
||||||
|
p1_no_offer: 'No Ofrecer',
|
||||||
|
p2_snatch: 'Robar',
|
||||||
|
p2_accept: 'Aceptar Oferta',
|
||||||
|
p2_force: 'Forzar Oferta',
|
||||||
|
p2_no_force: 'No Forzar Oferta',
|
||||||
|
p2_reject: 'Rechazar Oferta',
|
||||||
|
p1_shame: 'Asignar Vergüenza',
|
||||||
|
p1_no_shame: 'No Asignar Vergüenza',
|
||||||
|
p1_report: 'Denunciar',
|
||||||
|
p1_no_report: 'No Denunciar',
|
||||||
|
};
|
||||||
|
return map[k] || k;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.leaderboard { min-height: 100vh; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 20px; }
|
||||||
|
.container { background:#fff; border-radius: 16px; border:1px solid #e5e9f0; box-shadow: 0 18px 50px rgba(0,0,0,0.35); max-width: 1000px; margin: 0 auto; padding: 16px; }
|
||||||
|
.header { display:flex; align-items:center; justify-content:space-between; }
|
||||||
|
.header h1 { margin: 0; }
|
||||||
|
.actions .btn { background:#667eea; color:#fff; border:none; border-radius:8px; padding:8px 12px; font-weight:700; cursor:pointer; }
|
||||||
|
.actions .btn.toggle { background:#eef2ff; color:#3949ab; border:1px solid #c7d2fe; }
|
||||||
|
.actions .btn.toggle.active { background:#3949ab; color:#fff; border-color:#2e3f9a; }
|
||||||
|
.section { margin-top: 16px; padding: 12px; background:#f8fafc; border:1px solid #e5e9f0; border-radius: 12px; }
|
||||||
|
.section-title { margin: 0 0 10px; color:#334155; }
|
||||||
|
.placeholder { color:#64748b; padding: 12px; background:#fff; border:1px dashed #e5e9f0; border-radius:10px; }
|
||||||
|
.bars { display:flex; flex-direction:column; gap:8px; }
|
||||||
|
.bar-row { display:grid; grid-template-columns: 180px 1fr 70px; gap:8px; align-items:center; }
|
||||||
|
.bar { height: 14px; background:#eef2ff; border-radius: 999px; overflow:hidden; border:1px solid #c7d2fe; }
|
||||||
|
.bar-fill { height: 100%; }
|
||||||
|
.bar-fill.p1 { background: #667eea; }
|
||||||
|
.bar-fill.p2 { background: #764ba2; }
|
||||||
|
.value { font-weight:800; color:#334155; text-align:right; }
|
||||||
|
.hint.small { font-size: 12px; color:#64748b; }
|
||||||
|
|
||||||
|
.player-filter { display:flex; align-items:center; gap:8px; margin-bottom: 8px; }
|
||||||
|
.select { padding:6px 8px; border:1px solid #cbd5e1; border-radius:8px; }
|
||||||
|
.player-stats { display:flex; flex-direction:column; gap:10px; }
|
||||||
|
.events-list { background:#fff; border:1px solid #e5e9f0; border-radius:8px; padding:8px; }
|
||||||
|
.events-head { font-weight:800; color:#334155; font-size: 13px; margin-bottom: 6px; }
|
||||||
|
.events-body { display:flex; gap:6px; flex-wrap: wrap; }
|
||||||
|
.pill { padding:4px 8px; background:#f1f5f9; border:1px solid #cbd5e1; border-radius:999px; font-size:12px; font-weight:700; color:#334155; }
|
||||||
|
</style>
|
||||||
@@ -37,7 +37,12 @@
|
|||||||
<div class="history-modal">
|
<div class="history-modal">
|
||||||
<div class="history-modal-header">
|
<div class="history-modal-header">
|
||||||
<div class="title">Historial del sistema — {{ player.name }}</div>
|
<div class="title">Historial del sistema — {{ player.name }}</div>
|
||||||
<button class="close-history" @click.stop="toggleHistory">Cerrar</button>
|
<div class="header-actions">
|
||||||
|
<button class="btn-filter" :class="{ active: onlyEventsMade }" @click.stop="onlyEventsMade = !onlyEventsMade" :title="onlyEventsMade ? 'Mostrando solo eventos hechos por el jugador' : 'Mostrar solo eventos hechos por el jugador'">
|
||||||
|
{{ onlyEventsMade ? 'Filtro: activo' : 'Filtro: eventos del jugador' }}
|
||||||
|
</button>
|
||||||
|
<button class="close-history" @click.stop="toggleHistory">Cerrar</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="loadingHistory" class="history-loading">Cargando…</div>
|
<div v-if="loadingHistory" class="history-loading">Cargando…</div>
|
||||||
<div v-else-if="!historyItems.length" class="history-empty">Sin historial</div>
|
<div v-else-if="!historyItems.length" class="history-empty">Sin historial</div>
|
||||||
@@ -51,11 +56,11 @@
|
|||||||
<span class="th x">Mensaje</span>
|
<span class="th x">Mensaje</span>
|
||||||
<span class="th room">Sala</span>
|
<span class="th room">Sala</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-for="m in historyItems" :key="m.timestamp + '-' + (m.kind||'')" class="history-row history-grid">
|
<div v-for="m in filteredHistory" :key="m.timestamp + '-' + (m.kind||'')" class="history-row history-grid">
|
||||||
<span class="t">{{ fmtTime(m.timestamp) }}</span>
|
<span class="t">{{ fmtTime(m.timestamp) }}</span>
|
||||||
<span class="r">{{ (m.role || '') || '—' }}</span>
|
<span class="r">{{ (m.role || '') || '—' }}</span>
|
||||||
<span class="tok">🦃 {{ m.pavoTokens ?? 0 }} · 🌽 {{ m.eloteTokens ?? 0 }} <span v-if="(m.shameTokens ?? 0) > 0">· 😶 {{ m.shameTokens }}</span></span>
|
<span class="tok">🦃 {{ m.pavoTokens ?? 0 }} · 🌽 {{ m.eloteTokens ?? 0 }} <span v-if="(m.shameTokens ?? 0) > 0">· 😶 {{ m.shameTokens }}</span></span>
|
||||||
<span class="k">{{ m.kind }}</span>
|
<span class="k">{{ friendlyKind(m.kind) }}</span>
|
||||||
<span class="x">{{ m.text }}</span>
|
<span class="x">{{ m.text }}</span>
|
||||||
<span class="room">{{ (m.roomId || '').slice(0,8) }}</span>
|
<span class="room">{{ (m.roomId || '').slice(0,8) }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -92,6 +97,19 @@ const primary = computed(() => props.player.color || '#667eea');
|
|||||||
const showHistory = ref(false);
|
const showHistory = ref(false);
|
||||||
const loadingHistory = ref(false);
|
const loadingHistory = ref(false);
|
||||||
const historyItems = ref<any[]>([]);
|
const historyItems = ref<any[]>([]);
|
||||||
|
const onlyEventsMade = ref(false);
|
||||||
|
const filteredHistory = computed(() => {
|
||||||
|
if (!onlyEventsMade.value) return historyItems.value || [];
|
||||||
|
const list = Array.isArray(historyItems.value) ? historyItems.value : [];
|
||||||
|
return list.filter((h: any) => {
|
||||||
|
const kind = (h?.kind || '').toString();
|
||||||
|
if (!kind) return false;
|
||||||
|
const prefix = kind.slice(0,3).toLowerCase();
|
||||||
|
if (prefix === 'p1_') return ((h?.role || '').toUpperCase() === 'P1');
|
||||||
|
if (prefix === 'p2_') return ((h?.role || '').toUpperCase() === 'P2');
|
||||||
|
return true; // system/agnostic events
|
||||||
|
});
|
||||||
|
});
|
||||||
const room = computed(() => colyseusService.gameRoom.value as any);
|
const room = computed(() => colyseusService.gameRoom.value as any);
|
||||||
|
|
||||||
function toggleHistory() {
|
function toggleHistory() {
|
||||||
@@ -155,6 +173,24 @@ onMounted(() => {
|
|||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
window.removeEventListener('keydown', onKeydown);
|
window.removeEventListener('keydown', onKeydown);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function friendlyKind(kind: string): string {
|
||||||
|
const k = (kind || '').toString();
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
p1_propose: 'Ofrecer',
|
||||||
|
p1_no_offer: 'No Ofrecer',
|
||||||
|
p2_snatch: 'Robar',
|
||||||
|
p2_accept: 'Aceptar Oferta',
|
||||||
|
p2_force: 'Forzar Oferta',
|
||||||
|
p2_no_force: 'No Forzar Oferta',
|
||||||
|
p2_reject: 'Rechazar Oferta',
|
||||||
|
p1_shame: 'Asignar Vergüenza',
|
||||||
|
p1_no_shame: 'No Asignar Vergüenza',
|
||||||
|
p1_report: 'Denunciar',
|
||||||
|
p1_no_report: 'No Denunciar',
|
||||||
|
};
|
||||||
|
return map[k] || k;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -175,6 +211,9 @@ onBeforeUnmount(() => {
|
|||||||
.history-modal { width: min(900px, 94vw); background:#fff; border-radius:12px; border:1px solid #e5e9f0; box-shadow: 0 30px 80px rgba(0,0,0,0.45); }
|
.history-modal { width: min(900px, 94vw); background:#fff; border-radius:12px; border:1px solid #e5e9f0; box-shadow: 0 30px 80px rgba(0,0,0,0.45); }
|
||||||
.history-modal-header { display:flex; align-items:center; justify-content:space-between; padding:10px 12px; border-bottom:1px solid #e5e9f0; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color:#fff; border-top-left-radius:12px; border-top-right-radius:12px; }
|
.history-modal-header { display:flex; align-items:center; justify-content:space-between; padding:10px 12px; border-bottom:1px solid #e5e9f0; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color:#fff; border-top-left-radius:12px; border-top-right-radius:12px; }
|
||||||
.history-modal-header .title { font-weight:800; font-size:14px; }
|
.history-modal-header .title { font-weight:800; font-size:14px; }
|
||||||
|
.history-modal-header .header-actions { display:flex; align-items:center; gap:8px; }
|
||||||
|
.btn-filter { background:#eef2ff; color:#3949ab; border:1px solid #c7d2fe; border-radius:8px; padding:6px 10px; font-weight:700; cursor:pointer; }
|
||||||
|
.btn-filter.active { background:#3949ab; color:#fff; border-color:#2e3f9a; }
|
||||||
.close-history { background:#fff; color:#3949ab; border:1px solid #c7d2fe; border-radius:8px; padding:6px 10px; font-weight:700; cursor:pointer; }
|
.close-history { background:#fff; color:#3949ab; border:1px solid #c7d2fe; border-radius:8px; padding:6px 10px; font-weight:700; cursor:pointer; }
|
||||||
.history-loading, .history-empty { font-size:12px; color:#666; padding:6px; text-align:center; }
|
.history-loading, .history-empty { font-size:12px; color:#666; padding:6px; text-align:center; }
|
||||||
.history-table {}
|
.history-table {}
|
||||||
|
|||||||
Reference in New Issue
Block a user