fix(rithmomachia): reconnect player assignment UI and fix setup layout

Restore all missing white/black player assignment functionality that was
disconnected after git rebase operations.

**Player Assignment UI (reconnected):**
- Wire up assignWhitePlayer and assignBlackPlayer from context
- Pass whitePlayerId, blackPlayerId, onAssignWhitePlayer, onAssignBlackPlayer,
  and gamePhase props to PageWithNav
- Enable split [W][B] buttons on hover for unassigned players
- Enable SWAP button on hover for assigned players
- Support observer mode (players not assigned to white/black can spectate)
- Permission system: only host (in rooms) or local players can assign

**Duplicate Indicator Fix:**
- Hide playerBadge pill when assignment UI is active to prevent showing both
  the fancy badge AND the assignment buttons
- Keep "Your turn" / "Their turn" labels (working as intended)

**Setup Layout Fix:**
- Add SetupPlayerRequirement panel when tooFew players (styled for medieval theme)
- Hide entire setup config UI when showing player requirement panel
- Prevents squishing/scrolling issues - clean swap between the two UIs
- Panel shows inactive local players, invite codes, and room history

**Navigation Banner:**
- Hide "Need two active players" banner during setup (panel handles this)
- Show banner during playing phase only

All ~10 hours of missing player management work now restored and functional.

🤖 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-30 21:36:03 -05:00
parent 8f226cee1b
commit a1a0374fac
3 changed files with 88 additions and 24 deletions

View File

