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:
@@ -28,6 +28,26 @@ const onJoinGame = (client: any) => {
|
||||
gameClient.value = client
|
||||
currentScreen.value = 'game'
|
||||
logger.info('Transitioning to game screen')
|
||||
|
||||
// Handle admin kick notification
|
||||
client.onAdminKicked((data: any) => {
|
||||
// Show alert message
|
||||
alert(`🚫 ${data.message}`)
|
||||
|
||||
// Return to home screen
|
||||
currentScreen.value = 'home'
|
||||
gameClient.value = null
|
||||
|
||||
logger.info('Player kicked by admin, returned to home screen')
|
||||
})
|
||||
|
||||
// Handle round change notification
|
||||
client.onRoundChanged((data: any) => {
|
||||
// Show alert message
|
||||
alert(`🎯 ${data.message}`)
|
||||
|
||||
logger.info('Round changed by admin:', data)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ export class GameClient {
|
||||
// Event callbacks
|
||||
private onStateChangeCallbacks: ((state: GameState) => void)[] = []
|
||||
private onGamePhaseChangeCallbacks: ((phase: string) => void)[] = []
|
||||
private onAdminKickedCallbacks: ((data: any) => void)[] = []
|
||||
private onRoundChangedCallbacks: ((data: any) => void)[] = []
|
||||
|
||||
constructor() {
|
||||
const serverUrl = import.meta.env.VITE_SERVER_URL || 'ws://localhost:2567'
|
||||
@@ -61,12 +63,38 @@ export class GameClient {
|
||||
this.room.onLeave((code) => {
|
||||
logger.info('Left room with code:', code)
|
||||
this.isConnected = false
|
||||
|
||||
// Handle forced disconnect by admin
|
||||
if (code === 4000) {
|
||||
logger.info('Disconnected by admin (code 4000)')
|
||||
}
|
||||
})
|
||||
|
||||
this.room.onError((code, message) => {
|
||||
logger.error('Room error:', { code, message })
|
||||
})
|
||||
|
||||
// Handle admin kick message
|
||||
this.room.onMessage("adminKicked", (data) => {
|
||||
logger.info('Received admin kick message:', data)
|
||||
this.onAdminKickedCallbacks.forEach(callback => callback(data))
|
||||
})
|
||||
|
||||
// Handle game pause/resume messages
|
||||
this.room.onMessage("gamePaused", (data) => {
|
||||
logger.info('Game paused by admin:', data)
|
||||
})
|
||||
|
||||
this.room.onMessage("gameResumed", (data) => {
|
||||
logger.info('Game resumed by admin:', data)
|
||||
})
|
||||
|
||||
// Handle round change messages
|
||||
this.room.onMessage("roundChanged", (data) => {
|
||||
logger.info('Round changed by admin:', data)
|
||||
this.onRoundChangedCallbacks.forEach(callback => callback(data))
|
||||
})
|
||||
|
||||
return this.room
|
||||
} catch (error) {
|
||||
logger.error('Failed to join room:', error)
|
||||
@@ -122,6 +150,30 @@ export class GameClient {
|
||||
}
|
||||
}
|
||||
|
||||
onAdminKicked(callback: (data: any) => void): () => void {
|
||||
this.onAdminKickedCallbacks.push(callback)
|
||||
|
||||
// Return unsubscribe function
|
||||
return () => {
|
||||
const index = this.onAdminKickedCallbacks.indexOf(callback)
|
||||
if (index > -1) {
|
||||
this.onAdminKickedCallbacks.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onRoundChanged(callback: (data: any) => void): () => void {
|
||||
this.onRoundChangedCallbacks.push(callback)
|
||||
|
||||
// Return unsubscribe function
|
||||
return () => {
|
||||
const index = this.onRoundChangedCallbacks.indexOf(callback)
|
||||
if (index > -1) {
|
||||
this.onRoundChangedCallbacks.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Game actions
|
||||
sendClick(): void {
|
||||
if (this.room && this.gameState?.gamePhase === 'playing') {
|
||||
|
||||
Reference in New Issue
Block a user