diff --git a/deep-sea-stories/packages/backend/src/controllers/peers.ts b/deep-sea-stories/packages/backend/src/controllers/peers.ts index e9a8c6a..94a0575 100644 --- a/deep-sea-stories/packages/backend/src/controllers/peers.ts +++ b/deep-sea-stories/packages/backend/src/controllers/peers.ts @@ -1,4 +1,6 @@ import type { RoomId } from '@fishjam-cloud/js-server-sdk'; +import { TRPCError } from '@trpc/server'; +import { GameRoomFullError } from '../domain/errors.js'; import { GameRoom } from '../game/room.js'; import { createPeerInputSchema } from '../schemas.js'; import { roomService } from '../service/room.js'; @@ -18,10 +20,19 @@ export const createPeer = publicProcedure roomService.setGameRoom(room.id, gameRoom); } - const { peer, peerToken } = await gameRoom.addPlayer(input.name); - - return { - peer, - token: peerToken, - }; + try { + const { peer, peerToken } = await gameRoom.addPlayer(input.name); + return { + peer, + token: peerToken, + }; + } catch (error) { + if (error instanceof GameRoomFullError) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: error.message, + }); + } + throw error; + } }); diff --git a/deep-sea-stories/packages/backend/src/controllers/stories.ts b/deep-sea-stories/packages/backend/src/controllers/stories.ts index a5436aa..a50ff87 100644 --- a/deep-sea-stories/packages/backend/src/controllers/stories.ts +++ b/deep-sea-stories/packages/backend/src/controllers/stories.ts @@ -1,6 +1,5 @@ import type { RoomId } from '@fishjam-cloud/js-server-sdk'; import { stories } from '../config.js'; -import { FailedToStartStoryError } from '../domain/errors.js'; import { selectStoryInputSchema, startStoryInputSchema, @@ -57,7 +56,7 @@ export const startStory = publicProcedure }; } catch (error) { console.error(`Failed to start story: %o`, error); - throw new FailedToStartStoryError(0, (error as Error).message); + throw new Error((error as Error).message); } }); diff --git a/deep-sea-stories/packages/backend/src/domain/errors.ts b/deep-sea-stories/packages/backend/src/domain/errors.ts index bfcebac..640bb6b 100644 --- a/deep-sea-stories/packages/backend/src/domain/errors.ts +++ b/deep-sea-stories/packages/backend/src/domain/errors.ts @@ -8,61 +8,13 @@ export abstract class DomainError extends Error { this.statusCode = statusCode; } } - -export class GameSessionNotFoundError extends DomainError { - constructor(roomId: string) { - super( - 'GAME_SESSION_NOT_FOUND', - `No game session found for room ${roomId}`, - 404, - ); - this.name = 'GameSessionNotFoundError'; - } -} - -export class StoryNotFoundError extends DomainError { - constructor(roomId: string) { - super('STORY_NOT_FOUND', `No story available for room ${roomId}`, 400); - this.name = 'StoryNotFoundError'; - } -} - -export class NoPeersConnectedError extends DomainError { - constructor(roomId: string) { - super('NO_PEERS_CONNECTED', `No connected peers in room ${roomId}`, 400); - this.name = 'NoPeersConnectedError'; - } -} - -export class NoVoiceSessionManagerError extends DomainError { - constructor(roomId: string) { - super( - 'NO_VOICE_SESSION_MANAGER', - `No voice session manager configured for room ${roomId}`, - 500, - ); - this.name = 'NoVoiceSessionManagerError'; - } -} - -export class AudioConnectionError extends DomainError { - constructor(peerId: string, reason: string) { - super( - 'AUDIO_CONNECTION_ERROR', - `Failed to establish audio connection for peer ${peerId}: ${reason}`, - 500, - ); - this.name = 'AudioConnectionError'; - } -} - -export class FailedToStartStoryError extends DomainError { - constructor(storyId: number, reason: string) { +export class GameRoomFullError extends DomainError { + constructor() { super( - 'FAILED_TO_START_STORY', - `Failed to start story ${storyId}: ${reason}`, - 500, + 'GAME_ROOM_FULL', + 'Room is full. Please wait for a spot or create a new room.', + 400, ); - this.name = 'FailedToStartStoryError'; + this.name = 'GameRoomFullError'; } } diff --git a/deep-sea-stories/packages/backend/src/game/room.ts b/deep-sea-stories/packages/backend/src/game/room.ts index 46fd19e..f69396e 100644 --- a/deep-sea-stories/packages/backend/src/game/room.ts +++ b/deep-sea-stories/packages/backend/src/game/room.ts @@ -1,4 +1,7 @@ -import { GAME_TIME_LIMIT_SECONDS } from '@deep-sea-stories/common'; +import { + GAME_TIME_LIMIT_SECONDS, + ROOM_PLAYERS_LIMIT, +} from '@deep-sea-stories/common'; import { type FishjamClient, type Peer, @@ -13,6 +16,7 @@ import { AudioStreamingOrchestrator } from '../service/audio-streaming-orchestra import type { NotifierService } from '../service/notifier.js'; import type { Story } from '../types.js'; import { GameSession } from './session.js'; +import { GameRoomFullError } from '../domain/errors.js'; type Player = { name: string; @@ -61,6 +65,9 @@ export class GameRoom { } async addPlayer(name: string): Promise<{ peer: Peer; peerToken: string }> { + if (this.players.size >= ROOM_PLAYERS_LIMIT) { + throw new GameRoomFullError(); + } const { peer, peerToken } = await this.fishjamClient.createPeer( this.roomId, ); diff --git a/deep-sea-stories/packages/common/src/constants.ts b/deep-sea-stories/packages/common/src/constants.ts index 4942cc9..fbfb6bc 100644 --- a/deep-sea-stories/packages/common/src/constants.ts +++ b/deep-sea-stories/packages/common/src/constants.ts @@ -1,3 +1,5 @@ export const GAME_TIME_LIMIT_MINUTES = 30; export const GAME_TIME_LIMIT_SECONDS = GAME_TIME_LIMIT_MINUTES * 60; + +export const ROOM_PLAYERS_LIMIT = 4; diff --git a/deep-sea-stories/packages/common/src/index.ts b/deep-sea-stories/packages/common/src/index.ts index a97bd81..8d4e2aa 100644 --- a/deep-sea-stories/packages/common/src/index.ts +++ b/deep-sea-stories/packages/common/src/index.ts @@ -1,6 +1,7 @@ export { GAME_TIME_LIMIT_MINUTES, GAME_TIME_LIMIT_SECONDS, + ROOM_PLAYERS_LIMIT, } from './constants.js'; export type { AgentEvent } from './events.js'; export type { StoryData } from './types.js'; diff --git a/deep-sea-stories/packages/web/src/components/PlayerCountIndicator.tsx b/deep-sea-stories/packages/web/src/components/PlayerCountIndicator.tsx new file mode 100644 index 0000000..3430881 --- /dev/null +++ b/deep-sea-stories/packages/web/src/components/PlayerCountIndicator.tsx @@ -0,0 +1,18 @@ +import { ROOM_PLAYERS_LIMIT } from '@deep-sea-stories/common'; +import { Users } from 'lucide-react'; +import type { FC } from 'react'; + +type PlayerCountIndicatorProps = { + count: number; +}; + +export const PlayerCountIndicator: FC = ({ + count, +}) => { + return ( +
+ + {count}/{ROOM_PLAYERS_LIMIT} +
+ ); +}; diff --git a/deep-sea-stories/packages/web/src/views/GameView.tsx b/deep-sea-stories/packages/web/src/views/GameView.tsx index c56c7fd..fb78786 100644 --- a/deep-sea-stories/packages/web/src/views/GameView.tsx +++ b/deep-sea-stories/packages/web/src/views/GameView.tsx @@ -3,6 +3,7 @@ import type { FC } from 'react'; import { useEffect, useMemo, useRef } from 'react'; import GameControlPanel from '@/components/GameControlPanel'; import PeerGrid from '@/components/PeerGrid'; +import { PlayerCountIndicator } from '@/components/PlayerCountIndicator'; export type GameViewProps = { roomId: string; @@ -29,6 +30,8 @@ const GameView: FC = ({ roomId }) => { }, [agentPeer?.tracks[0]?.stream]); const userName = localPeer?.metadata?.peer?.name ?? 'Unknown'; + const playerCount = (localPeer ? 1 : 0) + displayedPeers.length; + return (
= ({ roomId }) => { agentStream={agentPeer?.tracks[0]?.stream} /> - +
+
+ +
+ +
{/* biome-ignore lint/a11y/useMediaCaption: Peer audio feed from WebRTC doesn't have captions */}