feat: Complete Admin Dashboard with game control and player management (v0.0.8-alpha)
## Major Features Added - **🎛️ Complete Admin Dashboard**: Real-time player monitoring with detailed stats - **👥 Player Management**: Individual and mass player kicking with proper notifications - **🎯 Global Round Control**: Advance/retreat rounds across all rooms simultaneously - **⏸️ Game Control**: Pause/resume games from admin interface - **🔔 Client Notifications**: Players receive alerts for kicks and round changes ## Technical Improvements - **🏗️ Official Colyseus API**: Replaced global variable hacks with `matchMaker.query()` and `matchMaker.remoteRoomCall()` - **📡 Proper Client Communication**: Implemented broadcast messages for `adminKicked`, `gamePaused`, `gameResumed`, `roundChanged` - **🎮 Enhanced GameRoom Methods**: Added `pauseGame()`, `resumeGame()`, `advanceRound()`, `previousRound()`, `_forceClientDisconnect()`, `_forceDisconnectAllClients()`, `getInspectData()` ## UI/UX Enhancements - **📊 Detailed Player Info**: Name, room, role, producer type, and current tokens (🦃☕🌽) - **🚫 Proper Kick Notifications**: Clients auto-redirect to home with clear messaging - **🎨 Improved Admin Interface**: Better organized controls for non-technical commentators - **📱 Responsive Design**: Works well on different screen sizes ## Bug Fixes - **🔧 Fixed Admin Service URLs**: Now correctly calls Colyseus server (port 2567) instead of admin server (port 3001) - **✅ Mass Kick Notifications**: All players receive proper notifications when expelled en masse - **🔄 Auto-redirect**: Kicked clients properly return to home screen ## Architecture - **🏗️ Clean API Design**: All admin endpoints use official Colyseus patterns - **🔒 Type Safety**: Maintained TypeScript sync between server and clients - **📦 Microservices Ready**: Separated concerns between game server and admin interface **Breaking Changes:** None - fully backward compatible **Migration:** No migration needed
This commit is contained in:
@@ -268,4 +268,148 @@ export class GameRoom extends Room<GameState> {
|
||||
onDispose() {
|
||||
console.log(`GameRoom ${this.roomId} disposed`);
|
||||
}
|
||||
|
||||
// Method for admin monitoring - used by Colyseus monitor and admin API
|
||||
getInspectData() {
|
||||
const stateSize = JSON.stringify(this.state).length;
|
||||
const roomElapsedTime = this.clock.elapsedTime;
|
||||
|
||||
// Gather client information
|
||||
const clients = this.clients.map((client) => ({
|
||||
sessionId: client.sessionId,
|
||||
elapsedTime: roomElapsedTime - (client as any)._joinedAt || 0
|
||||
}));
|
||||
|
||||
// Return comprehensive room data
|
||||
return {
|
||||
roomId: this.roomId,
|
||||
name: 'game',
|
||||
clients: clients.length,
|
||||
maxClients: this.maxClients,
|
||||
locked: this.locked,
|
||||
state: this.state,
|
||||
stateSize,
|
||||
clients: clients,
|
||||
elapsedTime: roomElapsedTime,
|
||||
metadata: {
|
||||
gamePhase: this.state.gamePhase,
|
||||
gameStarted: this.state.gameStarted,
|
||||
round: this.state.round,
|
||||
playerCount: this.state.players.size,
|
||||
activeOffers: this.state.activeTradeOffers.length
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Admin methods for game control
|
||||
pauseGame() {
|
||||
if (this.state.gameStarted && this.state.gamePhase !== 'paused') {
|
||||
this.state.gamePhase = 'paused';
|
||||
console.log(`⏸️ Game paused in room ${this.roomId} by admin`);
|
||||
|
||||
// Broadcast pause message to all clients
|
||||
this.broadcast("gamePaused", {
|
||||
message: "El juego ha sido pausado por el administrador",
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
resumeGame() {
|
||||
if (this.state.gameStarted && this.state.gamePhase === 'paused') {
|
||||
this.state.gamePhase = 'trading'; // Resume to trading phase
|
||||
console.log(`▶️ Game resumed in room ${this.roomId} by admin`);
|
||||
|
||||
// Broadcast resume message to all clients
|
||||
this.broadcast("gameResumed", {
|
||||
message: "El juego ha sido reanudado por el administrador",
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_forceClientDisconnect(sessionId: string) {
|
||||
const client = this.clients.find(c => c.sessionId === sessionId);
|
||||
if (client) {
|
||||
console.log(`🚫 Admin force disconnect player ${sessionId} from room ${this.roomId}`);
|
||||
|
||||
// Send notification to the specific client before disconnecting
|
||||
client.send("adminKicked", {
|
||||
message: "Has sido expulsado del juego por el administrador",
|
||||
reason: "admin_kick",
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
// Give client time to process the message, then disconnect
|
||||
setTimeout(() => {
|
||||
client.leave(4000); // Force disconnect with code 4000
|
||||
}, 1000);
|
||||
} else {
|
||||
throw new Error(`Player ${sessionId} not found in room ${this.roomId}`);
|
||||
}
|
||||
}
|
||||
|
||||
_forceDisconnectAllClients() {
|
||||
console.log(`🚫🚫 Admin force disconnect ALL players from room ${this.roomId}`);
|
||||
|
||||
if (this.clients.length === 0) {
|
||||
return { success: true, kickedPlayers: 0 };
|
||||
}
|
||||
|
||||
// Send notification to all clients first
|
||||
this.broadcast("adminKicked", {
|
||||
message: "Todos los jugadores han sido expulsados por el administrador",
|
||||
reason: "admin_kick_all",
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
const kickedCount = this.clients.length;
|
||||
|
||||
// Give clients time to process the message, then disconnect all
|
||||
setTimeout(() => {
|
||||
// Create a copy of clients array since it will be modified during iteration
|
||||
const clientsToDisconnect = [...this.clients];
|
||||
clientsToDisconnect.forEach(client => {
|
||||
client.leave(4000); // Force disconnect with code 4000
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
return { success: true, kickedPlayers: kickedCount };
|
||||
}
|
||||
|
||||
advanceRound() {
|
||||
const oldRound = this.state.round;
|
||||
this.state.round = Math.min(oldRound + 1, 10); // Max 10 rounds
|
||||
const newRound = this.state.round;
|
||||
|
||||
console.log(`⏭️ Round advanced from ${oldRound} to ${newRound} in room ${this.roomId}`);
|
||||
|
||||
// Broadcast round change to all clients
|
||||
this.broadcast("roundChanged", {
|
||||
oldRound,
|
||||
newRound,
|
||||
message: `Ronda ${newRound} - Cambio realizado por el administrador`,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
return { success: true, newRound, oldRound };
|
||||
}
|
||||
|
||||
previousRound() {
|
||||
const oldRound = this.state.round;
|
||||
this.state.round = Math.max(oldRound - 1, 1); // Min round 1
|
||||
const newRound = this.state.round;
|
||||
|
||||
console.log(`⏮️ Round went back from ${oldRound} to ${newRound} in room ${this.roomId}`);
|
||||
|
||||
// Broadcast round change to all clients
|
||||
this.broadcast("roundChanged", {
|
||||
oldRound,
|
||||
newRound,
|
||||
message: `Ronda ${newRound} - Cambio realizado por el administrador`,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
return { success: true, newRound, oldRound };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user