diff --git a/apps/web/src/arcade-games/rithmomachia/Provider.tsx b/apps/web/src/arcade-games/rithmomachia/Provider.tsx index 92f08350..c04c91d1 100644 --- a/apps/web/src/arcade-games/rithmomachia/Provider.tsx +++ b/apps/web/src/arcade-games/rithmomachia/Provider.tsx @@ -132,7 +132,11 @@ export function RithmomachiaProvider({ children }: { children: ReactNode }) { } if (localCount === 0) { - return { status: 'noLocalControl', activePlayerCount: activeCount, localPlayerCount: localCount } + return { + status: 'noLocalControl', + activePlayerCount: activeCount, + localPlayerCount: localCount, + } } return { status: 'ok', activePlayerCount: activeCount, localPlayerCount: localCount } diff --git a/apps/web/src/arcade-games/rithmomachia/components/RithmomachiaGame.tsx b/apps/web/src/arcade-games/rithmomachia/components/RithmomachiaGame.tsx index 01c40e22..10d797ef 100644 --- a/apps/web/src/arcade-games/rithmomachia/components/RithmomachiaGame.tsx +++ b/apps/web/src/arcade-games/rithmomachia/components/RithmomachiaGame.tsx @@ -9,6 +9,8 @@ import { StandardGameLayout } from '@/components/StandardGameLayout' import { Z_INDEX } from '@/constants/zIndex' import { useGameMode } from '@/contexts/GameModeContext' import { useFullscreen } from '@/contexts/FullscreenContext' +import { useRoomData, useKickUser } from '@/hooks/useRoomData' +import { useViewerId } from '@/hooks/useViewerId' import { css } from '../../../../styled-system/css' import { useRithmomachia } from '../Provider' import type { Piece, RelationKind, RithmomachiaConfig } from '../types' @@ -198,6 +200,9 @@ export function RithmomachiaGame() { function RosterStatusNotice({ phase }: { phase: 'setup' | 'playing' }) { const { rosterStatus, whitePlayerId, blackPlayerId } = useRithmomachia() const { players: playerMap, activePlayers: activePlayerIds, addPlayer, setActive } = useGameMode() + const { roomData } = useRoomData() + const { data: viewerId } = useViewerId() + const { mutate: kickUser } = useKickUser() const playersArray = useMemo(() => { const list = Array.from(playerMap.values()) @@ -218,137 +223,229 @@ function RosterStatusNotice({ phase }: { phase: 'setup' | 'playing' }) { }) }, [playerMap]) - const inactiveLocalPlayer = useMemo( - () => - playersArray.find( - (player) => player.isLocal !== false && !activePlayerIds.has(player.id) - ) || null, - [playersArray, activePlayerIds] - ) + // Check if current user is room host + const isHost = useMemo(() => { + if (!roomData || !viewerId) return false + const currentMember = roomData.members.find((m) => m.userId === viewerId) + return currentMember?.isCreator === true + }, [roomData, viewerId]) - const removableLocalPlayer = useMemo( + // Find all removable local players (active but not assigned to white/black) + const removableLocalPlayers = useMemo( () => - playersArray.find( + playersArray.filter( (player) => player.isLocal !== false && activePlayerIds.has(player.id) && player.id !== whitePlayerId && player.id !== blackPlayerId - ) || null, + ), [playersArray, activePlayerIds, whitePlayerId, blackPlayerId] ) - const quickFix = useMemo(() => { - if (rosterStatus.status === 'tooFew') { - if (inactiveLocalPlayer) { - return { - label: `Activate ${inactiveLocalPlayer.name}`, - action: () => setActive(inactiveLocalPlayer.id, true), - } - } + // Find all kickable remote players (active remote players, if we're host) + const kickablePlayers = useMemo(() => { + if (!isHost || !roomData) return [] - return { - label: 'Create local player', - action: () => addPlayer({ isActive: true }), - } - } + return playersArray.filter( + (player) => + player.isLocal === false && // Remote player + activePlayerIds.has(player.id) && // Active + player.id !== whitePlayerId && // Not assigned to white + player.id !== blackPlayerId // Not assigned to black + ) + }, [isHost, roomData, playersArray, activePlayerIds, whitePlayerId, blackPlayerId]) - if (rosterStatus.status === 'noLocalControl') { - if (inactiveLocalPlayer) { - return { - label: `Activate ${inactiveLocalPlayer.name}`, - action: () => setActive(inactiveLocalPlayer.id, true), - } - } - - return null - } - - if (rosterStatus.status === 'tooMany' && removableLocalPlayer) { - return { - label: `Deactivate ${removableLocalPlayer.name}`, - action: () => setActive(removableLocalPlayer.id, false), - } - } - - return null - }, [rosterStatus.status, inactiveLocalPlayer, removableLocalPlayer, addPlayer, setActive]) + const inactiveLocalPlayer = useMemo( + () => + playersArray.find((player) => player.isLocal !== false && !activePlayerIds.has(player.id)) || + null, + [playersArray, activePlayerIds] + ) const heading = useMemo(() => { - switch (rosterStatus.status) { - case 'tooFew': - return 'Need two active players' - case 'tooMany': - return 'Too many active players' - case 'noLocalControl': - return 'Join the roster from this device' - default: - return '' - } + if (rosterStatus.status === 'tooFew') return 'Need two active players' + if (rosterStatus.status === 'tooMany') return 'Too many active players' + return '' }, [rosterStatus.status]) const description = useMemo(() => { - switch (rosterStatus.status) { - case 'tooFew': - return phase === 'setup' - ? 'Rithmomachia needs exactly two active players before the match can begin. Use the roster controls in the game nav to activate or add another player.' - : 'Gameplay is paused until two players are active. Use the roster controls in the game nav to activate or add another player and resume the match.' - case 'tooMany': - return 'Rithmomachia supports only two active players. Use the game nav roster to deactivate extras so each color has exactly one seat.' - case 'noLocalControl': - return phase === 'setup' - ? 'All active seats belong to other devices. Activate a local player from the game nav if you want to start from this computer.' - : 'All active seats belong to other devices. Activate a local player in the game nav if you want to make moves from this computer.' - default: - return '' + if (rosterStatus.status === 'tooFew') { + return phase === 'setup' + ? 'Rithmomachia needs exactly two active players before the match can begin.' + : 'Gameplay is paused until two players are active.' } + if (rosterStatus.status === 'tooMany') { + return 'Rithmomachia supports only two active players. Deactivate or kick extras below:' + } + return '' }, [phase, rosterStatus.status]) - if (rosterStatus.status === 'ok') { + // Don't show notice for 'ok' or 'noLocalControl' (observers are allowed) + if (rosterStatus.status === 'ok' || rosterStatus.status === 'noLocalControl') { return null } + const handleKick = (player: any) => { + if (!roomData) return + + // Find the user ID for this player + for (const [userId, players] of Object.entries(roomData.memberPlayers)) { + if (players.some((p) => p.id === player.id)) { + kickUser({ roomId: roomData.id, userId }) + break + } + } + } + return (
{description}
+ Your players: +
++ Remote players (host can kick): +
+