fix: move pointer lock management to MapRenderer

**Problem:** Pointer lock was activating on MapRenderer's container,
but PlayingPhase was trying to manage it, so state never updated.

**Solution:** Move all pointer lock management into MapRenderer:
- Add pointerLocked state in MapRenderer
- Add pointer lock event listeners in MapRenderer
- Add click handler to request lock on MapRenderer container
- Add "Enable Precision Controls" overlay in MapRenderer
- Remove all pointer lock code from PlayingPhase

Now pointer lock state tracks correctly and custom cursor will render.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock
2025-11-19 09:26:08 -06:00
parent 5388441ebb
commit 0ed4d13db6
3 changed files with 105 additions and 112 deletions

View File

@@ -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,

View File

@@ -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<number>(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<HTMLDivElement>) => {
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 && (
<div
data-element="pointer-lock-prompt"
className={css({
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
bg: isDark ? 'rgba(0, 0, 0, 0.9)' : 'rgba(255, 255, 255, 0.95)',
color: isDark ? 'white' : 'gray.900',
padding: '8',
rounded: 'xl',
border: '3px solid',
borderColor: 'blue.500',
boxShadow: '0 10px 40px rgba(0, 0, 0, 0.5)',
zIndex: 10000,
textAlign: 'center',
cursor: 'pointer',
transition: 'all 0.2s',
_hover: {
transform: 'translate(-50%, -50%) scale(1.05)',
borderColor: 'blue.400',
},
})}
>
<div className={css({ fontSize: '4xl', marginBottom: '4' })}>🎯</div>
<div className={css({ fontSize: 'xl', fontWeight: 'bold', marginBottom: '2' })}>
Enable Precision Controls
</div>
<div className={css({ fontSize: 'sm', color: isDark ? 'gray.400' : 'gray.600' })}>
Click anywhere to lock cursor for precise control
</div>
</div>
)}
<svg
ref={svgRef}
viewBox={mapData.viewBox}

View File

@@ -12,9 +12,6 @@ export function PlayingPhase() {
const isDark = resolvedTheme === 'dark'
const { state, clickRegion, lastError, clearError } = useKnowYourWorld()
const [pointerLocked, setPointerLocked] = useState(false)
const [showLockPrompt, setShowLockPrompt] = useState(true)
const containerRef = useRef<HTMLDivElement>(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 (
<div
ref={containerRef}
data-component="playing-phase"
onClick={handleContainerClick}
className={css({
display: 'flex',
flexDirection: 'column',
@@ -130,41 +57,6 @@ export function PlayingPhase() {
position: 'relative',
})}
>
{/* Pointer Lock Prompt Overlay */}
{showLockPrompt && !pointerLocked && (
<div
data-element="pointer-lock-prompt"
className={css({
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
bg: isDark ? 'rgba(0, 0, 0, 0.9)' : 'rgba(255, 255, 255, 0.95)',
color: isDark ? 'white' : 'gray.900',
padding: '8',
rounded: 'xl',
border: '3px solid',
borderColor: 'blue.500',
boxShadow: '0 10px 40px rgba(0, 0, 0, 0.5)',
zIndex: 10000,
textAlign: 'center',
cursor: 'pointer',
transition: 'all 0.2s',
_hover: {
transform: 'translate(-50%, -50%) scale(1.05)',
borderColor: 'blue.400',
},
})}
>
<div className={css({ fontSize: '4xl', marginBottom: '4' })}>🎯</div>
<div className={css({ fontSize: 'xl', fontWeight: 'bold', marginBottom: '2' })}>
Enable Precision Controls
</div>
<div className={css({ fontSize: 'sm', color: isDark ? 'gray.400' : 'gray.600' })}>
Click anywhere to lock cursor and enable precise clicking on tiny regions
</div>
</div>
)}
{/* Current Prompt */}
<div
data-section="current-prompt"
@@ -283,7 +175,6 @@ export function PlayingPhase() {
onRegionClick={clickRegion}
guessHistory={state.guessHistory}
playerMetadata={state.playerMetadata}
pointerLocked={pointerLocked}
/>
{/* Game Mode Info */}