feat: show rithmomachia turn in nav

This commit is contained in:
Thomas Hallock 2025-10-29 21:37:58 -05:00
parent 42a93e94a9
commit 7c89bfef9c
6 changed files with 151 additions and 17 deletions

View File

@ -5,6 +5,7 @@ import { animated, to, useSpring } from '@react-spring/web'
import { useRouter } from 'next/navigation'
import { useEffect, useMemo, useRef, useState } from 'react'
import { PageWithNav } from '@/components/PageWithNav'
import type { PlayerBadge } from '@/components/nav/types'
import { StandardGameLayout } from '@/components/StandardGameLayout'
import { Z_INDEX } from '@/constants/zIndex'
import { useGameMode } from '@/contexts/GameModeContext'
@ -136,7 +137,7 @@ function CaptureErrorDialog({
*/
export function RithmomachiaGame() {
const router = useRouter()
const { state, resetGame, goToSetup } = useRithmomachia()
const { state, resetGame, goToSetup, whitePlayerId, blackPlayerId } = useRithmomachia()
const { setFullscreenElement } = useFullscreen()
const gameRef = useRef<HTMLDivElement>(null)
@ -147,6 +148,41 @@ export function RithmomachiaGame() {
}
}, [setFullscreenElement])
const currentPlayerId = useMemo(() => {
if (state.turn === 'W') {
return whitePlayerId ?? undefined
}
if (state.turn === 'B') {
return blackPlayerId ?? undefined
}
return undefined
}, [state.turn, whitePlayerId, blackPlayerId])
const playerBadges = useMemo<Record<string, PlayerBadge>>(() => {
const badges: Record<string, PlayerBadge> = {}
if (whitePlayerId) {
badges[whitePlayerId] = {
label: 'White',
icon: '⚪',
background: 'linear-gradient(135deg, rgba(248, 250, 252, 0.95), rgba(226, 232, 240, 0.9))',
color: '#0f172a',
borderColor: 'rgba(226, 232, 240, 0.8)',
shadowColor: 'rgba(148, 163, 184, 0.35)',
}
}
if (blackPlayerId) {
badges[blackPlayerId] = {
label: 'Black',
icon: '⚫',
background: 'linear-gradient(135deg, rgba(30, 41, 59, 0.92), rgba(15, 23, 42, 0.94))',
color: '#f8fafc',
borderColor: 'rgba(30, 41, 59, 0.9)',
shadowColor: 'rgba(15, 23, 42, 0.45)',
}
}
return badges
}, [whitePlayerId, blackPlayerId])
return (
<PageWithNav
navTitle="Rithmomachia"
@ -157,6 +193,8 @@ export function RithmomachiaGame() {
}}
onNewGame={resetGame}
onSetup={goToSetup}
currentPlayerId={currentPlayerId}
playerBadges={playerBadges}
>
<StandardGameLayout>
<div

View File

@ -6,6 +6,7 @@ import { useRoomData } from '../hooks/useRoomData'
import { useViewerId } from '../hooks/useViewerId'
import { AppNavBar } from './AppNavBar'
import { GameContextNav } from './nav/GameContextNav'
import type { PlayerBadge } from './nav/types'
import { PlayerConfigDialog } from './nav/PlayerConfigDialog'
import { ModerationNotifications } from './nav/ModerationNotifications'
@ -22,6 +23,7 @@ interface PageWithNavProps {
currentPlayerId?: string
playerScores?: Record<string, number>
playerStreaks?: Record<string, number>
playerBadges?: Record<string, PlayerBadge>
}
export function PageWithNav({
@ -36,6 +38,7 @@ export function PageWithNav({
currentPlayerId,
playerScores,
playerStreaks,
playerBadges,
}: PageWithNavProps) {
const { players, activePlayers, setActive, activePlayerCount } = useGameMode()
const { roomData, isInRoom, moderationEvent, clearModerationEvent } = useRoomData()
@ -168,6 +171,7 @@ export function PageWithNav({
currentPlayerId={currentPlayerId}
playerScores={playerScores}
playerStreaks={playerStreaks}
playerBadges={playerBadges}
showPopover={showPopover}
setShowPopover={setShowPopover}
activeTab={activeTab}

View File

@ -1,5 +1,6 @@
import React from 'react'
import { PlayerTooltip } from './PlayerTooltip'
import type { PlayerBadge } from './types'
interface Player {
id: string
@ -19,6 +20,7 @@ interface ActivePlayersListProps {
currentPlayerId?: string
playerScores?: Record<string, number>
playerStreaks?: Record<string, number>
playerBadges?: Record<string, PlayerBadge>
}
export function ActivePlayersList({
@ -29,6 +31,7 @@ export function ActivePlayersList({
currentPlayerId,
playerScores = {},
playerStreaks = {},
playerBadges = {},
}: ActivePlayersListProps) {
const [hoveredPlayerId, setHoveredPlayerId] = React.useState<string | null>(null)
@ -48,6 +51,7 @@ export function ActivePlayersList({
const score = playerScores[player.id] || 0
const streak = playerStreaks[player.id] || 0
const celebrationLevel = getCelebrationLevel(streak)
const badge = playerBadges[player.id]
return (
<PlayerTooltip
@ -153,6 +157,41 @@ export function ActivePlayersList({
</div>
)}
{badge && (
<div
style={{
display: 'inline-flex',
alignItems: 'center',
gap: '6px',
padding: '4px 10px',
borderRadius: '999px',
background: badge.background ?? 'rgba(148, 163, 184, 0.25)',
color: badge.color ?? '#0f172a',
fontSize: '11px',
fontWeight: 700,
letterSpacing: '0.04em',
textTransform: 'uppercase',
boxShadow: badge.shadowColor
? `0 4px 12px ${badge.shadowColor}`
: '0 4px 12px rgba(15, 23, 42, 0.25)',
border: badge.borderColor ? `2px solid ${badge.borderColor}` : '2px solid rgba(255,255,255,0.4)',
backdropFilter: 'blur(4px)',
marginTop: '6px',
whiteSpace: 'nowrap',
}}
>
{badge.icon && (
<span
aria-hidden
style={{ fontSize: '14px', filter: 'drop-shadow(0 2px 4px rgba(15,23,42,0.35))' }}
>
{badge.icon}
</span>
)}
<span>{badge.label}</span>
</div>
)}
{shouldEmphasize && hoveredPlayerId === player.id && (
<>
{/* Configure button - bottom left */}

View File

@ -8,6 +8,7 @@ import { GameTitleMenu } from './GameTitleMenu'
import { NetworkPlayerIndicator } from './NetworkPlayerIndicator'
import { PendingInvitations } from './PendingInvitations'
import { RoomInfo } from './RoomInfo'
import type { PlayerBadge } from './types'
type GameMode = 'none' | 'single' | 'battle' | 'tournament'
@ -55,6 +56,7 @@ interface GameContextNavProps {
currentPlayerId?: string
playerScores?: Record<string, number>
playerStreaks?: Record<string, number>
playerBadges?: Record<string, PlayerBadge>
// Lifted popover state from PageWithNav
showPopover?: boolean
setShowPopover?: (show: boolean) => void
@ -82,6 +84,7 @@ export function GameContextNav({
currentPlayerId,
playerScores,
playerStreaks,
playerBadges,
showPopover,
setShowPopover,
activeTab,
@ -281,19 +284,20 @@ export function GameContextNav({
margin: '0 4px',
}}
/>
{networkPlayers.map((player) => (
<NetworkPlayerIndicator
key={player.id}
player={player}
shouldEmphasize={shouldEmphasize}
currentPlayerId={currentPlayerId}
playerScores={playerScores}
playerStreaks={playerStreaks}
roomId={roomInfo?.roomId}
currentUserId={currentUserId ?? undefined}
isCurrentUserHost={isCurrentUserHost}
/>
))}
{networkPlayers.map((player) => (
<NetworkPlayerIndicator
key={player.id}
player={player}
shouldEmphasize={shouldEmphasize}
currentPlayerId={currentPlayerId}
playerScores={playerScores}
playerStreaks={playerStreaks}
playerBadges={playerBadges}
roomId={roomInfo?.roomId}
currentUserId={currentUserId ?? undefined}
isCurrentUserHost={isCurrentUserHost}
/>
))}
</>
)}
</div>
@ -327,6 +331,7 @@ export function GameContextNav({
currentPlayerId={currentPlayerId}
playerScores={playerScores}
playerStreaks={playerStreaks}
playerBadges={playerBadges}
/>
<AddPlayerButton

View File

@ -1,6 +1,7 @@
import { useState } from 'react'
import { PlayerTooltip } from './PlayerTooltip'
import { ReportPlayerModal } from './ReportPlayerModal'
import type { PlayerBadge } from './types'
interface NetworkPlayer {
id: string
@ -19,6 +20,7 @@ interface NetworkPlayerIndicatorProps {
currentPlayerId?: string
playerScores?: Record<string, number>
playerStreaks?: Record<string, number>
playerBadges?: Record<string, PlayerBadge>
// Moderation props
roomId?: string
currentUserId?: string
@ -35,6 +37,7 @@ export function NetworkPlayerIndicator({
currentPlayerId,
playerScores = {},
playerStreaks = {},
playerBadges = {},
roomId,
currentUserId,
isCurrentUserHost,
@ -72,6 +75,7 @@ export function NetworkPlayerIndicator({
return 'normal'
}
const celebrationLevel = getCelebrationLevel(streak)
const badge = playerBadges[player.id]
return (
<>
@ -252,10 +256,45 @@ export function NetworkPlayerIndicator({
`,
}}
/>
</div>
</div>
{/* Turn label */}
{isCurrentPlayer && hasGameState && (
{badge && (
<div
style={{
display: 'inline-flex',
alignItems: 'center',
gap: '6px',
padding: '4px 10px',
borderRadius: '999px',
background: badge.background ?? 'rgba(148, 163, 184, 0.25)',
color: badge.color ?? '#0f172a',
fontSize: '11px',
fontWeight: 700,
letterSpacing: '0.04em',
textTransform: 'uppercase',
boxShadow: badge.shadowColor
? `0 4px 12px ${badge.shadowColor}`
: '0 4px 12px rgba(15, 23, 42, 0.25)',
border: badge.borderColor ? `2px solid ${badge.borderColor}` : '2px solid rgba(255,255,255,0.4)',
backdropFilter: 'blur(4px)',
marginTop: '6px',
whiteSpace: 'nowrap',
}}
>
{badge.icon && (
<span
aria-hidden
style={{ fontSize: '14px', filter: 'drop-shadow(0 2px 4px rgba(15,23,42,0.35))' }}
>
{badge.icon}
</span>
)}
<span>{badge.label}</span>
</div>
)}
{/* Turn label */}
{isCurrentPlayer && hasGameState && (
<div
style={{
fontSize: '12px',

View File

@ -0,0 +1,9 @@
export interface PlayerBadge {
label: string
icon?: string
background?: string
color?: string
borderColor?: string
shadowColor?: string
}