diff --git a/apps/web/src/arcade-games/know-your-world/Provider.tsx b/apps/web/src/arcade-games/know-your-world/Provider.tsx index 24bf82dc..f5a587f5 100644 --- a/apps/web/src/arcade-games/know-your-world/Provider.tsx +++ b/apps/web/src/arcade-games/know-your-world/Provider.tsx @@ -38,7 +38,12 @@ interface KnowYourWorldContextValue { // Cursor position sharing (for multiplayer) otherPlayerCursors: Record< string, - { x: number; y: number; userId: string; hoveredRegionId: string | null } | null + { + x: number + y: number + userId: string + hoveredRegionId: string | null + } | null > sendCursorUpdate: ( playerId: string, @@ -98,7 +103,7 @@ export function KnowYourWorldProvider({ children }: { children: React.ReactNode : ['huge', 'large', 'medium'] // Default to most regions // Validate assistanceLevel - const validAssistanceLevels = ['guided', 'helpful', 'standard', 'none'] + const validAssistanceLevels = ['learning', 'guided', 'helpful', 'standard', 'none'] const rawAssistance = gameConfig?.assistanceLevel const assistanceLevel: AssistanceLevel = typeof rawAssistance === 'string' && validAssistanceLevels.includes(rawAssistance) diff --git a/apps/web/src/arcade-games/know-your-world/Validator.ts b/apps/web/src/arcade-games/know-your-world/Validator.ts index bb84a1ea..bb025961 100644 --- a/apps/web/src/arcade-games/know-your-world/Validator.ts +++ b/apps/web/src/arcade-games/know-your-world/Validator.ts @@ -127,7 +127,10 @@ export class KnowYourWorldValidator data: any ): ValidationResult { if (state.gamePhase !== 'playing') { - return { valid: false, error: 'Can only click regions during playing phase' } + return { + valid: false, + error: 'Can only click regions during playing phase', + } } if (!state.currentPrompt) { @@ -351,7 +354,10 @@ export class KnowYourWorldValidator includeSizes: RegionSize[] ): ValidationResult { if (state.gamePhase !== 'setup') { - return { valid: false, error: 'Can only change region sizes during setup' } + return { + valid: false, + error: 'Can only change region sizes during setup', + } } const newState: KnowYourWorldState = { @@ -367,7 +373,10 @@ export class KnowYourWorldValidator assistanceLevel: AssistanceLevel ): ValidationResult { if (state.gamePhase !== 'setup') { - return { valid: false, error: 'Can only change assistance level during setup' } + return { + valid: false, + error: 'Can only change assistance level during setup', + } } const newState: KnowYourWorldState = { @@ -448,7 +457,10 @@ export class KnowYourWorldValidator // Check if this session has already voted const existingVotes = state.giveUpVotes ?? [] if (existingVotes.includes(userId)) { - return { valid: false, error: 'Your session has already voted to give up' } + return { + valid: false, + error: 'Your session has already voted to give up', + } } // Add this session's vote @@ -498,10 +510,12 @@ export class KnowYourWorldValidator : [...existingGivenUp, region.id] // Determine re-ask position based on assistance level - // Guided/Helpful: re-ask soon (after 2-3 regions) to reinforce learning + // Learning/Guided/Helpful: re-ask soon (after 2-3 regions) to reinforce learning // Standard/None: re-ask at the end const isHighAssistance = - state.assistanceLevel === 'guided' || state.assistanceLevel === 'helpful' + state.assistanceLevel === 'learning' || + state.assistanceLevel === 'guided' || + state.assistanceLevel === 'helpful' const reaskDelay = isHighAssistance ? 3 : state.regionsToFind.length // Build new regions queue: take next regions, then insert given-up region at appropriate position @@ -548,7 +562,10 @@ export class KnowYourWorldValidator timestamp: number ): ValidationResult { if (state.gamePhase !== 'playing') { - return { valid: false, error: 'Can only request hints during playing phase' } + return { + valid: false, + error: 'Can only request hints during playing phase', + } } if (!state.currentPrompt) { @@ -588,7 +605,13 @@ export class KnowYourWorldValidator : ['huge', 'large', 'medium'] // Default // Validate assistanceLevel - const validAssistanceLevels: AssistanceLevel[] = ['guided', 'helpful', 'standard', 'none'] + const validAssistanceLevels: AssistanceLevel[] = [ + 'learning', + 'guided', + 'helpful', + 'standard', + 'none', + ] const rawAssistance = typedConfig?.assistanceLevel const assistanceLevel: AssistanceLevel = validAssistanceLevels.includes( rawAssistance as AssistanceLevel diff --git a/apps/web/src/arcade-games/know-your-world/components/GameInfoPanel.tsx b/apps/web/src/arcade-games/know-your-world/components/GameInfoPanel.tsx index f93a1932..e24c9ee8 100644 --- a/apps/web/src/arcade-games/know-your-world/components/GameInfoPanel.tsx +++ b/apps/web/src/arcade-games/know-your-world/components/GameInfoPanel.tsx @@ -26,6 +26,8 @@ interface GameInfoPanelProps { foundCount: number totalRegions: number progress: number + /** Callback when hints are unlocked (after name confirmation) */ + onHintsUnlock?: () => void } export function GameInfoPanel({ @@ -36,6 +38,7 @@ export function GameInfoPanel({ foundCount, totalRegions, progress, + onHintsUnlock, }: GameInfoPanelProps) { // Get flag emoji for world map countries (not USA states) const flagEmoji = @@ -117,11 +120,12 @@ export function GameInfoPanel({ if (requiresNameConfirmation > 0 && currentRegionName && nameInput.length > 0) { const requiredPart = currentRegionName.slice(0, requiresNameConfirmation).toLowerCase() const inputPart = nameInput.toLowerCase() - if (inputPart === requiredPart) { + if (inputPart === requiredPart && !nameConfirmed) { setNameConfirmed(true) + onHintsUnlock?.() } } - }, [nameInput, currentRegionName, requiresNameConfirmation]) + }, [nameInput, currentRegionName, requiresNameConfirmation, nameConfirmed, onHintsUnlock]) // Determine if hints are available based on difficulty config const hintsAvailable = useMemo(() => { diff --git a/apps/web/src/arcade-games/know-your-world/components/MapRenderer.tsx b/apps/web/src/arcade-games/know-your-world/components/MapRenderer.tsx index cfe43382..ff27f426 100644 --- a/apps/web/src/arcade-games/know-your-world/components/MapRenderer.tsx +++ b/apps/web/src/arcade-games/know-your-world/components/MapRenderer.tsx @@ -202,7 +202,7 @@ interface MapRendererProps { mapData: MapData regionsFound: string[] currentPrompt: string | null - assistanceLevel: 'guided' | 'helpful' | 'standard' | 'none' // Controls gameplay features (hints, hot/cold) + assistanceLevel: 'learning' | 'guided' | 'helpful' | 'standard' | 'none' // Controls gameplay features (hints, hot/cold) selectedMap: 'world' | 'usa' // Map ID for calculating excluded regions selectedContinent: string // Continent ID for calculating excluded regions onRegionClick: (regionId: string, regionName: string) => void @@ -251,7 +251,12 @@ interface MapRendererProps { localPlayerId?: string // The local player's ID (to filter out our own cursor from others) otherPlayerCursors?: Record< string, - { x: number; y: number; userId: string; hoveredRegionId: string | null } | null + { + x: number + y: number + userId: string + hoveredRegionId: string | null + } | null > onCursorUpdate?: ( cursorPosition: { x: number; y: number } | null, @@ -263,6 +268,8 @@ interface MapRendererProps { viewerId?: string // This viewer's userId (to check if local session has voted) // Member players mapping (userId -> players) for cursor emoji display memberPlayers?: Record> + /** When true, hints are locked (e.g., user hasn't typed required name confirmation yet) */ + hintsLocked?: boolean } /** @@ -328,6 +335,7 @@ export function MapRenderer({ activeUserIds = [], viewerId, memberPlayers = {}, + hintsLocked = false, }: MapRendererProps) { // Extract force tuning parameters with defaults const { @@ -440,8 +448,14 @@ export function MapRenderer({ initialZoom: 10, }) - const [svgDimensions, setSvgDimensions] = useState({ width: 1000, height: 500 }) - const [cursorPosition, setCursorPosition] = useState<{ x: number; y: number } | null>(null) + const [svgDimensions, setSvgDimensions] = useState({ + width: 1000, + height: 500, + }) + const [cursorPosition, setCursorPosition] = useState<{ + x: number + y: number + } | null>(null) const [showMagnifier, setShowMagnifier] = useState(false) const [targetOpacity, setTargetOpacity] = useState(0) const [targetTop, setTargetTop] = useState(20) @@ -754,12 +768,13 @@ export function MapRenderer({ hotColdEnabledRef.current = effectiveHotColdEnabled // Handle hint bubble and auto-speak when the prompt changes (new region to find) - // Only runs when currentPrompt changes, not when settings change + // Also re-runs when hintsLocked changes (e.g., user unlocked hints by typing name) useEffect(() => { const isNewRegion = prevPromptRef.current !== null && prevPromptRef.current !== currentPrompt prevPromptRef.current = currentPrompt - if (autoHintRef.current && hasHint) { + // Don't auto-show hints when locked (e.g., waiting for name confirmation) + if (autoHintRef.current && hasHint && !hintsLocked) { setShowHintBubble(true) // If region changed and both auto-hint and auto-speak are enabled, speak immediately // This handles the case where the bubble was already open @@ -769,7 +784,15 @@ export function MapRenderer({ } else { setShowHintBubble(false) } - }, [currentPrompt, hasHint, currentRegionName, hintText, isSpeechSupported, speakWithRegionName]) + }, [ + currentPrompt, + hasHint, + currentRegionName, + hintText, + isSpeechSupported, + speakWithRegionName, + hintsLocked, + ]) // Hot/cold audio feedback hook // Only enabled if: 1) assistance level allows it, 2) user toggle is on, 3) not touch device @@ -865,7 +888,10 @@ export function MapRenderer({ if (pointerLocked && cursorPositionRef.current && containerRef.current && svgRef.current) { const { x: cursorX, y: cursorY } = cursorPositionRef.current - console.log('[CLICK] Pointer lock click at cursor position:', { cursorX, cursorY }) + console.log('[CLICK] Pointer lock click at cursor position:', { + cursorX, + cursorY, + }) // Check if clicking on any registered button (Give Up, Hint, etc.) if (buttonRegistry.handleClick(cursorX, cursorY)) { @@ -1127,7 +1153,11 @@ export function MapRenderer({ }) // Start zoom-in animation using CSS transform - console.log('[GiveUp Zoom] Setting zoom target:', { scale, translateX, translateY }) + console.log('[GiveUp Zoom] Setting zoom target:', { + scale, + translateX, + translateY, + }) setGiveUpZoomTarget({ scale, translateX, translateY }) } } @@ -1666,8 +1696,13 @@ export function MapRenderer({ const viewBoxHeight = viewBoxParts[3] || 1000 const showOutline = (region: MapRegion): boolean => { - // Guided/Helpful modes: always show outlines - if (assistanceLevel === 'guided' || assistanceLevel === 'helpful') return true + // Learning/Guided/Helpful modes: always show outlines + if ( + assistanceLevel === 'learning' || + assistanceLevel === 'guided' || + assistanceLevel === 'helpful' + ) + return true // Standard/None modes: only show outline on hover or if found return hoveredRegion === region.id || regionsFound.includes(region.id) @@ -1763,7 +1798,10 @@ export function MapRenderer({ width: containerRect.width.toFixed(1), height: containerRect.height.toFixed(1), }, - svgSize: { width: svgRect.width.toFixed(1), height: svgRect.height.toFixed(1) }, + svgSize: { + width: svgRect.width.toFixed(1), + height: svgRect.height.toFixed(1), + }, svgOffset: { x: svgOffsetX.toFixed(1), y: svgOffsetY.toFixed(1) }, distances: { left: dampenedDistLeft.toFixed(1), @@ -3722,7 +3760,10 @@ export function MapRenderer({ const magTL = { x: magLeft, y: magTop } const magTR = { x: magLeft + magnifierWidth, y: magTop } const magBL = { x: magLeft, y: magTop + magnifierHeight } - const magBR = { x: magLeft + magnifierWidth, y: magTop + magnifierHeight } + const magBR = { + x: magLeft + magnifierWidth, + y: magTop + magnifierHeight, + } // Check if a line segment passes through a rectangle (excluding endpoints) const linePassesThroughRect = ( @@ -3963,7 +4004,11 @@ export function MapRenderer({ {zoomSearchDebugInfo && ( <>
Zoom Decision:
@@ -4011,7 +4056,13 @@ export function MapRenderer({ )} -
+
Detected Regions ({detectedRegions.length}):
{detectedRegions.map((region) => ( diff --git a/apps/web/src/arcade-games/know-your-world/components/PlayingPhase.tsx b/apps/web/src/arcade-games/know-your-world/components/PlayingPhase.tsx index 4e1c1ea3..e9f0d513 100644 --- a/apps/web/src/arcade-games/know-your-world/components/PlayingPhase.tsx +++ b/apps/web/src/arcade-games/know-your-world/components/PlayingPhase.tsx @@ -1,10 +1,10 @@ 'use client' -import { useCallback, useMemo } from 'react' +import { useCallback, useMemo, useState, useEffect } from 'react' import { css } from '@styled/css' import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels' import { useKnowYourWorld } from '../Provider' -import { getFilteredMapDataBySizesSync } from '../maps' +import { getFilteredMapDataBySizesSync, getAssistanceLevel } from '../maps' import { MapRenderer } from './MapRenderer' import { GameInfoPanel } from './GameInfoPanel' import { useViewerId } from '@/lib/arcade/game-sdk' @@ -55,6 +55,26 @@ export function PlayingPhase() { const currentRegionName = currentRegion?.name ?? null const currentRegionId = currentRegion?.id ?? null + // Check if hints are locked (name confirmation required but not yet done) + const assistanceConfig = getAssistanceLevel(state.assistanceLevel) + const requiresNameConfirmation = assistanceConfig.nameConfirmationLetters ?? 0 + + // Track whether hints have been unlocked for the current region + const [hintsUnlocked, setHintsUnlocked] = useState(false) + + // Reset hints locked state when region changes + useEffect(() => { + setHintsUnlocked(false) + }, [state.currentPrompt]) + + // Hints are locked if name confirmation is required and not yet unlocked + const hintsLocked = requiresNameConfirmation > 0 && !hintsUnlocked + + // Callback for GameInfoPanel to notify when hints are unlocked + const handleHintsUnlock = useCallback(() => { + setHintsUnlocked(true) + }, []) + // Error if prompt not found in filtered regions (indicates server/client filter mismatch) if (state.currentPrompt && !currentRegion) { const errorInfo = { @@ -102,6 +122,7 @@ export function PlayingPhase() { foundCount={foundCount} totalRegions={totalRegions} progress={progress} + onHintsUnlock={handleHintsUnlock} /> @@ -164,6 +185,7 @@ export function PlayingPhase() { activeUserIds={state.activeUserIds} viewerId={viewerId ?? undefined} memberPlayers={memberPlayers} + hintsLocked={hintsLocked} />
diff --git a/apps/web/src/arcade-games/know-your-world/components/SetupPhase.tsx b/apps/web/src/arcade-games/know-your-world/components/SetupPhase.tsx index ba54005b..6d588020 100644 --- a/apps/web/src/arcade-games/know-your-world/components/SetupPhase.tsx +++ b/apps/web/src/arcade-games/know-your-world/components/SetupPhase.tsx @@ -369,7 +369,7 @@ export function SetupPhase() { - setAssistanceLevel(value as 'guided' | 'helpful' | 'standard' | 'none') + setAssistanceLevel(value as 'learning' | 'guided' | 'helpful' | 'standard' | 'none') } > diff --git a/apps/web/src/arcade-games/know-your-world/index.ts b/apps/web/src/arcade-games/know-your-world/index.ts index 021f2e4f..fdddf144 100644 --- a/apps/web/src/arcade-games/know-your-world/index.ts +++ b/apps/web/src/arcade-games/know-your-world/index.ts @@ -48,7 +48,7 @@ function validateKnowYourWorldConfig(config: unknown): config is KnowYourWorldCo ] const validSizes = ['huge', 'large', 'medium', 'small', 'tiny'] - const validAssistanceLevels = ['guided', 'helpful', 'standard', 'none'] + const validAssistanceLevels = ['learning', 'guided', 'helpful', 'standard', 'none'] return ( typeof config === 'object' && diff --git a/apps/web/src/arcade-games/know-your-world/maps.ts b/apps/web/src/arcade-games/know-your-world/maps.ts index c3b222b7..8c328c6c 100644 --- a/apps/web/src/arcade-games/know-your-world/maps.ts +++ b/apps/web/src/arcade-games/know-your-world/maps.ts @@ -163,11 +163,11 @@ export interface AssistanceLevelConfig { */ export const ASSISTANCE_LEVELS: AssistanceLevelConfig[] = [ { - id: 'guided', - label: 'Guided', - emoji: '🎓', + id: 'learning', + label: 'Learning', + emoji: '📚', description: - 'Maximum help - type name to unlock hints, hot/cold feedback, shows names on wrong clicks', + 'Type first 3 letters to unlock hints, maximum feedback, best for memorizing names', hotColdEnabled: true, hintsMode: 'onRequest', autoHintDefault: true, @@ -176,6 +176,18 @@ export const ASSISTANCE_LEVELS: AssistanceLevelConfig[] = [ wrongClickShowsName: true, nameConfirmationLetters: 3, // Must type first 3 letters to unlock hints }, + { + id: 'guided', + label: 'Guided', + emoji: '🎓', + description: 'Maximum help - auto hints, hot/cold feedback, shows names on wrong clicks', + hotColdEnabled: true, + hintsMode: 'onRequest', + autoHintDefault: true, + struggleHintEnabled: true, + giveUpMode: 'reaskSoon', + wrongClickShowsName: true, + }, { id: 'helpful', label: 'Helpful', diff --git a/apps/web/src/arcade-games/know-your-world/types.ts b/apps/web/src/arcade-games/know-your-world/types.ts index ff8461be..bb1fa86e 100644 --- a/apps/web/src/arcade-games/know-your-world/types.ts +++ b/apps/web/src/arcade-games/know-your-world/types.ts @@ -6,7 +6,7 @@ import type { MapDifficultyConfig, RegionSize } from './maps' * Assistance level - controls gameplay features (hints, hot/cold, etc.) * Separate from region filtering */ -export type AssistanceLevel = 'guided' | 'helpful' | 'standard' | 'none' +export type AssistanceLevel = 'learning' | 'guided' | 'helpful' | 'standard' | 'none' // Game configuration (persisted to database) export interface KnowYourWorldConfig extends GameConfig {