feat(know-your-world): add turn-based restrictions for letter typing
In turn-based learning mode: - Show current player's emoji next to typing instruction - Only allow the current player to type letters - Show "Waiting for [player] to type..." when it's not your turn - Display "Not your turn!" notice when attempting to type during another player's turn This makes it clear whose turn it is and prevents confusion in multiplayer games. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -119,7 +119,13 @@ export function GameInfoPanel({
|
|||||||
} = controlsState
|
} = controlsState
|
||||||
|
|
||||||
// Get game state values
|
// Get game state values
|
||||||
const { giveUpVotes = [], activeUserIds = [], gameMode } = state
|
const {
|
||||||
|
giveUpVotes = [],
|
||||||
|
activeUserIds = [],
|
||||||
|
gameMode,
|
||||||
|
currentPlayer,
|
||||||
|
playerMetadata = {},
|
||||||
|
} = state
|
||||||
|
|
||||||
// Get viewer ID for vote checking
|
// Get viewer ID for vote checking
|
||||||
const { data: viewerId } = useViewerId()
|
const { data: viewerId } = useViewerId()
|
||||||
@@ -127,6 +133,23 @@ export function GameInfoPanel({
|
|||||||
// Touch device detection for virtual keyboard
|
// Touch device detection for virtual keyboard
|
||||||
const isTouchDevice = useIsTouchDevice()
|
const isTouchDevice = useIsTouchDevice()
|
||||||
|
|
||||||
|
// Track "not your turn" notification
|
||||||
|
const [showNotYourTurn, setShowNotYourTurn] = useState(false)
|
||||||
|
|
||||||
|
// Check if it's the local viewer's turn (for turn-based mode)
|
||||||
|
const isMyTurn = useMemo(() => {
|
||||||
|
if (gameMode !== 'turn-based') return true // Always "your turn" in non-turn-based modes
|
||||||
|
if (!currentPlayer || !viewerId) return false
|
||||||
|
const currentPlayerMeta = playerMetadata[currentPlayer]
|
||||||
|
return currentPlayerMeta?.userId === viewerId
|
||||||
|
}, [gameMode, currentPlayer, viewerId, playerMetadata])
|
||||||
|
|
||||||
|
// Get current player's emoji for display
|
||||||
|
const currentPlayerEmoji = useMemo(() => {
|
||||||
|
if (!currentPlayer) return null
|
||||||
|
return playerMetadata[currentPlayer]?.emoji || null
|
||||||
|
}, [currentPlayer, playerMetadata])
|
||||||
|
|
||||||
// Music context and modal state
|
// Music context and modal state
|
||||||
const music = useMusic()
|
const music = useMusic()
|
||||||
const [isMusicModalOpen, setIsMusicModalOpen] = useState(false)
|
const [isMusicModalOpen, setIsMusicModalOpen] = useState(false)
|
||||||
@@ -334,6 +357,20 @@ export function GameInfoPanel({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only accept single character keys (letters only)
|
||||||
|
const pressedLetter = e.key.toLowerCase()
|
||||||
|
if (pressedLetter.length !== 1 || !/[a-z]/i.test(pressedLetter)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// In turn-based mode, only allow the current player to type
|
||||||
|
if (gameMode === 'turn-based' && !isMyTurn) {
|
||||||
|
setShowNotYourTurn(true)
|
||||||
|
// Auto-hide the notice after 2 seconds
|
||||||
|
setTimeout(() => setShowNotYourTurn(false), 2000)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Use optimistic count to prevent race conditions when typing fast
|
// Use optimistic count to prevent race conditions when typing fast
|
||||||
const nextLetterIndex = optimisticLetterCountRef.current
|
const nextLetterIndex = optimisticLetterCountRef.current
|
||||||
if (nextLetterIndex >= requiresNameConfirmation) {
|
if (nextLetterIndex >= requiresNameConfirmation) {
|
||||||
@@ -350,23 +387,26 @@ export function GameInfoPanel({
|
|||||||
// Normalize accented letters to base ASCII (e.g., 'é' → 'e', 'ñ' → 'n')
|
// Normalize accented letters to base ASCII (e.g., 'é' → 'e', 'ñ' → 'n')
|
||||||
// so users can type region names like "Côte d'Ivoire" or "São Tomé" with a regular keyboard
|
// so users can type region names like "Côte d'Ivoire" or "São Tomé" with a regular keyboard
|
||||||
const expectedLetter = normalizeToBaseLetter(letterInfo.char)
|
const expectedLetter = normalizeToBaseLetter(letterInfo.char)
|
||||||
const pressedLetter = e.key.toLowerCase()
|
|
||||||
|
|
||||||
// Only accept single character keys (letters only, no space needed since we skip spaces)
|
if (pressedLetter === expectedLetter) {
|
||||||
if (pressedLetter.length === 1 && /[a-z]/i.test(pressedLetter)) {
|
// Optimistically advance count before server responds
|
||||||
if (pressedLetter === expectedLetter) {
|
optimisticLetterCountRef.current = nextLetterIndex + 1
|
||||||
// Optimistically advance count before server responds
|
// Dispatch to shared state - server validates and broadcasts to all sessions
|
||||||
optimisticLetterCountRef.current = nextLetterIndex + 1
|
confirmLetter(pressedLetter, nextLetterIndex)
|
||||||
// Dispatch to shared state - server validates and broadcasts to all sessions
|
|
||||||
confirmLetter(pressedLetter, nextLetterIndex)
|
|
||||||
}
|
|
||||||
// Ignore wrong characters silently (no feedback, no backspace needed)
|
|
||||||
}
|
}
|
||||||
|
// Ignore wrong characters silently (no feedback, no backspace needed)
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('keydown', handleKeyDown)
|
window.addEventListener('keydown', handleKeyDown)
|
||||||
return () => window.removeEventListener('keydown', handleKeyDown)
|
return () => window.removeEventListener('keydown', handleKeyDown)
|
||||||
}, [requiresNameConfirmation, nameConfirmed, currentRegionName, confirmLetter])
|
}, [
|
||||||
|
requiresNameConfirmation,
|
||||||
|
nameConfirmed,
|
||||||
|
currentRegionName,
|
||||||
|
confirmLetter,
|
||||||
|
gameMode,
|
||||||
|
isMyTurn,
|
||||||
|
])
|
||||||
|
|
||||||
// Check if animation is in progress based on timestamp
|
// Check if animation is in progress based on timestamp
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -653,8 +693,37 @@ export function GameInfoPanel({
|
|||||||
gap: '1.5',
|
gap: '1.5',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<span>⌨️</span>
|
{/* In turn-based mode, show current player's emoji to indicate whose turn it is */}
|
||||||
<span>Type the underlined letter{requiresNameConfirmation > 1 ? 's' : ''}</span>
|
{gameMode === 'turn-based' && currentPlayerEmoji ? (
|
||||||
|
<span>{currentPlayerEmoji}</span>
|
||||||
|
) : (
|
||||||
|
<span>⌨️</span>
|
||||||
|
)}
|
||||||
|
<span>
|
||||||
|
{gameMode === 'turn-based' && !isMyTurn
|
||||||
|
? `Waiting for ${playerMetadata[currentPlayer]?.name || 'player'} to type...`
|
||||||
|
: `Type the underlined letter${requiresNameConfirmation > 1 ? 's' : ''}`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* "Not your turn" notice */}
|
||||||
|
{showNotYourTurn && (
|
||||||
|
<div
|
||||||
|
data-element="not-your-turn-notice"
|
||||||
|
className={css({
|
||||||
|
marginTop: '3',
|
||||||
|
padding: '2 4',
|
||||||
|
bg: isDark ? 'red.900/80' : 'red.100',
|
||||||
|
color: isDark ? 'red.200' : 'red.800',
|
||||||
|
rounded: 'lg',
|
||||||
|
fontSize: 'sm',
|
||||||
|
fontWeight: 'medium',
|
||||||
|
textAlign: 'center',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
⏳ Not your turn! Wait for {playerMetadata[currentPlayer]?.name || 'the other player'}
|
||||||
|
.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</animated.div>
|
</animated.div>
|
||||||
@@ -688,6 +757,13 @@ export function GameInfoPanel({
|
|||||||
})()}
|
})()}
|
||||||
isDark={isDark}
|
isDark={isDark}
|
||||||
onKeyPress={(letter) => {
|
onKeyPress={(letter) => {
|
||||||
|
// In turn-based mode, only allow the current player to type
|
||||||
|
if (gameMode === 'turn-based' && !isMyTurn) {
|
||||||
|
setShowNotYourTurn(true)
|
||||||
|
setTimeout(() => setShowNotYourTurn(false), 2000)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const nextLetterIndex = optimisticLetterCountRef.current
|
const nextLetterIndex = optimisticLetterCountRef.current
|
||||||
if (nextLetterIndex >= requiresNameConfirmation) return
|
if (nextLetterIndex >= requiresNameConfirmation) return
|
||||||
|
|
||||||
@@ -1218,8 +1294,17 @@ export function GameInfoPanel({
|
|||||||
gap: '1.5',
|
gap: '1.5',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<span>⌨️</span>
|
{/* In turn-based mode, show current player's emoji to indicate whose turn it is */}
|
||||||
<span>Type the underlined letter{requiresNameConfirmation > 1 ? 's' : ''}</span>
|
{gameMode === 'turn-based' && currentPlayerEmoji ? (
|
||||||
|
<span>{currentPlayerEmoji}</span>
|
||||||
|
) : (
|
||||||
|
<span>⌨️</span>
|
||||||
|
)}
|
||||||
|
<span>
|
||||||
|
{gameMode === 'turn-based' && !isMyTurn
|
||||||
|
? `Waiting for ${playerMetadata[currentPlayer]?.name || 'player'} to type...`
|
||||||
|
: `Type the underlined letter${requiresNameConfirmation > 1 ? 's' : ''}`}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user