diff --git a/apps/web/src/arcade-games/know-your-world/components/MapRenderer.stories.tsx b/apps/web/src/arcade-games/know-your-world/components/MapRenderer.stories.tsx index 62013520..99ebe32c 100644 --- a/apps/web/src/arcade-games/know-your-world/components/MapRenderer.stories.tsx +++ b/apps/web/src/arcade-games/know-your-world/components/MapRenderer.stories.tsx @@ -105,7 +105,6 @@ const Template = (args: any) => { onRegionClick={(id, name) => console.log('Clicked:', id, name)} guessHistory={guessHistory} playerMetadata={mockPlayerMetadata} - pointerLocked={false} forceTuning={{ showArrows: args.showArrows, centeringStrength: args.centeringStrength, 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 627db06c..cdda73fe 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 @@ -48,7 +48,6 @@ interface MapRendererProps { color: string } > - pointerLocked: boolean // Whether pointer lock is currently active // Force simulation tuning parameters forceTuning?: { showArrows?: boolean @@ -109,7 +108,6 @@ export function MapRenderer({ onRegionClick, guessHistory, playerMetadata, - pointerLocked, forceTuning = {}, }: MapRendererProps) { // Extract force tuning parameters with defaults @@ -165,6 +163,10 @@ export function MapRenderer({ const [targetTop, setTargetTop] = useState(20) const [targetLeft, setTargetLeft] = useState(20) + // Pointer lock management + const [pointerLocked, setPointerLocked] = useState(false) + const [showLockPrompt, setShowLockPrompt] = useState(true) + // Cursor position tracking (container-relative coordinates) const cursorPositionRef = useRef<{ x: number; y: number } | null>(null) const lastMoveTimeRef = useRef(Date.now()) @@ -189,6 +191,71 @@ export function MapRenderer({ return 1.0 // Normal speed for larger regions } + // Set up pointer lock event listeners + useEffect(() => { + const handlePointerLockChange = () => { + const isLocked = document.pointerLockElement === containerRef.current + console.log('[MapRenderer] Pointer lock change event:', { + isLocked, + pointerLockElement: document.pointerLockElement, + containerElement: containerRef.current, + elementsMatch: document.pointerLockElement === containerRef.current, + }) + setPointerLocked(isLocked) + if (isLocked) { + setShowLockPrompt(false) // Hide prompt when locked + } + } + + const handlePointerLockError = () => { + console.error('[Pointer Lock] ❌ Failed to acquire pointer lock') + setPointerLocked(false) + } + + document.addEventListener('pointerlockchange', handlePointerLockChange) + document.addEventListener('pointerlockerror', handlePointerLockError) + + console.log('[MapRenderer] Pointer lock listeners attached') + + return () => { + document.removeEventListener('pointerlockchange', handlePointerLockChange) + document.removeEventListener('pointerlockerror', handlePointerLockError) + console.log('[MapRenderer] Pointer lock listeners removed') + } + }, []) + + // Release pointer lock when component unmounts + useEffect(() => { + return () => { + if (document.pointerLockElement) { + console.log('[Pointer Lock] 🔓 RELEASING (MapRenderer unmount)') + document.exitPointerLock() + } + } + }, []) + + // Request pointer lock on first click + const handleContainerClick = (e: React.MouseEvent) => { + console.log('[MapRenderer] Container clicked:', { + pointerLocked, + hasContainer: !!containerRef.current, + showLockPrompt, + willRequestLock: !pointerLocked && containerRef.current && showLockPrompt, + target: e.target, + }) + + if (!pointerLocked && containerRef.current && showLockPrompt) { + console.log('[Pointer Lock] 🔒 REQUESTING pointer lock (user clicked map)') + try { + containerRef.current.requestPointerLock() + console.log('[Pointer Lock] Request sent successfully') + } catch (error) { + console.error('[Pointer Lock] Request failed with error:', error) + } + setShowLockPrompt(false) // Hide prompt after first click attempt + } + } + // Animated spring values for smooth transitions // Different fade speeds: fast fade-in (100ms), slow fade-out (1000ms) // Position animates with medium speed (300ms) @@ -911,6 +978,7 @@ export function MapRenderer({ data-component="map-renderer" onMouseMove={handleMouseMove} onMouseLeave={handleMouseLeave} + onClick={handleContainerClick} className={css({ position: 'relative', width: '100%', @@ -922,6 +990,41 @@ export function MapRenderer({ shadow: 'lg', })} > + {/* Pointer Lock Prompt Overlay */} + {showLockPrompt && !pointerLocked && ( +
+
🎯
+
+ Enable Precision Controls +
+
+ Click anywhere to lock cursor for precise control +
+
+ )} (null) const mapData = getFilteredMapData(state.selectedMap, state.selectedContinent, state.difficulty) const totalRegions = mapData.regions.length @@ -29,74 +26,6 @@ export function PlayingPhase() { } }, [lastError, clearError]) - // Set up pointer lock event listeners - useEffect(() => { - const handlePointerLockChange = () => { - const isLocked = document.pointerLockElement === containerRef.current - console.log('[PlayingPhase] Pointer lock change event:', { - isLocked, - pointerLockElement: document.pointerLockElement, - containerElement: containerRef.current, - elementsMatch: document.pointerLockElement === containerRef.current, - }) - setPointerLocked(isLocked) - console.log('[Pointer Lock]', isLocked ? '🔒 LOCKED' : '🔓 UNLOCKED') - } - - const handlePointerLockError = () => { - console.error('[Pointer Lock] ❌ Failed to acquire pointer lock') - setPointerLocked(false) - setShowLockPrompt(true) // Show prompt again if lock fails - } - - document.addEventListener('pointerlockchange', handlePointerLockChange) - document.addEventListener('pointerlockerror', handlePointerLockError) - - console.log('[PlayingPhase] Pointer lock listeners attached') - - return () => { - document.removeEventListener('pointerlockchange', handlePointerLockChange) - document.removeEventListener('pointerlockerror', handlePointerLockError) - console.log('[PlayingPhase] Pointer lock listeners removed') - } - }, []) - - // Release pointer lock when component unmounts (game ends) - useEffect(() => { - return () => { - if (document.pointerLockElement) { - console.log('[Pointer Lock] 🔓 RELEASING (PlayingPhase unmount)') - document.exitPointerLock() - } - } - }, []) - - // Request pointer lock on first click - const handleContainerClick = () => { - console.log('[PlayingPhase] Container clicked:', { - pointerLocked, - hasContainer: !!containerRef.current, - showLockPrompt, - willRequestLock: !pointerLocked && containerRef.current && showLockPrompt, - }) - - if (!pointerLocked && containerRef.current && showLockPrompt) { - console.log('[Pointer Lock] 🔒 REQUESTING pointer lock (user clicked)') - try { - containerRef.current.requestPointerLock() - console.log('[Pointer Lock] Request sent successfully') - } catch (error) { - console.error('[Pointer Lock] Request failed with error:', error) - } - setShowLockPrompt(false) // Hide prompt after first click - } - } - - // Log when pointerLocked state changes - useEffect(() => { - console.log('[PlayingPhase] pointerLocked state changed:', pointerLocked) - }, [pointerLocked]) - // Get the display name for the current prompt const currentRegionName = state.currentPrompt ? mapData.regions.find((r) => r.id === state.currentPrompt)?.name @@ -115,9 +44,7 @@ export function PlayingPhase() { return (
- {/* Pointer Lock Prompt Overlay */} - {showLockPrompt && !pointerLocked && ( -
-
🎯
-
- Enable Precision Controls -
-
- Click anywhere to lock cursor and enable precise clicking on tiny regions -
-
- )} {/* Current Prompt */}
{/* Game Mode Info */}