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:
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
Reference in New Issue
Block a user