feat(matching): use nav avatars as turn indicators
Replace the in-game PlayerStatusBar with turn indicators integrated directly into the nav bar avatars. This creates a cleaner, more unified UI with less visual clutter while making turn state always visible at the top of the screen. Changes: - Pass game state (currentPlayer, scores, streaks) through PageWithNav to GameContextNav to ActivePlayersList - Add turn indicator styling to avatars: - Current player: larger (70px vs 56px), colored border ring with glow - Other players: dimmed opacity (0.65) - Floating animation on current player - Add score badge (bottom-right) showing pairs matched - Add streak badge (top-right) with fire emoji, color-coded by level: - Green: 2+ streak (great) - Orange: 3+ streak (epic) - Purple: 5+ streak (legendary) - Remove PlayerStatusBar from GamePhase since nav now shows all info - Add CSS animations: avatarFloat, borderPulse, streakPulse Benefits: - Less visual clutter in game area - Turn state always visible in persistent nav - More space for the actual game - Consistent with nav-as-control-center design 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,6 @@ import { useGameMode } from '../../../../contexts/GameModeContext'
|
||||
import { pluralizeWord } from '../../../../utils/pluralization'
|
||||
import { useMemoryPairs } from '../context/MemoryPairsContext'
|
||||
import { MemoryGrid } from './MemoryGrid'
|
||||
import { PlayerStatusBar } from './PlayerStatusBar'
|
||||
|
||||
export function GamePhase() {
|
||||
const { state, resetGame, activePlayers } = useMemoryPairs()
|
||||
@@ -119,8 +118,7 @@ export function GamePhase() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Player Status Bar */}
|
||||
<PlayerStatusBar />
|
||||
{/* Player Status Bar - Removed, now using nav avatars as turn indicator */}
|
||||
|
||||
{/* Memory Grid - The main game area */}
|
||||
<div
|
||||
|
||||
@@ -28,6 +28,9 @@ export function MemoryPairsGame() {
|
||||
navTitle="Memory Pairs"
|
||||
navEmoji="🧩"
|
||||
emphasizeGameContext={state.gamePhase === 'setup'}
|
||||
currentPlayerId={state.currentPlayer}
|
||||
playerScores={state.scores}
|
||||
playerStreaks={state.consecutiveMatches}
|
||||
>
|
||||
<StandardGameLayout>
|
||||
<div
|
||||
|
||||
@@ -18,6 +18,10 @@ interface PageWithNavProps {
|
||||
onNewGame?: () => void
|
||||
canModifyPlayers?: boolean
|
||||
children: React.ReactNode
|
||||
// Game state for turn indicator
|
||||
currentPlayerId?: string
|
||||
playerScores?: Record<string, number>
|
||||
playerStreaks?: Record<string, number>
|
||||
}
|
||||
|
||||
export function PageWithNav({
|
||||
@@ -29,6 +33,9 @@ export function PageWithNav({
|
||||
onNewGame,
|
||||
canModifyPlayers = true,
|
||||
children,
|
||||
currentPlayerId,
|
||||
playerScores,
|
||||
playerStreaks,
|
||||
}: PageWithNavProps) {
|
||||
const { players, activePlayers, setActive, activePlayerCount } = useGameMode()
|
||||
const { hasActiveSession, activeSession } = useArcadeGuard({
|
||||
@@ -142,6 +149,9 @@ export function PageWithNav({
|
||||
canModifyPlayers={canModifyPlayers}
|
||||
roomInfo={roomInfo}
|
||||
networkPlayers={networkPlayers}
|
||||
currentPlayerId={currentPlayerId}
|
||||
playerScores={playerScores}
|
||||
playerStreaks={playerStreaks}
|
||||
/>
|
||||
) : null
|
||||
|
||||
|
||||
@@ -15,6 +15,10 @@ interface ActivePlayersListProps {
|
||||
shouldEmphasize: boolean
|
||||
onRemovePlayer: (playerId: string) => void
|
||||
onConfigurePlayer: (playerId: string) => void
|
||||
// Game state for turn indicator
|
||||
currentPlayerId?: string
|
||||
playerScores?: Record<string, number>
|
||||
playerStreaks?: Record<string, number>
|
||||
}
|
||||
|
||||
export function ActivePlayersList({
|
||||
@@ -22,37 +26,122 @@ export function ActivePlayersList({
|
||||
shouldEmphasize,
|
||||
onRemovePlayer,
|
||||
onConfigurePlayer,
|
||||
currentPlayerId,
|
||||
playerScores = {},
|
||||
playerStreaks = {},
|
||||
}: ActivePlayersListProps) {
|
||||
const [hoveredPlayerId, setHoveredPlayerId] = React.useState<string | null>(null)
|
||||
|
||||
// Helper to get celebration level based on consecutive matches
|
||||
const getCelebrationLevel = (consecutiveMatches: number) => {
|
||||
if (consecutiveMatches >= 5) return 'legendary'
|
||||
if (consecutiveMatches >= 3) return 'epic'
|
||||
if (consecutiveMatches >= 2) return 'great'
|
||||
return 'normal'
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{activePlayers.map((player) => (
|
||||
<PlayerTooltip
|
||||
key={player.id}
|
||||
playerName={player.name}
|
||||
playerColor={player.color}
|
||||
isLocal={player.isLocal !== false}
|
||||
createdAt={player.createdAt}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
fontSize: '56px',
|
||||
lineHeight: 1,
|
||||
transition: 'filter 0.4s ease',
|
||||
filter: 'drop-shadow(0 6px 12px rgba(0,0,0,0.3))',
|
||||
cursor: shouldEmphasize ? 'pointer' : 'default',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
onClick={() => shouldEmphasize && onConfigurePlayer(player.id)}
|
||||
onMouseEnter={() => shouldEmphasize && setHoveredPlayerId(player.id)}
|
||||
onMouseLeave={() => shouldEmphasize && setHoveredPlayerId(null)}
|
||||
{activePlayers.map((player) => {
|
||||
const isCurrentPlayer = player.id === currentPlayerId
|
||||
const score = playerScores[player.id] || 0
|
||||
const streak = playerStreaks[player.id] || 0
|
||||
const celebrationLevel = getCelebrationLevel(streak)
|
||||
|
||||
return (
|
||||
<PlayerTooltip
|
||||
key={player.id}
|
||||
playerName={player.name}
|
||||
playerColor={player.color}
|
||||
isLocal={player.isLocal !== false}
|
||||
createdAt={player.createdAt}
|
||||
>
|
||||
{player.emoji}
|
||||
{shouldEmphasize && hoveredPlayerId === player.id && (
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
fontSize: isCurrentPlayer ? '70px' : '56px',
|
||||
lineHeight: 1,
|
||||
transition: 'all 0.4s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
filter: 'drop-shadow(0 6px 12px rgba(0,0,0,0.3))',
|
||||
cursor: shouldEmphasize ? 'pointer' : 'default',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
opacity: isCurrentPlayer ? 1 : 0.65,
|
||||
transform: isCurrentPlayer ? 'scale(1.1)' : 'scale(1)',
|
||||
animation: isCurrentPlayer ? 'avatarFloat 3s ease-in-out infinite' : 'none',
|
||||
}}
|
||||
onClick={() => shouldEmphasize && onConfigurePlayer(player.id)}
|
||||
onMouseEnter={() => shouldEmphasize && setHoveredPlayerId(player.id)}
|
||||
onMouseLeave={() => shouldEmphasize && setHoveredPlayerId(null)}
|
||||
>
|
||||
{/* Border ring for current player */}
|
||||
{isCurrentPlayer && (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: '-8px',
|
||||
borderRadius: '50%',
|
||||
border: `4px solid ${player.color || '#3b82f6'}`,
|
||||
boxShadow: `0 0 0 2px white, 0 0 20px ${player.color || '#3b82f6'}80`,
|
||||
animation: 'borderPulse 2s ease-in-out infinite',
|
||||
zIndex: -1,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{player.emoji}
|
||||
|
||||
{/* Score badge - bottom right */}
|
||||
{score > 0 && (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: '-6px',
|
||||
right: '-6px',
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
borderRadius: '50%',
|
||||
border: '3px solid white',
|
||||
background: player.color || '#3b82f6',
|
||||
color: 'white',
|
||||
fontSize: '11px',
|
||||
fontWeight: 'bold',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
boxShadow: '0 4px 10px rgba(0,0,0,0.4)',
|
||||
zIndex: 2,
|
||||
lineHeight: 1,
|
||||
}}
|
||||
>
|
||||
{score}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Streak badge - top right */}
|
||||
{streak >= 2 && (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '-8px',
|
||||
right: '-8px',
|
||||
fontSize: '20px',
|
||||
filter:
|
||||
celebrationLevel === 'legendary'
|
||||
? 'drop-shadow(0 0 8px #a855f7)'
|
||||
: celebrationLevel === 'epic'
|
||||
? 'drop-shadow(0 0 8px #f97316)'
|
||||
: 'drop-shadow(0 0 8px #22c55e)',
|
||||
animation: isCurrentPlayer ? 'streakPulse 1s ease-in-out infinite' : 'none',
|
||||
zIndex: 2,
|
||||
}}
|
||||
>
|
||||
🔥
|
||||
</div>
|
||||
)}
|
||||
|
||||
{shouldEmphasize && hoveredPlayerId === player.id && (
|
||||
<>
|
||||
{/* Configure button - bottom left */}
|
||||
<button
|
||||
@@ -142,7 +231,45 @@ export function ActivePlayersList({
|
||||
)}
|
||||
</div>
|
||||
</PlayerTooltip>
|
||||
))}
|
||||
)})}
|
||||
|
||||
{/* Animation styles */}
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
@keyframes avatarFloat {
|
||||
0%, 100% {
|
||||
transform: scale(1.1) translateY(0px);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1) translateY(-6px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes borderPulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
box-shadow: 0 0 0 2px white, 0 0 20px currentColor;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.8;
|
||||
box-shadow: 0 0 0 2px white, 0 0 30px currentColor;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes streakPulse {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.2);
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -47,6 +47,10 @@ interface GameContextNavProps {
|
||||
// Arcade session info
|
||||
networkPlayers?: NetworkPlayer[]
|
||||
roomInfo?: ArcadeRoomInfo
|
||||
// Game state for turn indicator
|
||||
currentPlayerId?: string
|
||||
playerScores?: Record<string, number>
|
||||
playerStreaks?: Record<string, number>
|
||||
}
|
||||
|
||||
export function GameContextNav({
|
||||
@@ -66,6 +70,9 @@ export function GameContextNav({
|
||||
canModifyPlayers = true,
|
||||
networkPlayers = [],
|
||||
roomInfo,
|
||||
currentPlayerId,
|
||||
playerScores,
|
||||
playerStreaks,
|
||||
}: GameContextNavProps) {
|
||||
// 2x2 grid layout for normal mode, column for fullscreen
|
||||
if (showFullscreenSelection) {
|
||||
@@ -259,6 +266,9 @@ export function GameContextNav({
|
||||
shouldEmphasize={shouldEmphasize}
|
||||
onRemovePlayer={onRemovePlayer}
|
||||
onConfigurePlayer={onConfigurePlayer}
|
||||
currentPlayerId={currentPlayerId}
|
||||
playerScores={playerScores}
|
||||
playerStreaks={playerStreaks}
|
||||
/>
|
||||
|
||||
{canModifyPlayers && (
|
||||
|
||||
Reference in New Issue
Block a user