diff --git a/apps/web/src/arcade-games/complement-race/Provider.tsx b/apps/web/src/arcade-games/complement-race/Provider.tsx index 329827b2..809da2e6 100644 --- a/apps/web/src/arcade-games/complement-race/Provider.tsx +++ b/apps/web/src/arcade-games/complement-race/Provider.tsx @@ -564,6 +564,23 @@ export function ComplementRaceProvider({ children }: { children: ReactNode }) { } }, [multiplayerState.currentRoute, compatibleState.style, multiplayerState.passengers.length]) + // Broadcast position to server for multiplayer ghost trains + useEffect(() => { + if (!compatibleState.isGameActive || compatibleState.style !== 'sprint') { + return + } + + // Send position update every 200ms + const interval = setInterval(() => { + makeMove({ + type: 'UPDATE_POSITION', + data: { position: clientPosition }, + }) + }, 200) + + return () => clearInterval(interval) + }, [compatibleState.isGameActive, compatibleState.style, clientPosition, makeMove]) + // Keep lastLogRef for future debugging needs // (removed debug logging) diff --git a/apps/web/src/arcade-games/complement-race/Validator.ts b/apps/web/src/arcade-games/complement-race/Validator.ts index b1f84948..564fa9fb 100644 --- a/apps/web/src/arcade-games/complement-race/Validator.ts +++ b/apps/web/src/arcade-games/complement-race/Validator.ts @@ -97,6 +97,9 @@ export class ComplementRaceValidator case 'UPDATE_INPUT': return this.validateUpdateInput(state, move.playerId, move.data.input) + case 'UPDATE_POSITION': + return this.validateUpdatePosition(state, move.playerId, move.data.position) + case 'CLAIM_PASSENGER': return this.validateClaimPassenger( state, @@ -397,6 +400,39 @@ export class ComplementRaceValidator return { valid: true, newState } } + private validateUpdatePosition( + state: ComplementRaceState, + playerId: string, + position: number + ): ValidationResult { + if (state.gamePhase !== 'playing') { + return { valid: false, error: 'Game not in playing phase' } + } + + const player = state.players[playerId] + if (!player) { + return { valid: false, error: 'Player not found' } + } + + // Validate position is a reasonable number (0-100) + if (typeof position !== 'number' || position < 0 || position > 100) { + return { valid: false, error: 'Invalid position value' } + } + + const newState: ComplementRaceState = { + ...state, + players: { + ...state.players, + [playerId]: { + ...player, + position, + }, + }, + } + + return { valid: true, newState } + } + // ========================================================================== // Sprint Mode: Passenger Management // ========================================================================== diff --git a/apps/web/src/arcade-games/complement-race/types.ts b/apps/web/src/arcade-games/complement-race/types.ts index 12020046..43c4422e 100644 --- a/apps/web/src/arcade-games/complement-race/types.ts +++ b/apps/web/src/arcade-games/complement-race/types.ts @@ -143,6 +143,7 @@ export type ComplementRaceMove = BaseGameMove & // Playing phase | { type: 'SUBMIT_ANSWER'; data: { answer: number; responseTime: number } } | { type: 'UPDATE_INPUT'; data: { input: string } } // Show "thinking" indicator + | { type: 'UPDATE_POSITION'; data: { position: number } } // Sprint mode: sync train position | { type: 'CLAIM_PASSENGER'; data: { passengerId: string; carIndex: number } } // Sprint mode: pickup | { type: 'DELIVER_PASSENGER'; data: { passengerId: string } } // Sprint mode: delivery