historial de acciones persistente en el jugador y visible desde playerstats
This commit is contained in:
@@ -6,11 +6,11 @@
|
||||
:aria-label="hasShame ? 'Jugador con vergüenza' : 'Jugador'"
|
||||
>
|
||||
<div class="header">
|
||||
<div class="name">{{ player.name || '—' }}</div>
|
||||
<div class="name clickable" @click.stop="toggleHistory" :title="'Ver historial'">{{ player.name || '—' }}</div>
|
||||
<div class="role" :class="player.role">{{ player.role || '—' }}</div>
|
||||
<div v-if="hasShame" class="shame-badge" :title="`Vergüenza: ${player.shameTokens}`">😶 × {{ player.shameTokens }}</div>
|
||||
</div>
|
||||
<div class="tokens">
|
||||
<div v-if="!showHistory" class="tokens">
|
||||
<div class="token pill">
|
||||
<span class="icon">🦃</span>
|
||||
<span class="val"><AnimatedNumber :value="player.pavoTokens ?? 0" /></span>
|
||||
@@ -26,16 +26,49 @@
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
<div class="score">
|
||||
<div v-if="!showHistory" class="score">
|
||||
<span class="label">Puntuación</span>
|
||||
<span class="value"><AnimatedNumber :value="displayScore" /></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fullscreen modal overlay for history -->
|
||||
<div v-if="showHistory" class="history-overlay" @click.self="toggleHistory">
|
||||
<div class="history-modal">
|
||||
<div class="history-modal-header">
|
||||
<div class="title">Historial del sistema — {{ player.name }}</div>
|
||||
<button class="close-history" @click.stop="toggleHistory">Cerrar</button>
|
||||
</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 class="history-table">
|
||||
<div class="history-scroll">
|
||||
<div class="history-header history-grid">
|
||||
<span class="th t">Hora</span>
|
||||
<span class="th r">Rol</span>
|
||||
<span class="th tok">Tokens</span>
|
||||
<span class="th k">Evento</span>
|
||||
<span class="th x">Mensaje</span>
|
||||
<span class="th room">Sala</span>
|
||||
</div>
|
||||
<div v-for="m in historyItems" :key="m.timestamp + '-' + (m.kind||'')" class="history-row history-grid">
|
||||
<span class="t">{{ fmtTime(m.timestamp) }}</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="k">{{ m.kind }}</span>
|
||||
<span class="x">{{ m.text }}</span>
|
||||
<span class="room">{{ (m.roomId || '').slice(0,8) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { computed, ref, onMounted, onBeforeUnmount } from 'vue';
|
||||
import AnimatedNumber from './AnimatedNumber.vue';
|
||||
import { colyseusService } from '../../services/colyseus';
|
||||
|
||||
interface PlayerView {
|
||||
sessionId?: string;
|
||||
@@ -46,7 +79,7 @@ interface PlayerView {
|
||||
shameTokens?: number;
|
||||
}
|
||||
|
||||
const props = defineProps<{ player: PlayerView & { color?: string }; highlight?: boolean }>();
|
||||
const props = defineProps<{ player: PlayerView & { color?: string; uuid?: string }; highlight?: boolean }>();
|
||||
const highlight = computed(() => !!props.highlight);
|
||||
const hasShame = computed(() => (props.player.shameTokens || 0) > 0);
|
||||
|
||||
@@ -54,6 +87,74 @@ const scoreAsP1 = computed(() => (props.player.pavoTokens || 0) * 1 + (props.pla
|
||||
const scoreAsP2 = computed(() => (props.player.eloteTokens || 0) * 1 + (props.player.pavoTokens || 0) * 2);
|
||||
const displayScore = computed(() => props.player.role === 'P2' ? scoreAsP2.value : scoreAsP1.value);
|
||||
const primary = computed(() => props.player.color || '#667eea');
|
||||
|
||||
// History state
|
||||
const showHistory = ref(false);
|
||||
const loadingHistory = ref(false);
|
||||
const historyItems = ref<any[]>([]);
|
||||
const room = computed(() => colyseusService.gameRoom.value as any);
|
||||
|
||||
function toggleHistory() {
|
||||
if (!showHistory.value) {
|
||||
fetchHistory();
|
||||
}
|
||||
showHistory.value = !showHistory.value;
|
||||
}
|
||||
|
||||
function fetchHistory() {
|
||||
const r = room.value;
|
||||
loadingHistory.value = true;
|
||||
if (r) {
|
||||
try { r.send('getSystemHistory', props.player.sessionId || ''); } catch { loadingHistory.value = false; }
|
||||
} else {
|
||||
// Fallback in lobby: fetch by UUID via admin API
|
||||
const uuid = (props.player as any)?.uuid || '';
|
||||
const base = (import.meta as any).env?.VITE_API_URL || `${window.location.protocol}//${window.location.host}/api`;
|
||||
if (!uuid) { loadingHistory.value = false; historyItems.value = []; return; }
|
||||
fetch(`${base}/players/${uuid}/history`).then(r => r.json()).then((data) => {
|
||||
historyItems.value = Array.isArray(data?.history) ? data.history : [];
|
||||
loadingHistory.value = false;
|
||||
}).catch(() => { loadingHistory.value = false; historyItems.value = []; });
|
||||
}
|
||||
}
|
||||
|
||||
function onSystemHistory(payload: any) {
|
||||
if (!payload || payload.for !== (props.player.sessionId || '')) return;
|
||||
try {
|
||||
historyItems.value = Array.isArray(payload.history) ? payload.history : [];
|
||||
} finally {
|
||||
loadingHistory.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function fmtTime(ts: number): string {
|
||||
try { return new Date(Number(ts)).toLocaleTimeString(); } catch { return ''; }
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const r = room.value;
|
||||
if (r) r.onMessage('systemHistory', onSystemHistory);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
// No explicit off(); guard inside handler
|
||||
});
|
||||
|
||||
// Close on Escape and lock scroll while modal open
|
||||
function onKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape' && showHistory.value) {
|
||||
e.stopPropagation();
|
||||
showHistory.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('keydown', onKeydown);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('keydown', onKeydown);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -68,6 +169,27 @@ const primary = computed(() => props.player.color || '#667eea');
|
||||
.role { font-size:12px; padding:2px 8px; border-radius:10px; background:#f0f0f0; color:#555; }
|
||||
.role.P1 { background: color-mix(in srgb, var(--primary) 15%, white); color: var(--primary); }
|
||||
.role.P2 { background: color-mix(in srgb, var(--primary) 15%, white); color: var(--primary); }
|
||||
.name.clickable { cursor: pointer; }
|
||||
.name.clickable:hover { text-decoration: underline; text-underline-offset: 2px; }
|
||||
.history-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.55); display:flex; align-items:center; justify-content:center; z-index: 1500; }
|
||||
.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 .title { font-weight:800; font-size:14px; }
|
||||
.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-table {}
|
||||
.history-scroll { max-height: 60vh; overflow:auto; border:1px solid #e5e9f0; border-radius:8px; background:#fff; margin: 10px; }
|
||||
.history-grid { display:grid; grid-template-columns: 82px 36px 160px 110px 2fr 70px; gap:6px; align-items:center; }
|
||||
.history-header { position: sticky; top: 0; z-index: 1; background:#667eea; border-bottom:1px solid #e5e9f0; padding:6px; box-shadow: 0 2px 6px rgba(0,0,0,0.08); }
|
||||
.history-header .th { font-size:11px; font-weight:800; color:#ffffff; text-transform: uppercase; }
|
||||
.history-row { padding:6px; border-bottom:1px solid #f1f5f9; }
|
||||
.history-row:last-child { border-bottom: none; }
|
||||
.history-row .t { font-family: monospace; font-size:11px; color:#666; }
|
||||
.history-row .r { font-size:11px; font-weight:700; color:#555; text-transform: uppercase; }
|
||||
.history-row .tok { font-size:12px; color:#334155; }
|
||||
.history-row .k { font-size:12px; font-weight:700; color:#334155; }
|
||||
.history-row .x { font-size:12px; color:#475569; }
|
||||
.history-row .room { font-family: monospace; font-size:11px; color:#777; text-align:right; }
|
||||
.shame-badge { margin-left:8px; margin-right:0; background:#fee2e2; color:#b91c1c; border:1px solid #fecaca; border-radius:999px; padding:2px 8px; font-weight:800; font-size:12px; display:flex; align-items:center; gap:6px; }
|
||||
.tokens { display:flex; gap:10px; margin:8px 0; }
|
||||
.pill { display:inline-flex; align-items:center; gap:6px; padding:6px 10px; border-radius:999px; background:#f7f7f7; border:1px solid #eee; }
|
||||
|
||||
@@ -65,6 +65,29 @@ export class GameRoom extends Room<GameState> {
|
||||
setTimeout(() => {
|
||||
broadcastDashboardUpdate();
|
||||
}, 50);
|
||||
|
||||
// Persist as "seen" for currently connected players by UUID
|
||||
try {
|
||||
this.state.players.forEach((player, sessionId) => {
|
||||
if (!player?.connected) return;
|
||||
const uuid = this.sessionToUuid.get(sessionId) || (player as any)?.uuid;
|
||||
if (!uuid) return;
|
||||
try {
|
||||
NameManager.getInstance().appendSystemMessage(uuid, {
|
||||
timestamp,
|
||||
kind,
|
||||
text,
|
||||
roomId: this.roomId,
|
||||
variant: this.state.currentVariant,
|
||||
round: this.state.currentRound,
|
||||
role: (player as any)?.role || '',
|
||||
pavoTokens: (player as any)?.pavoTokens || 0,
|
||||
eloteTokens: (player as any)?.eloteTokens || 0,
|
||||
shameTokens: (player as any)?.shameTokens || 0,
|
||||
});
|
||||
} catch {}
|
||||
});
|
||||
} catch {}
|
||||
}
|
||||
|
||||
onCreate(options: any) {
|
||||
@@ -262,6 +285,20 @@ export class GameRoom extends Room<GameState> {
|
||||
this.broadcast("chat", { id, text, from, fromId: client.sessionId, ts, color });
|
||||
});
|
||||
|
||||
// Provide per-player system history (as seen) to clients
|
||||
this.onMessage("getSystemHistory", (client, targetSessionId: string) => {
|
||||
try {
|
||||
const target = this.state.players.get((targetSessionId || '').toString());
|
||||
if (!target) return;
|
||||
const uuid = this.sessionToUuid.get(target.sessionId) || (target as any)?.uuid;
|
||||
if (!uuid) return;
|
||||
const history = NameManager.getInstance().getSystemHistory(uuid) || [];
|
||||
client.send("systemHistory", { for: target.sessionId, history });
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
});
|
||||
|
||||
// G3 shame token after snatch
|
||||
this.onMessage("assignShame", (client, assign: boolean) => {
|
||||
const player = this.state.players.get(client.sessionId);
|
||||
|
||||
@@ -3,6 +3,18 @@ export class NameManager {
|
||||
private uuidToName: Map<string, string> = new Map();
|
||||
private uuidToColor: Map<string, string> = new Map();
|
||||
private uuidToShame: Map<string, number> = new Map();
|
||||
private uuidToSystemHistory: Map<string, {
|
||||
timestamp: number;
|
||||
kind: string;
|
||||
text: string;
|
||||
roomId?: string;
|
||||
variant?: string;
|
||||
round?: number;
|
||||
role?: 'P1' | 'P2' | '';
|
||||
pavoTokens?: number;
|
||||
eloteTokens?: number;
|
||||
shameTokens?: number;
|
||||
}[]> = new Map();
|
||||
|
||||
// For shuffle functionality
|
||||
private roomAssignments: Map<string, { roomId: string; role: 'P1' | 'P2' }> = new Map();
|
||||
@@ -77,6 +89,7 @@ export class NameManager {
|
||||
this.uuidToName.forEach((_, k) => set.add(k));
|
||||
this.uuidToColor.forEach((_, k) => set.add(k));
|
||||
this.uuidToShame.forEach((_, k) => set.add(k));
|
||||
this.uuidToSystemHistory.forEach((_, k) => set.add(k));
|
||||
return Array.from(set.values());
|
||||
}
|
||||
|
||||
@@ -96,6 +109,7 @@ export class NameManager {
|
||||
this.uuidToName.delete(uuid);
|
||||
this.uuidToColor.delete(uuid);
|
||||
this.uuidToShame.set(uuid, 0);
|
||||
this.uuidToSystemHistory.delete(uuid);
|
||||
}
|
||||
|
||||
// Current game room assignment (for reconnection by UUID)
|
||||
@@ -156,4 +170,40 @@ export class NameManager {
|
||||
getAllRoomAssignments(): Map<string, { roomId: string; role: 'P1' | 'P2' }> {
|
||||
return new Map(this.roomAssignments);
|
||||
}
|
||||
|
||||
// Per-UUID system message history (as seen while connected)
|
||||
appendSystemMessage(uuid: string, entry: {
|
||||
timestamp: number; kind: string; text: string;
|
||||
roomId?: string; variant?: string; round?: number;
|
||||
role?: 'P1'|'P2'|''; pavoTokens?: number; eloteTokens?: number; shameTokens?: number;
|
||||
}): void {
|
||||
if (!uuid) return;
|
||||
const list = this.uuidToSystemHistory.get(uuid) || [];
|
||||
list.push({
|
||||
timestamp: Number(entry.timestamp) || Date.now(),
|
||||
kind: (entry.kind || '').toString(),
|
||||
text: (entry.text || '').toString(),
|
||||
roomId: entry.roomId,
|
||||
variant: entry.variant,
|
||||
round: entry.round,
|
||||
role: entry.role || '',
|
||||
pavoTokens: Number(entry.pavoTokens ?? 0),
|
||||
eloteTokens: Number(entry.eloteTokens ?? 0),
|
||||
shameTokens: Number(entry.shameTokens ?? 0),
|
||||
});
|
||||
if (list.length > 1000) list.splice(0, list.length - 1000);
|
||||
this.uuidToSystemHistory.set(uuid, list);
|
||||
}
|
||||
|
||||
getSystemHistory(uuid: string): {
|
||||
timestamp: number; kind: string; text: string;
|
||||
roomId?: string; variant?: string; round?: number;
|
||||
role?: 'P1'|'P2'|''; pavoTokens?: number; eloteTokens?: number; shameTokens?: number;
|
||||
}[] {
|
||||
return [...(this.uuidToSystemHistory.get(uuid) || [])];
|
||||
}
|
||||
|
||||
clearSystemHistory(uuid: string): void {
|
||||
this.uuidToSystemHistory.delete(uuid);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user