diff --git a/backend/utils/ecast-shard-client.js b/backend/utils/ecast-shard-client.js index 6709ae5..8476712 100644 --- a/backend/utils/ecast-shard-client.js +++ b/backend/utils/ecast-shard-client.js @@ -36,7 +36,7 @@ class EcastShardClient { lobbyState: roomVal.lobbyState ?? null, gameCanStart: !!roomVal.gameCanStart, gameIsStarting: !!roomVal.gameIsStarting, - gameStarted: roomVal.state === 'Gameplay', + gameStarted: roomVal.state != null && roomVal.state !== 'Lobby', gameFinished: !!roomVal.gameFinished, }; } @@ -213,6 +213,12 @@ class EcastShardClient { break; case 'client/disconnected': break; + case 'room/lock': + this.handleRoomLock(); + break; + case 'room/exit': + this.handleRoomExit(message.result); + break; case 'error': this.handleError(message.result); break; @@ -363,6 +369,44 @@ class EcastShardClient { } } + handleRoomLock() { + if (!this.gameStarted) { + console.log(`[Shard Monitor] Room ${this.roomCode} locked (game starting)`); + this.gameStarted = true; + this.gameState = this.gameState || 'Gameplay'; + this.onEvent('game.started', { + sessionId: this.sessionId, + gameId: this.gameId, + roomCode: this.roomCode, + playerCount: this.playerCount, + players: [...this.playerNames], + maxPlayers: this.maxPlayers, + }); + } + } + + handleRoomExit() { + if (this.gameFinished) return; + console.log(`[Shard Monitor] Room ${this.roomCode} exited`); + this.gameFinished = true; + this.onEvent('game.ended', { + sessionId: this.sessionId, + gameId: this.gameId, + roomCode: this.roomCode, + playerCount: this.playerCount, + players: [...this.playerNames], + }); + this.onEvent('room.disconnected', { + sessionId: this.sessionId, + gameId: this.gameId, + roomCode: this.roomCode, + reason: 'room_closed', + finalPlayerCount: this.playerCount, + }); + activeShards.delete(`${this.sessionId}-${this.gameId}`); + this.disconnect(); + } + handleError(result) { console.error(`[Shard Monitor] Ecast error ${result?.code}: ${result?.msg}`); if (result?.code === 2027) { diff --git a/frontend/src/config/branding.js b/frontend/src/config/branding.js index c5f6d46..9bd7a7e 100644 --- a/frontend/src/config/branding.js +++ b/frontend/src/config/branding.js @@ -2,7 +2,7 @@ export const branding = { app: { name: 'HSO Jackbox Game Picker', shortName: 'Jackbox Game Picker', - version: '0.6.5 - Fish Tank Edition', + version: '0.7.0 - Fixed For Real Edition', description: 'Spicing up Hyper Spaceout game nights!', }, meta: { diff --git a/tests/api/ecast-shard-client.test.js b/tests/api/ecast-shard-client.test.js index 115ee15..cda4b3d 100644 --- a/tests/api/ecast-shard-client.test.js +++ b/tests/api/ecast-shard-client.test.js @@ -50,6 +50,15 @@ describe('EcastShardClient', () => { expect(result.gameStarted).toBe(true); }); + test('detects game started from non-Lobby state (Pack 7 Logo)', () => { + const roomVal = { state: 'Logo', locale: 'en', platformId: 'PS4' }; + const result = EcastShardClient.parseRoomEntity(roomVal); + expect(result.gameStarted).toBe(true); + expect(result.gameState).toBe('Logo'); + expect(result.lobbyState).toBeNull(); + expect(result.gameFinished).toBe(false); + }); + test('detects game finished', () => { const roomVal = { state: 'Gameplay', lobbyState: '', gameCanStart: true, gameIsStarting: false, gameFinished: true }; const result = EcastShardClient.parseRoomEntity(roomVal); @@ -272,6 +281,26 @@ describe('EcastShardClient', () => { expect(startEvents[0].data.players).toEqual(['A', 'B', 'C', 'D']); }); + test('broadcasts game.started on state transition to Logo (Pack 7)', () => { + client.lobbyState = 'Countdown'; + client.gameState = 'Lobby'; + client.gameStarted = false; + client.playerCount = 4; + client.playerNames = ['A', 'B', 'C', 'D']; + + client.handleEntityUpdate({ + key: 'room', + val: { state: 'Logo', locale: 'en', platformId: 'PS4' }, + version: 14, from: 1, + }); + + const startEvents = events.filter(e => e.type === 'game.started'); + expect(startEvents).toHaveLength(1); + expect(startEvents[0].data.playerCount).toBe(4); + expect(client.gameState).toBe('Logo'); + expect(client.gameStarted).toBe(true); + }); + test('does not broadcast game.started if already started', () => { client.gameStarted = true; client.gameState = 'Gameplay'; @@ -573,4 +602,80 @@ describe('EcastShardClient', () => { expect(events.some(e => e.type === 'room.disconnected' && e.data.reason === 'room_closed')).toBe(true); }); }); + + describe('handleRoomLock', () => { + test('emits game.started when game has not yet started', () => { + const events = []; + const client = new EcastShardClient({ + sessionId: 1, + gameId: 5, + roomCode: 'TEST', + maxPlayers: 8, + onEvent: (type, data) => events.push({ type, data }), + }); + client.gameStarted = false; + client.playerCount = 4; + client.playerNames = ['A', 'B', 'C', 'D']; + + client.handleRoomLock(); + + expect(client.gameStarted).toBe(true); + const startEvents = events.filter(e => e.type === 'game.started'); + expect(startEvents).toHaveLength(1); + expect(startEvents[0].data.playerCount).toBe(4); + }); + + test('does not emit game.started if game already started', () => { + const events = []; + const client = new EcastShardClient({ + sessionId: 1, + gameId: 5, + roomCode: 'TEST', + maxPlayers: 8, + onEvent: (type, data) => events.push({ type, data }), + }); + client.gameStarted = true; + + client.handleRoomLock(); + + expect(events.filter(e => e.type === 'game.started')).toHaveLength(0); + }); + }); + + describe('handleRoomExit', () => { + test('emits game.ended and room.disconnected on room exit', () => { + const events = []; + const client = new EcastShardClient({ + sessionId: 1, + gameId: 5, + roomCode: 'TEST', + maxPlayers: 8, + onEvent: (type, data) => events.push({ type, data }), + }); + client.playerCount = 3; + client.playerNames = ['X', 'Y', 'Z']; + + client.handleRoomExit(); + + expect(client.gameFinished).toBe(true); + expect(events.some(e => e.type === 'game.ended')).toBe(true); + expect(events.some(e => e.type === 'room.disconnected' && e.data.reason === 'room_closed')).toBe(true); + }); + + test('does not emit events if game already finished', () => { + const events = []; + const client = new EcastShardClient({ + sessionId: 1, + gameId: 5, + roomCode: 'TEST', + maxPlayers: 8, + onEvent: (type, data) => events.push({ type, data }), + }); + client.gameFinished = true; + + client.handleRoomExit(); + + expect(events).toHaveLength(0); + }); + }); });