feat: add arcade room/session info and network players to nav

Add visual indicators for arcade sessions and other players:

Components added:
- NetworkPlayerIndicator: Shows network players with special
  "network" frame border (animated pulse, network icon badge)
- RoomInfo: Displays current arcade session info (game name,
  player count)

Modified:
- GameContextNav: Accept and render networkPlayers and roomInfo
- PageWithNav: Fetch arcade session info via useArcadeGuard and
  pass to GameContextNav

Visual features:
- Network players have gradient border and pulsing connection indicator
- Room info shows game name and player count in styled container
- Network players are visually distinct from local players
- Only shown when in active arcade session

This provides visibility into multiplayer state and prepares
for full room system implementation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock
2025-10-08 06:51:38 -05:00
parent 99906ae53d
commit 6800747f80
4 changed files with 273 additions and 0 deletions

View File

@@ -2,6 +2,7 @@
import React from 'react'
import { useGameMode } from '../contexts/GameModeContext'
import { useArcadeGuard } from '../hooks/useArcadeGuard'
import { AppNavBar } from './AppNavBar'
import { GameContextNav } from './nav/GameContextNav'
import { PlayerConfigDialog } from './nav/PlayerConfigDialog'
@@ -28,6 +29,7 @@ export function PageWithNav({
children,
}: PageWithNavProps) {
const { players, activePlayers, setActive, activePlayerCount } = useGameMode()
const { hasActiveSession, activeSession } = useArcadeGuard({ enabled: false }) // Don't redirect, just get info
const [mounted, setMounted] = React.useState(false)
const [configurePlayerId, setConfigurePlayerId] = React.useState<string | null>(null)
@@ -76,6 +78,19 @@ export function PageWithNav({
const shouldEmphasize = emphasizeGameContext && mounted
const showFullscreenSelection = shouldEmphasize && activePlayerCount === 0
// Compute arcade session info for display
const roomInfo = hasActiveSession && activeSession
? {
gameName: activeSession.currentGame,
playerCount: activePlayerCount, // TODO: Get actual player count from session when available
}
: undefined
// Compute network players (other players in the arcade session)
// For now, we don't have this info in activeSession, so return empty array
// TODO: When arcade room system is implemented, fetch other players from session
const networkPlayers: Array<{ id: string; emoji?: string; name?: string }> = []
// Create nav content if title is provided
const navContent = navTitle ? (
<GameContextNav
@@ -93,6 +108,8 @@ export function PageWithNav({
onSetup={onSetup}
onNewGame={onNewGame}
canModifyPlayers={canModifyPlayers}
roomInfo={roomInfo}
networkPlayers={networkPlayers}
/>
) : null

View File

@@ -4,6 +4,8 @@ import { AddPlayerButton } from './AddPlayerButton'
import { FullscreenPlayerSelection } from './FullscreenPlayerSelection'
import { GameControlButtons } from './GameControlButtons'
import { GameModeIndicator } from './GameModeIndicator'
import { NetworkPlayerIndicator } from './NetworkPlayerIndicator'
import { RoomInfo } from './RoomInfo'
type GameMode = 'none' | 'single' | 'battle' | 'tournament'
@@ -13,6 +15,17 @@ interface Player {
emoji: string
}
interface NetworkPlayer {
id: string
emoji?: string
name?: string
}
interface ArcadeRoomInfo {
gameName: string
playerCount: number
}
interface GameContextNavProps {
navTitle: string
navEmoji?: string
@@ -28,6 +41,9 @@ interface GameContextNavProps {
onSetup?: () => void
onNewGame?: () => void
canModifyPlayers?: boolean
// Arcade session info
networkPlayers?: NetworkPlayer[]
roomInfo?: ArcadeRoomInfo
}
export function GameContextNav({
@@ -45,6 +61,8 @@ export function GameContextNav({
onSetup,
onNewGame,
canModifyPlayers = true,
networkPlayers = [],
roomInfo,
}: GameContextNavProps) {
const [_isTransitioning, setIsTransitioning] = React.useState(false)
const [layoutMode, setLayoutMode] = React.useState<'column' | 'row'>(
@@ -113,6 +131,34 @@ export function GameContextNav({
showFullscreenSelection={showFullscreenSelection}
/>
{/* Room Info - show when in arcade session */}
{roomInfo && !showFullscreenSelection && (
<RoomInfo
gameName={roomInfo.gameName}
playerCount={roomInfo.playerCount}
shouldEmphasize={shouldEmphasize}
/>
)}
{/* Network Players - show other players in the room */}
{networkPlayers.length > 0 && !showFullscreenSelection && (
<div
style={{
display: 'flex',
alignItems: 'center',
gap: shouldEmphasize ? '12px' : '6px',
}}
>
{networkPlayers.map((player) => (
<NetworkPlayerIndicator
key={player.id}
player={player}
shouldEmphasize={shouldEmphasize}
/>
))}
</div>
)}
{/* Game Control Buttons - only show during active game */}
{!showFullscreenSelection && !canModifyPlayers && (
<GameControlButtons onSetup={onSetup} onNewGame={onNewGame} onQuit={onExitSession} />

View File

@@ -0,0 +1,121 @@
import React from 'react'
interface NetworkPlayer {
id: string
emoji?: string
name?: string
}
interface NetworkPlayerIndicatorProps {
player: NetworkPlayer
shouldEmphasize: boolean
}
/**
* Displays a network player with a special "network" frame border
* to distinguish them from local players
*/
export function NetworkPlayerIndicator({ player, shouldEmphasize }: NetworkPlayerIndicatorProps) {
const [isHovered, setIsHovered] = React.useState(false)
return (
<div
style={{
position: 'relative',
fontSize: shouldEmphasize ? '48px' : '20px',
lineHeight: 1,
transition: 'all 0.4s cubic-bezier(0.4, 0, 0.2, 1)',
cursor: 'default',
}}
title={player.name || `Network Player ${player.id.slice(0, 8)}`}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
{/* Network frame border */}
<div
style={{
position: 'absolute',
inset: '-6px',
borderRadius: '8px',
background: `
linear-gradient(135deg,
rgba(59, 130, 246, 0.4),
rgba(147, 51, 234, 0.4),
rgba(236, 72, 153, 0.4))
`,
opacity: isHovered ? 1 : 0.7,
transition: 'opacity 0.2s ease',
zIndex: -1,
}}
/>
{/* Animated network signal indicator */}
<div
style={{
position: 'absolute',
top: '-8px',
right: '-8px',
width: '12px',
height: '12px',
borderRadius: '50%',
background: 'rgba(34, 197, 94, 0.9)',
boxShadow: '0 0 8px rgba(34, 197, 94, 0.6)',
animation: 'networkPulse 2s ease-in-out infinite',
zIndex: 1,
}}
title="Connected"
/>
{/* Player emoji or fallback */}
<div
style={{
position: 'relative',
filter: shouldEmphasize ? 'drop-shadow(0 4px 8px rgba(0,0,0,0.25))' : 'none',
}}
>
{player.emoji || '🌐'}
</div>
{/* Network icon badge */}
<div
style={{
position: 'absolute',
bottom: '-4px',
left: '-4px',
width: '16px',
height: '16px',
borderRadius: '50%',
border: '2px solid white',
background: 'linear-gradient(135deg, #3b82f6, #8b5cf6)',
color: 'white',
fontSize: '8px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 2px 6px rgba(0,0,0,0.3)',
zIndex: 1,
}}
title="Network Player"
>
📡
</div>
<style
dangerouslySetInnerHTML={{
__html: `
@keyframes networkPulse {
0%, 100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.5;
transform: scale(1.2);
}
}
`,
}}
/>
</div>
)
}

View File

@@ -0,0 +1,89 @@
import React from 'react'
interface RoomInfoProps {
gameName: string
playerCount: number
shouldEmphasize: boolean
}
/**
* Displays current arcade room/session information
*/
export function RoomInfo({ gameName, playerCount, shouldEmphasize }: RoomInfoProps) {
return (
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
padding: shouldEmphasize ? '8px 16px' : '4px 12px',
background: 'linear-gradient(135deg, rgba(59, 130, 246, 0.2), rgba(147, 51, 234, 0.2))',
borderRadius: '12px',
border: '2px solid rgba(59, 130, 246, 0.4)',
fontSize: shouldEmphasize ? '16px' : '14px',
fontWeight: '600',
color: 'rgba(255, 255, 255, 0.95)',
transition: 'all 0.4s cubic-bezier(0.4, 0, 0.2, 1)',
boxShadow: '0 4px 12px rgba(59, 130, 246, 0.2)',
}}
title="Active Arcade Session"
>
{/* Room icon */}
<div
style={{
fontSize: shouldEmphasize ? '20px' : '16px',
display: 'flex',
alignItems: 'center',
}}
>
🎮
</div>
{/* Room details */}
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: '2px',
}}
>
<div
style={{
fontSize: shouldEmphasize ? '14px' : '12px',
opacity: 0.8,
textTransform: 'uppercase',
letterSpacing: '0.5px',
}}
>
Arcade Session
</div>
<div
style={{
fontSize: shouldEmphasize ? '16px' : '14px',
fontWeight: 'bold',
}}
>
{gameName}
</div>
</div>
{/* Player count badge */}
<div
style={{
marginLeft: '8px',
padding: '4px 8px',
background: 'rgba(255, 255, 255, 0.2)',
borderRadius: '8px',
fontSize: shouldEmphasize ? '14px' : '12px',
fontWeight: 'bold',
display: 'flex',
alignItems: 'center',
gap: '4px',
}}
>
<span>👥</span>
<span>{playerCount}</span>
</div>
</div>
)
}