Dashboard real-time updates con SSE y tabla de rooms

- Implementación de Server-Sent Events (SSE) para actualizaciones en tiempo real
- Nuevo componente RoomsTable para vista de águila con animaciones de tokens
- Componente RoomCard extraído para reutilización
- Modal para ver detalles de rooms
- SystemMessageDisplay usando AnimatedNumber creativamente
- Indicador de conexión SSE con fallback a polling
- Colores dinámicos de texto basados en brillo del fondo
- Backend: broadcast de actualizaciones del dashboard
- Backend: mensajes del sistema (excepto cambios de ronda) visibles en dashboard
- Configuración de Vite para acceso desde red local
This commit is contained in:
2025-08-11 23:07:09 -06:00
parent deb63d4e38
commit 32f69805f0
9 changed files with 1689 additions and 265 deletions

View File

@@ -2,27 +2,45 @@ import { Room, Client } from "colyseus";
import { GameState } from "./schemas/GameState";
import { GameStatus } from "../../../shared/types";
import { NameManager } from "../utils/nameManager";
import { broadcastDashboardUpdate } from "../adminApi";
export class GameRoom extends Room<GameState> {
maxClients = 2;
private gameInterval?: NodeJS.Timeout;
private recentSystemMessage: { text: string; kind: string; timestamp: number } | null = null;
private sysChat(text: string, kind: string) {
const timestamp = Date.now();
// Store the most recent system message for dashboard (exclude round changes)
if (kind !== 'round_advance') {
this.recentSystemMessage = { text, kind, timestamp };
}
this.broadcast("chat", {
id: `${Date.now()}-${Math.random().toString(36).slice(2)}`,
id: `${timestamp}-${Math.random().toString(36).slice(2)}`,
text,
from: "Sistema",
fromId: "system",
ts: Date.now(),
ts: timestamp,
kind,
} as any);
// Notify dashboard immediately after system message
setTimeout(() => {
broadcastDashboardUpdate();
}, 50);
}
onCreate(options: any) {
this.setState(new GameState());
this.state.roomId = this.roomId;
// Expose status via metadata for lobby listing
this.setMetadata({ gameStatus: 'waiting' });
this.setMetadata({
gameStatus: 'waiting',
currentRound: this.state.currentRound,
currentVariant: this.state.currentVariant
});
// Variant selection (both players can change)
this.onMessage("setVariant", (client, variant: string) => {
@@ -34,6 +52,12 @@ export class GameRoom extends Room<GameState> {
if (this.state.gameStatus === GameStatus.FINISHED) {
this.state.gameStatus = GameStatus.PLAYING;
}
// Update metadata with new variant and round
this.setMetadata({
gameStatus: this.state.gameStatus === GameStatus.WAITING ? 'waiting' : 'playing',
currentRound: this.state.currentRound,
currentVariant: this.state.currentVariant
});
// G2: Force offer by default
if (variant === 'G2') {
this.state.forcedByP2 = true;
@@ -145,6 +169,9 @@ export class GameRoom extends Room<GameState> {
const rE = this.state.requestElote;
if (p2.pavoTokens >= rP) { p2.pavoTokens -= rP; p1.pavoTokens += rP; }
if (p2.eloteTokens >= rE) { p2.eloteTokens -= rE; p1.eloteTokens += rE; }
// Notify dashboard of token changes
broadcastDashboardUpdate();
}
// Clear offer now
this.clearOffer();
@@ -179,7 +206,11 @@ export class GameRoom extends Room<GameState> {
if (assign && this.state.currentVariant === "G3" && this.state.p2Action === "snatch") {
// increment P2 shame immediately
const p2 = this.state.p2Id ? this.state.players.get(this.state.p2Id) : undefined;
if (p2) p2.shameTokens += 1;
if (p2) {
p2.shameTokens += 1;
// Notify dashboard of token change
broadcastDashboardUpdate();
}
}
// System chat feedback
if (assign) this.sysChat('😶 P1 asignó un token de vergüenza a P2', 'p1_shame');
@@ -232,6 +263,16 @@ export class GameRoom extends Room<GameState> {
roomId: this.roomId
});
// System message for player join
if (this.state.players.size === 1) {
this.sysChat(`👋 ${playerName} se unió - esperando oponente`, 'player_join');
} else if (this.state.players.size === 2) {
this.sysChat(`🎯 Todos los jugadores conectados`, 'players_ready');
}
// Notify dashboard of player join
broadcastDashboardUpdate();
if (this.state.players.size === 2 && this.state.gameStatus === GameStatus.WAITING) {
this.startGame();
}
@@ -246,6 +287,9 @@ export class GameRoom extends Room<GameState> {
// Don't release the name here - it's managed by the LobbyRoom
}
// Notify dashboard of player leave
broadcastDashboardUpdate();
if (this.state.gameStatus === GameStatus.PLAYING) {
if (this.getConnectedPlayersCount() < 2) {
this.pauseGame();
@@ -265,7 +309,13 @@ export class GameRoom extends Room<GameState> {
} catch {}
if (this.state.gameStatus === GameStatus.PAUSED && this.getConnectedPlayersCount() === 2) {
this.state.resumeGame();
this.setMetadata({ gameStatus: 'playing' });
this.setMetadata({
gameStatus: 'playing',
currentRound: this.state.currentRound,
currentVariant: this.state.currentVariant
});
// Notify dashboard of game resume
broadcastDashboardUpdate();
}
}).catch(() => {
// reconnection window expired; nothing to do here
@@ -305,7 +355,11 @@ export class GameRoom extends Room<GameState> {
private startGame() {
console.log(`[GameRoom] Starting demo game in room ${this.roomId}`);
this.state.startGame();
this.setMetadata({ gameStatus: 'playing' });
this.setMetadata({
gameStatus: 'playing',
currentRound: this.state.currentRound,
currentVariant: this.state.currentVariant
});
// G2: Force offer by default when starting game
if (this.state.currentVariant === 'G2') {
this.state.forcedByP2 = true;
@@ -313,19 +367,35 @@ export class GameRoom extends Room<GameState> {
this.broadcast("gameStart");
// System chat: start at round 1
this.sysChat(`▶️ Ronda ${this.state.currentRound}/3`, 'round_advance');
// Notify dashboard of game start (with some delay to ensure sysChat is processed)
setTimeout(() => {
broadcastDashboardUpdate();
}, 100);
}
private pauseGame() {
console.log(`[GameRoom] Pausing game in room ${this.roomId}`);
this.state.pauseGame();
this.broadcast("gamePaused");
this.setMetadata({ gameStatus: 'paused' });
this.setMetadata({
gameStatus: 'paused',
currentRound: this.state.currentRound,
currentVariant: this.state.currentVariant
});
// Notify dashboard of game pause
broadcastDashboardUpdate();
}
private endGame() {
console.log(`[GameRoom] Demo game ended in room ${this.roomId}`);
this.broadcast("gameEnd", {});
this.setMetadata({ gameStatus: 'finished' });
this.setMetadata({
gameStatus: 'finished',
currentRound: this.state.currentRound,
currentVariant: this.state.currentVariant
});
// Notify dashboard of game end
broadcastDashboardUpdate();
}
private resolveP2Action() {
@@ -351,6 +421,8 @@ export class GameRoom extends Room<GameState> {
p2.eloteTokens -= this.state.requestElote; p1.eloteTokens += this.state.requestElote;
}
this.clearOffer();
// Notify dashboard of token changes
broadcastDashboardUpdate();
}
else if (p2Action === 'reject') {
// No changes
@@ -363,6 +435,8 @@ export class GameRoom extends Room<GameState> {
p1.eloteTokens -= this.state.offerElote; p2.eloteTokens += this.state.offerElote;
}
// Keep offer data around for potential G4 report; it will be cleared on report or next round
// Notify dashboard of token changes
broadcastDashboardUpdate();
}
}
@@ -386,7 +460,11 @@ export class GameRoom extends Room<GameState> {
this.state.restartGame();
this.broadcast("gameRestart");
this.setMetadata({ gameStatus: 'waiting' });
this.setMetadata({
gameStatus: 'waiting',
currentRound: this.state.currentRound,
currentVariant: this.state.currentVariant
});
if (this.state.players.size === 2) {
setTimeout(() => this.startGame(), 500);
@@ -411,7 +489,7 @@ export class GameRoom extends Room<GameState> {
}
getState() {
return {
const result = {
roomId: this.roomId,
players: Array.from(this.state.players.values()).map(p => ({
sessionId: p.sessionId,
@@ -420,10 +498,12 @@ export class GameRoom extends Room<GameState> {
pavoTokens: p.pavoTokens,
eloteTokens: p.eloteTokens,
shameTokens: p.shameTokens,
color: p.color,
})),
gameStatus: this.state.gameStatus,
variant: this.state.currentVariant,
round: this.state.currentRound,
recentSystemMessage: this.recentSystemMessage,
decisions: {
p1Action: this.state.p1Action,
p2Action: this.state.p2Action,
@@ -440,14 +520,24 @@ export class GameRoom extends Room<GameState> {
},
outcome: {}
};
return result;
}
private advanceRound() {
if (this.state.currentRound < 3) {
this.state.currentRound += 1;
this.state.resetRound();
// Update metadata with new round
this.setMetadata({
gameStatus: 'playing',
currentRound: this.state.currentRound,
currentVariant: this.state.currentVariant
});
this.broadcast("roundStarted", { round: this.state.currentRound });
this.sysChat(`▶️ Ronda ${this.state.currentRound}/3`, 'round_advance');
// Notify dashboard of round advance
broadcastDashboardUpdate();
} else {
this.state.finishGame();
this.endGame();