@ -6,6 +6,7 @@ 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 { SetupPlayerRequirement } from '@/components/nav/SetupPlayerRequirement'
import { StandardGameLayout } from '@/components/StandardGameLayout'
import { Z_INDEX } from '@/constants/zIndex'
import { useGameMode } from '@/contexts/GameModeContext'
@ -206,6 +207,12 @@ function useRosterWarning(phase: 'setup' | 'playing'): RosterWarning | undefined
}
if (rosterStatus.status === 'tooFew') {
// During setup, don't show nav banner - SetupPlayerRequirement panel handles this
if (phase === 'setup') {
return undefined
}
// During playing phase, show nav warning banner
const actions = []
if (inactiveLocalPlayer) {
actions.push({
@ -221,10 +228,7 @@ function useRosterWarning(phase: 'setup' | 'playing'): RosterWarning | undefined
return {
heading: 'Need two active players',
description:
phase === 'setup'
? 'Rithmomachia needs exactly two active players before the match can begin.'
: 'Gameplay is paused until two players are active.',
description: 'Gameplay is paused until two players are active.',
actions,
}
}
@ -252,7 +256,8 @@ function useRosterWarning(phase: 'setup' | 'playing'): RosterWarning | undefined
*/
export function RithmomachiaGame() {
const router = useRouter()
const { state, resetGame, goToSetup, whitePlayerId, blackPlayerId } = useRithmomachia()
const { state, resetGame, goToSetup, whitePlayerId, blackPlayerId, assignWhitePlayer, assignBlackPlayer } =
useRithmomachia()
const { setFullscreenElement } = useFullscreen()
const gameRef = useRef<HTMLDivElement>(null)
const rosterWarning = useRosterWarning(state.gamePhase === 'setup' ? 'setup' : 'playing')
@ -312,6 +317,11 @@ export function RithmomachiaGame() {
currentPlayerId={currentPlayerId}
playerBadges={playerBadges}
rosterWarning={rosterWarning}
whitePlayerId={whitePlayerId}
blackPlayerId={blackPlayerId}
onAssignWhitePlayer={assignWhitePlayer}
onAssignBlackPlayer={assignBlackPlayer}
gamePhase={state.gamePhase}
>
<StandardGameLayout>
<div
@ -356,6 +366,7 @@ export function RithmomachiaGame() {
*/
function SetupPhase() {
const { state, startGame, setConfig, lastError, clearError, rosterStatus } = useRithmomachia()
const { players: playerMap, activePlayers: activePlayerIds, addPlayer, setActive } = useGameMode()
const startDisabled = rosterStatus.status !== 'ok'
const toggleSetting = (key: keyof typeof state) => {
@ -368,6 +379,10 @@ function SetupPhase() {
setConfig('pointWinThreshold', Math.max(1, value))
}
// Prepare data for SetupPlayerRequirement
const activePlayers = Array.from(playerMap.values()).filter((p) => activePlayerIds.has(p.id))
const inactivePlayers = Array.from(playerMap.values()).filter((p) => !activePlayerIds.has(p.id))
return (
<div
data-component="setup-phase-container"
@ -458,23 +473,68 @@ function SetupPhase() {
</div>
)}
{/* Title Section - Compact medieval manuscript style */}
<div
data-element="title-section"
className={css({
textAlign: 'center',
bg: 'rgba(255, 255, 255, 0.95)',
borderRadius: '1.5vh',
p: '1.5vh',
boxShadow: '0 1vh 3vh rgba(0,0,0,0.5)',
width: '100%',
position: 'relative',
border: '0.3vh solid',
borderColor: 'rgba(251, 191, 36, 0.6)',
backdropFilter: 'blur(10px)',
flexShrink: 0,
})}
>
{/* Player requirement panel - styled for medieval theme */}
{rosterStatus.status === 'tooFew' && (
<div
className={css({
'& > div': {
maxWidth: '100%',
margin: '0',
padding: '1.5vh',
background: 'rgba(30, 27, 75, 0.85)',
border: '0.3vh solid rgba(251, 191, 36, 0.6)',
borderRadius: '1.5vh',
backdropFilter: 'blur(10px)',
'& h2': {
fontSize: '2vh',
background: 'linear-gradient(135deg, #fbbf24, #f59e0b)',
backgroundClip: 'text',
WebkitBackgroundClip: 'text',
},
'& p': {
fontSize: '1.4vh',
color: 'rgba(255, 255, 255, 0.8)',
},
'& button': {
fontSize: '1.4vh',
padding: '0.8vh 1.5vh',
},
},
})}
>
<SetupPlayerRequirement
minPlayers={2}
currentPlayers={activePlayers}
inactivePlayers={inactivePlayers}
onAddPlayer={(playerId) => setActive(playerId, true)}
onConfigurePlayer={() => {
/* TODO: Add configure player handler */
}}
gameTitle="Rithmomachia"
/>
</div>
)}
{/* Only show setup config when we have enough players */}
{rosterStatus.status !== 'tooFew' && (
<>
{/* Title Section - Compact medieval manuscript style */}
<div
data-element="title-section"
className={css({
textAlign: 'center',
bg: 'rgba(255, 255, 255, 0.95)',
borderRadius: '1.5vh',
p: '1.5vh',
boxShadow: '0 1vh 3vh rgba(0,0,0,0.5)',
width: '100%',
position: 'relative',
border: '0.3vh solid',
borderColor: 'rgba(251, 191, 36, 0.6)',
backdropFilter: 'blur(10px)',
flexShrink: 0,
})}
>
{/* Ornamental corners - smaller */}
<div
className={css({
@ -994,6 +1054,8 @@ function SetupPhase() {
>
BEGIN BATTLE
</button>
</>
)}
</div>
</div>
)

View File

@ -223,7 +223,8 @@ export function ActivePlayersList({
</div>
)}
{badge && (
{/* Show playerBadge only if assignment UI is not present */}
{badge && !onAssignWhitePlayer && !onAssignBlackPlayer && (
<div
style={{
display: 'inline-flex',

View File

@ -379,7 +379,8 @@ export function NetworkPlayerIndicator({
/>
</div>
{badge && (
{/* Show playerBadge only if assignment UI is not present */}
{badge && !onAssignWhitePlayer && !onAssignBlackPlayer && (
<div
style={{
display: 'inline-flex',