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 e2d0708f..1b345be5 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
@@ -7,11 +7,9 @@ import { useTheme } from '@/contexts/ThemeContext'
import { useVisualDebugSafe } from '@/contexts/VisualDebugContext'
import type { ContinentId } from '../continents'
import { usePulsingAnimation } from '../features/animations'
-import { CompassCrosshair, CursorOverlay } from '../features/cursor'
+import { CursorOverlay } from '../features/cursor'
import { DebugAutoZoomPanel, HotColdDebugPanel, SafeZoneDebugPanel } from '../features/debug'
-import { MapRendererProvider, type MapRendererContextValue } from '../features/map-renderer'
-import { OtherPlayerCursors } from '../features/multiplayer'
-import { RegionLayer } from '../features/regions'
+import { useInteractionStateMachine } from '../features/interaction'
import { getRenderedViewport, LabelLayer, useD3ForceLabels } from '../features/labels'
import {
applyPanDelta,
@@ -30,7 +28,10 @@ import {
useMagnifierState,
ZoomLines,
} from '../features/magnifier'
+import { type MapRendererContextValue, MapRendererProvider } from '../features/map-renderer'
+import { OtherPlayerCursors } from '../features/multiplayer'
import { usePrecisionCalculations } from '../features/precision'
+import { RegionLayer } from '../features/regions'
import {
useCanUsePrecisionMode,
useHasAnyFinePointer,
@@ -60,7 +61,6 @@ import { useKnowYourWorld } from '../Provider'
import type { MapData, MapRegion } from '../types'
import { type BoundingBox as DebugBoundingBox, findOptimalZoom } from '../utils/adaptiveZoomSearch'
import { CELEBRATION_TIMING, classifyCelebration } from '../utils/celebration'
-import type { FeedbackType } from '../utils/hotColdPhrases'
import { getHeatBorderColors, getHeatCrosshairStyle } from '../utils/hotColdStyles'
import {
calculateMaxZoomAtThreshold,
@@ -341,6 +341,10 @@ export function MapRenderer({
const canUsePrecisionMode = useCanUsePrecisionMode() // For precision mode UI/behavior
const hasAnyFinePointer = useHasAnyFinePointer() // For hot/cold feedback visibility
+ // Interaction state machine - replaces scattered boolean flags with explicit states
+ // Currently running in parallel with existing state for incremental migration
+ const interactionMachine = useInteractionStateMachine()
+
// Memoize pointer lock callbacks to prevent render loop
const handleLockAcquired = useCallback(() => {
// Save initial cursor position
@@ -470,6 +474,67 @@ export function MapRenderer({
}
}, [isMagnifierExpanded, targetZoom])
+ // ==========================================================================
+ // State Machine Sync Effects (for incremental migration)
+ // These effects drive the state machine from existing boolean state changes.
+ // Once migration is complete, handlers will dispatch directly to the machine.
+ // ==========================================================================
+
+ // Sync precision mode state
+ useEffect(() => {
+ if (pointerLocked) {
+ interactionMachine.send({ type: 'REQUEST_PRECISION' })
+ } else if (isReleasingPointerLock) {
+ interactionMachine.send({ type: 'PRECISION_ESCAPE_BOUNDARY' })
+ }
+ }, [pointerLocked, isReleasingPointerLock, interactionMachine])
+
+ // Sync magnifier visibility
+ useEffect(() => {
+ if (showMagnifier) {
+ interactionMachine.send({ type: 'SHOW_MAGNIFIER' })
+ } else {
+ interactionMachine.send({ type: 'DISMISS_MAGNIFIER' })
+ }
+ }, [showMagnifier, interactionMachine])
+
+ // Sync magnifier expanded state
+ useEffect(() => {
+ if (isMagnifierExpanded) {
+ interactionMachine.send({ type: 'EXPAND_MAGNIFIER' })
+ } else {
+ interactionMachine.send({ type: 'COLLAPSE_MAGNIFIER' })
+ }
+ }, [isMagnifierExpanded, interactionMachine])
+
+ // Debug: Log state machine state changes and verify sync (only in dev)
+ useEffect(() => {
+ if (process.env.NODE_ENV === 'development') {
+ // Compare state machine derived values with existing boolean state
+ const machineShowMagnifier = interactionMachine.showMagnifier
+ const machineIsPrecision = interactionMachine.isPrecisionMode
+ const machineIsReleasingPrecision = interactionMachine.isReleasingPrecision
+
+ // Log state transitions with comparison
+ console.log('[StateMachine]', {
+ state: interactionMachine.state,
+ machine: { showMagnifier: machineShowMagnifier, isPrecision: machineIsPrecision },
+ boolean: { showMagnifier, pointerLocked, isReleasingPointerLock },
+ })
+
+ // Warn on sync divergence (helps identify missing sync effects)
+ // Note: Divergence is expected during transitions due to effect timing
+ }
+ }, [
+ interactionMachine.state,
+ interactionMachine.showMagnifier,
+ interactionMachine.isPrecisionMode,
+ interactionMachine.isReleasingPrecision,
+ showMagnifier,
+ pointerLocked,
+ isReleasingPointerLock,
+ ])
+
// Give up reveal animation state
const [giveUpFlashProgress, setGiveUpFlashProgress] = useState(0) // 0-1 pulsing value
const [isGiveUpAnimating, setIsGiveUpAnimating] = useState(false) // Track if animation in progress
@@ -2859,41 +2924,41 @@ export function MapRenderer({
-
`scale(${s}) translate(${tx / s}px, ${ty / s}px)`
- ),
+ backgroundSize: '100px 100px',
}}
>
- {/* Render all regions (included + excluded) */}
- `scale(${s}) translate(${tx / s}px, ${ty / s}px)`
+ ),
+ }}
+ >
+ {/* Render all regions (included + excluded) */}
+
+
+ {/* Debug: Render bounding boxes (only if enabled) */}
+ {effectiveShowDebugBoundingBoxes &&
+ debugBoundingBoxes.map((bbox) => {
+ // Color based on acceptance and importance
+ // Green = accepted, Orange = high importance, Yellow = medium, Gray = low
+ const importance = bbox.importance ?? 0
+ let strokeColor = '#888888' // Default gray for low importance
+ let fillColor = 'rgba(136, 136, 136, 0.1)'
+
+ if (bbox.wasAccepted) {
+ strokeColor = '#00ff00' // Green for accepted region
+ fillColor = 'rgba(0, 255, 0, 0.15)'
+ } else if (importance > 1.5) {
+ strokeColor = '#ff6600' // Orange for high importance (2.0× boost + close)
+ fillColor = 'rgba(255, 102, 0, 0.1)'
+ } else if (importance > 0.5) {
+ strokeColor = '#ffcc00' // Yellow for medium importance
+ fillColor = 'rgba(255, 204, 0, 0.1)'
+ }
+
+ return (
+
+
+
+ )
+ })}
+
+ {/* Arrow marker definition */}
+
+
+
+
+
+
+
+
+ {/* Player emoji patterns for region backgrounds */}
+ {Object.values(playerMetadata).map((player) => (
+
+
+
+ {player.emoji}
+
+
+ ))}
+
+
+ {/* Magnifier region indicator on main map */}
+ {showMagnifier && cursorPosition && svgRef.current && containerRef.current && (
+ {
+ const containerRect = containerRef.current!.getBoundingClientRect()
+ const svgRect = svgRef.current!.getBoundingClientRect()
+ // Account for preserveAspectRatio letterboxing
+ const viewport = getRenderedViewport(
+ svgRect,
+ parsedViewBox.x,
+ parsedViewBox.y,
+ parsedViewBox.width,
+ parsedViewBox.height
+ )
+ const svgOffsetX = svgRect.left - containerRect.left + viewport.letterboxX
+ const cursorSvgX =
+ (cursorPosition.x - svgOffsetX) / viewport.scale + parsedViewBox.x
+ // Calculate leftover dimensions for magnifier sizing
+ const leftoverW =
+ containerRect.width - SAFE_ZONE_MARGINS.left - SAFE_ZONE_MARGINS.right
+ const leftoverH =
+ containerRect.height - SAFE_ZONE_MARGINS.top - SAFE_ZONE_MARGINS.bottom
+ const { width: magnifiedWidth } = getAdjustedMagnifiedDimensions(
+ parsedViewBox.width,
+ parsedViewBox.height,
+ zoom,
+ leftoverW,
+ leftoverH
+ )
+ return cursorSvgX - magnifiedWidth / 2
+ })}
+ y={zoomSpring.to((zoom: number) => {
+ const containerRect = containerRef.current!.getBoundingClientRect()
+ const svgRect = svgRef.current!.getBoundingClientRect()
+ // Account for preserveAspectRatio letterboxing
+ const viewport = getRenderedViewport(
+ svgRect,
+ parsedViewBox.x,
+ parsedViewBox.y,
+ parsedViewBox.width,
+ parsedViewBox.height
+ )
+ const svgOffsetY = svgRect.top - containerRect.top + viewport.letterboxY
+ const cursorSvgY =
+ (cursorPosition.y - svgOffsetY) / viewport.scale + parsedViewBox.y
+ // Calculate leftover dimensions for magnifier sizing
+ const leftoverW =
+ containerRect.width - SAFE_ZONE_MARGINS.left - SAFE_ZONE_MARGINS.right
+ const leftoverH =
+ containerRect.height - SAFE_ZONE_MARGINS.top - SAFE_ZONE_MARGINS.bottom
+ const { height: magnifiedHeight } = getAdjustedMagnifiedDimensions(
+ parsedViewBox.width,
+ parsedViewBox.height,
+ zoom,
+ leftoverW,
+ leftoverH
+ )
+ return cursorSvgY - magnifiedHeight / 2
+ })}
+ width={zoomSpring.to((zoom: number) => {
+ const containerRect = containerRef.current!.getBoundingClientRect()
+ // Calculate leftover dimensions for magnifier sizing
+ const leftoverW =
+ containerRect.width - SAFE_ZONE_MARGINS.left - SAFE_ZONE_MARGINS.right
+ const leftoverH =
+ containerRect.height - SAFE_ZONE_MARGINS.top - SAFE_ZONE_MARGINS.bottom
+ const { width } = getAdjustedMagnifiedDimensions(
+ parsedViewBox.width,
+ parsedViewBox.height,
+ zoom,
+ leftoverW,
+ leftoverH
+ )
+ return width
+ })}
+ height={zoomSpring.to((zoom: number) => {
+ const containerRect = containerRef.current!.getBoundingClientRect()
+ // Calculate leftover dimensions for magnifier sizing
+ const leftoverW =
+ containerRect.width - SAFE_ZONE_MARGINS.left - SAFE_ZONE_MARGINS.right
+ const leftoverH =
+ containerRect.height - SAFE_ZONE_MARGINS.top - SAFE_ZONE_MARGINS.bottom
+ const { height } = getAdjustedMagnifiedDimensions(
+ parsedViewBox.width,
+ parsedViewBox.height,
+ zoom,
+ leftoverW,
+ leftoverH
+ )
+ return height
+ })}
+ fill="none"
+ stroke={isDark ? '#60a5fa' : '#3b82f6'}
+ strokeWidth={parsedViewBox.width / 500}
+ vectorEffect="non-scaling-stroke"
+ strokeDasharray="5,5"
+ pointerEvents="none"
+ opacity={0.8}
+ />
+ )}
+
+
+ {/* Labels for found regions - rendered via LabelLayer component */}
+
- {/* Debug: Render bounding boxes (only if enabled) */}
+ {/* Debug: Bounding box labels as HTML overlays */}
{effectiveShowDebugBoundingBoxes &&
+ containerRef.current &&
+ svgRef.current &&
debugBoundingBoxes.map((bbox) => {
- // Color based on acceptance and importance
- // Green = accepted, Orange = high importance, Yellow = medium, Gray = low
const importance = bbox.importance ?? 0
- let strokeColor = '#888888' // Default gray for low importance
- let fillColor = 'rgba(136, 136, 136, 0.1)'
+ let strokeColor = '#888888'
if (bbox.wasAccepted) {
- strokeColor = '#00ff00' // Green for accepted region
- fillColor = 'rgba(0, 255, 0, 0.15)'
+ strokeColor = '#00ff00'
} else if (importance > 1.5) {
- strokeColor = '#ff6600' // Orange for high importance (2.0× boost + close)
- fillColor = 'rgba(255, 102, 0, 0.1)'
+ strokeColor = '#ff6600'
} else if (importance > 0.5) {
- strokeColor = '#ffcc00' // Yellow for medium importance
- fillColor = 'rgba(255, 204, 0, 0.1)'
+ strokeColor = '#ffcc00'
}
+ // Convert SVG coordinates to pixel coordinates (accounting for preserveAspectRatio)
+ const containerRect = containerRef.current!.getBoundingClientRect()
+ const svgRect = svgRef.current!.getBoundingClientRect()
+
+ const viewport = getRenderedViewport(
+ svgRect,
+ parsedViewBox.x,
+ parsedViewBox.y,
+ parsedViewBox.width,
+ parsedViewBox.height
+ )
+ const svgOffsetX = svgRect.left - containerRect.left + viewport.letterboxX
+ const svgOffsetY = svgRect.top - containerRect.top + viewport.letterboxY
+
+ // Convert bbox center from SVG coords to pixels
+ const centerX =
+ (bbox.x + bbox.width / 2 - parsedViewBox.x) * viewport.scale + svgOffsetX
+ const centerY =
+ (bbox.y + bbox.height / 2 - parsedViewBox.y) * viewport.scale + svgOffsetY
+
return (
-
-
-
+
+
{bbox.regionId}
+
{importance.toFixed(2)}
+
)
})}
- {/* Arrow marker definition */}
-
-
-
-
-
-
-
+ {/* Custom cursor and heat crosshair overlays */}
+
- {/* Player emoji patterns for region backgrounds */}
- {Object.values(playerMetadata).map((player) => (
-
-
-
- {player.emoji}
-
-
- ))}
-
-
- {/* Magnifier region indicator on main map */}
- {showMagnifier && cursorPosition && svgRef.current && containerRef.current && (
-
{
- const containerRect = containerRef.current!.getBoundingClientRect()
- const svgRect = svgRef.current!.getBoundingClientRect()
- // Account for preserveAspectRatio letterboxing
- const viewport = getRenderedViewport(
- svgRect,
- parsedViewBox.x,
- parsedViewBox.y,
- parsedViewBox.width,
- parsedViewBox.height
- )
- const svgOffsetX = svgRect.left - containerRect.left + viewport.letterboxX
- const cursorSvgX = (cursorPosition.x - svgOffsetX) / viewport.scale + parsedViewBox.x
- // Calculate leftover dimensions for magnifier sizing
- const leftoverW =
- containerRect.width - SAFE_ZONE_MARGINS.left - SAFE_ZONE_MARGINS.right
- const leftoverH =
- containerRect.height - SAFE_ZONE_MARGINS.top - SAFE_ZONE_MARGINS.bottom
- const { width: magnifiedWidth } = getAdjustedMagnifiedDimensions(
- parsedViewBox.width,
- parsedViewBox.height,
- zoom,
- leftoverW,
- leftoverH
- )
- return cursorSvgX - magnifiedWidth / 2
- })}
- y={zoomSpring.to((zoom: number) => {
- const containerRect = containerRef.current!.getBoundingClientRect()
- const svgRect = svgRef.current!.getBoundingClientRect()
- // Account for preserveAspectRatio letterboxing
- const viewport = getRenderedViewport(
- svgRect,
- parsedViewBox.x,
- parsedViewBox.y,
- parsedViewBox.width,
- parsedViewBox.height
- )
- const svgOffsetY = svgRect.top - containerRect.top + viewport.letterboxY
- const cursorSvgY = (cursorPosition.y - svgOffsetY) / viewport.scale + parsedViewBox.y
- // Calculate leftover dimensions for magnifier sizing
- const leftoverW =
- containerRect.width - SAFE_ZONE_MARGINS.left - SAFE_ZONE_MARGINS.right
- const leftoverH =
- containerRect.height - SAFE_ZONE_MARGINS.top - SAFE_ZONE_MARGINS.bottom
- const { height: magnifiedHeight } = getAdjustedMagnifiedDimensions(
- parsedViewBox.width,
- parsedViewBox.height,
- zoom,
- leftoverW,
- leftoverH
- )
- return cursorSvgY - magnifiedHeight / 2
- })}
- width={zoomSpring.to((zoom: number) => {
- const containerRect = containerRef.current!.getBoundingClientRect()
- // Calculate leftover dimensions for magnifier sizing
- const leftoverW =
- containerRect.width - SAFE_ZONE_MARGINS.left - SAFE_ZONE_MARGINS.right
- const leftoverH =
- containerRect.height - SAFE_ZONE_MARGINS.top - SAFE_ZONE_MARGINS.bottom
- const { width } = getAdjustedMagnifiedDimensions(
- parsedViewBox.width,
- parsedViewBox.height,
- zoom,
- leftoverW,
- leftoverH
- )
- return width
- })}
- height={zoomSpring.to((zoom: number) => {
- const containerRect = containerRef.current!.getBoundingClientRect()
- // Calculate leftover dimensions for magnifier sizing
- const leftoverW =
- containerRect.width - SAFE_ZONE_MARGINS.left - SAFE_ZONE_MARGINS.right
- const leftoverH =
- containerRect.height - SAFE_ZONE_MARGINS.top - SAFE_ZONE_MARGINS.bottom
- const { height } = getAdjustedMagnifiedDimensions(
- parsedViewBox.width,
- parsedViewBox.height,
- zoom,
- leftoverW,
- leftoverH
- )
- return height
- })}
- fill="none"
- stroke={isDark ? '#60a5fa' : '#3b82f6'}
- strokeWidth={parsedViewBox.width / 500}
- vectorEffect="non-scaling-stroke"
- strokeDasharray="5,5"
- pointerEvents="none"
- opacity={0.8}
- />
- )}
-
-
- {/* Labels for found regions - rendered via LabelLayer component */}
-
-
- {/* Debug: Bounding box labels as HTML overlays */}
- {effectiveShowDebugBoundingBoxes &&
- containerRef.current &&
- svgRef.current &&
- debugBoundingBoxes.map((bbox) => {
- const importance = bbox.importance ?? 0
- let strokeColor = '#888888'
-
- if (bbox.wasAccepted) {
- strokeColor = '#00ff00'
- } else if (importance > 1.5) {
- strokeColor = '#ff6600'
- } else if (importance > 0.5) {
- strokeColor = '#ffcc00'
+ {/* Magnifier overlay - centers on cursor position */}
+ {(() => {
+ if (!cursorPosition || !svgRef.current || !containerRef.current) {
+ return null
}
- // Convert SVG coordinates to pixel coordinates (accounting for preserveAspectRatio)
- const containerRect = containerRef.current!.getBoundingClientRect()
- const svgRect = svgRef.current!.getBoundingClientRect()
+ // Calculate magnifier size based on leftover rectangle (area not covered by UI)
+ const containerRect = containerRef.current.getBoundingClientRect()
+ const leftoverWidth =
+ containerRect.width - SAFE_ZONE_MARGINS.left - SAFE_ZONE_MARGINS.right
+ const leftoverHeight =
+ containerRect.height - SAFE_ZONE_MARGINS.top - SAFE_ZONE_MARGINS.bottom
+ // When expanded (during/after pinch-to-zoom), use full leftover area
+ // Otherwise use the normal calculated dimensions
+ const { width: normalWidth, height: normalHeight } = getMagnifierDimensions(
+ leftoverWidth,
+ leftoverHeight
+ )
+ const magnifierWidthPx = isMagnifierExpanded ? leftoverWidth : normalWidth
+ const magnifierHeightPx = isMagnifierExpanded ? leftoverHeight : normalHeight
+
+ // Pre-compute SVG coordinate transformation for crosshair and pixel grid
+ const svgRect = svgRef.current.getBoundingClientRect()
const viewport = getRenderedViewport(
svgRect,
parsedViewBox.x,
@@ -3192,402 +3342,329 @@ export function MapRenderer({
)
const svgOffsetX = svgRect.left - containerRect.left + viewport.letterboxX
const svgOffsetY = svgRect.top - containerRect.top + viewport.letterboxY
+ const cursorSvgX = (cursorPosition.x - svgOffsetX) / viewport.scale + parsedViewBox.x
+ const cursorSvgY = (cursorPosition.y - svgOffsetY) / viewport.scale + parsedViewBox.y
- // Convert bbox center from SVG coords to pixels
- const centerX = (bbox.x + bbox.width / 2 - parsedViewBox.x) * viewport.scale + svgOffsetX
- const centerY = (bbox.y + bbox.height / 2 - parsedViewBox.y) * viewport.scale + svgOffsetY
+ // Get heat-based crosshair styling
+ const heatStyle = getHeatCrosshairStyle(
+ hotColdFeedbackType,
+ isDark,
+ effectiveHotColdEnabled
+ )
return (
- {
+ // When hot/cold is enabled, use heat-based colors
+ if (effectiveHotColdEnabled && hotColdFeedbackType) {
+ const heatColors = getHeatBorderColors(hotColdFeedbackType, isDark)
+ return `${heatColors.width}px solid ${heatColors.border}`
+ }
+ // Fall back to zoom-based coloring
+ return zoomSpring.to(
+ (zoom: number) =>
+ zoom > HIGH_ZOOM_THRESHOLD
+ ? `4px solid ${isDark ? '#fbbf24' : '#f59e0b'}` // gold-400/gold-500
+ : `3px solid ${isDark ? '#60a5fa' : '#3b82f6'}` // blue-400/blue-600
+ )
+ })(),
+ borderRadius: '12px',
+ overflow: 'hidden',
+ // Enable touch events on mobile for panning, but keep mouse events disabled
+ // This allows touch-based panning while not interfering with mouse-based interactions
+ pointerEvents: 'auto',
+ touchAction: 'none', // Prevent browser handling of touch gestures
+ zIndex: 100,
+ // Box shadow with heat glow when hot/cold is enabled
+ boxShadow: (() => {
+ if (effectiveHotColdEnabled && hotColdFeedbackType) {
+ const heatColors = getHeatBorderColors(hotColdFeedbackType, isDark)
+ return `0 10px 40px rgba(0, 0, 0, 0.3), 0 0 25px ${heatColors.glow}`
+ }
+ return zoomSpring.to((zoom: number) =>
+ zoom > HIGH_ZOOM_THRESHOLD
+ ? '0 10px 40px rgba(251, 191, 36, 0.4), 0 0 20px rgba(251, 191, 36, 0.2)' // Gold glow
+ : '0 10px 40px rgba(0, 0, 0, 0.5)'
+ )
+ })(),
+ background: isDark ? '#111827' : '#f3f4f6',
+ opacity: magnifierSpring.opacity,
}}
>
-
{bbox.regionId}
-
{importance.toFixed(2)}
-
- )
- })}
+ {
+ // Calculate magnified viewBox centered on cursor
+ const containerRect = containerRef.current!.getBoundingClientRect()
+ const svgRect = svgRef.current!.getBoundingClientRect()
- {/* Custom cursor and heat crosshair overlays */}
-
-
- {/* Magnifier overlay - centers on cursor position */}
- {(() => {
- if (!cursorPosition || !svgRef.current || !containerRef.current) {
- return null
- }
-
- // Calculate magnifier size based on leftover rectangle (area not covered by UI)
- const containerRect = containerRef.current.getBoundingClientRect()
- const leftoverWidth = containerRect.width - SAFE_ZONE_MARGINS.left - SAFE_ZONE_MARGINS.right
- const leftoverHeight =
- containerRect.height - SAFE_ZONE_MARGINS.top - SAFE_ZONE_MARGINS.bottom
-
- // When expanded (during/after pinch-to-zoom), use full leftover area
- // Otherwise use the normal calculated dimensions
- const { width: normalWidth, height: normalHeight } = getMagnifierDimensions(
- leftoverWidth,
- leftoverHeight
- )
- const magnifierWidthPx = isMagnifierExpanded ? leftoverWidth : normalWidth
- const magnifierHeightPx = isMagnifierExpanded ? leftoverHeight : normalHeight
-
- // Pre-compute SVG coordinate transformation for crosshair and pixel grid
- const svgRect = svgRef.current.getBoundingClientRect()
- const viewport = getRenderedViewport(
- svgRect,
- parsedViewBox.x,
- parsedViewBox.y,
- parsedViewBox.width,
- parsedViewBox.height
- )
- const svgOffsetX = svgRect.left - containerRect.left + viewport.letterboxX
- const svgOffsetY = svgRect.top - containerRect.top + viewport.letterboxY
- const cursorSvgX = (cursorPosition.x - svgOffsetX) / viewport.scale + parsedViewBox.x
- const cursorSvgY = (cursorPosition.y - svgOffsetY) / viewport.scale + parsedViewBox.y
-
- // Get heat-based crosshair styling
- const heatStyle = getHeatCrosshairStyle(
- hotColdFeedbackType,
- isDark,
- effectiveHotColdEnabled
- )
-
- return (
- {
- // When hot/cold is enabled, use heat-based colors
- if (effectiveHotColdEnabled && hotColdFeedbackType) {
- const heatColors = getHeatBorderColors(hotColdFeedbackType, isDark)
- return `${heatColors.width}px solid ${heatColors.border}`
- }
- // Fall back to zoom-based coloring
- return zoomSpring.to(
- (zoom: number) =>
- zoom > HIGH_ZOOM_THRESHOLD
- ? `4px solid ${isDark ? '#fbbf24' : '#f59e0b'}` // gold-400/gold-500
- : `3px solid ${isDark ? '#60a5fa' : '#3b82f6'}` // blue-400/blue-600
- )
- })(),
- borderRadius: '12px',
- overflow: 'hidden',
- // Enable touch events on mobile for panning, but keep mouse events disabled
- // This allows touch-based panning while not interfering with mouse-based interactions
- pointerEvents: 'auto',
- touchAction: 'none', // Prevent browser handling of touch gestures
- zIndex: 100,
- // Box shadow with heat glow when hot/cold is enabled
- boxShadow: (() => {
- if (effectiveHotColdEnabled && hotColdFeedbackType) {
- const heatColors = getHeatBorderColors(hotColdFeedbackType, isDark)
- return `0 10px 40px rgba(0, 0, 0, 0.3), 0 0 25px ${heatColors.glow}`
- }
- return zoomSpring.to((zoom: number) =>
- zoom > HIGH_ZOOM_THRESHOLD
- ? '0 10px 40px rgba(251, 191, 36, 0.4), 0 0 20px rgba(251, 191, 36, 0.2)' // Gold glow
- : '0 10px 40px rgba(0, 0, 0, 0.5)'
- )
- })(),
- background: isDark ? '#111827' : '#f3f4f6',
- opacity: magnifierSpring.opacity,
- }}
- >
- {
- // Calculate magnified viewBox centered on cursor
- const containerRect = containerRef.current!.getBoundingClientRect()
- const svgRect = svgRef.current!.getBoundingClientRect()
-
- // Use memoized parsedViewBox for coordinate conversion
- const viewport = getRenderedViewport(
- svgRect,
- parsedViewBox.x,
- parsedViewBox.y,
- parsedViewBox.width,
- parsedViewBox.height
- )
-
- // Center position relative to SVG (uses reveal center during give-up animation)
- const svgOffsetX = svgRect.left - containerRect.left + viewport.letterboxX
- const svgOffsetY = svgRect.top - containerRect.top + viewport.letterboxY
- const cursorSvgX =
- (cursorPosition.x - svgOffsetX) / viewport.scale + parsedViewBox.x
- const cursorSvgY =
- (cursorPosition.y - svgOffsetY) / viewport.scale + parsedViewBox.y
-
- // Magnified view: adjust dimensions to match magnifier container aspect ratio
- // This eliminates letterboxing and ensures outline matches what's visible
- // Use leftover dimensions for magnifier sizing
- const leftoverW =
- containerRect.width - SAFE_ZONE_MARGINS.left - SAFE_ZONE_MARGINS.right
- const leftoverH =
- containerRect.height - SAFE_ZONE_MARGINS.top - SAFE_ZONE_MARGINS.bottom
- const { width: magnifiedWidth, height: magnifiedHeight } =
- getAdjustedMagnifiedDimensions(
+ // Use memoized parsedViewBox for coordinate conversion
+ const viewport = getRenderedViewport(
+ svgRect,
+ parsedViewBox.x,
+ parsedViewBox.y,
parsedViewBox.width,
- parsedViewBox.height,
- zoom,
- leftoverW,
- leftoverH
+ parsedViewBox.height
)
- // Center the magnified viewBox on the cursor
- const magnifiedViewBoxX = cursorSvgX - magnifiedWidth / 2
- const magnifiedViewBoxY = cursorSvgY - magnifiedHeight / 2
+ // Center position relative to SVG (uses reveal center during give-up animation)
+ const svgOffsetX = svgRect.left - containerRect.left + viewport.letterboxX
+ const svgOffsetY = svgRect.top - containerRect.top + viewport.letterboxY
+ const cursorSvgX =
+ (cursorPosition.x - svgOffsetX) / viewport.scale + parsedViewBox.x
+ const cursorSvgY =
+ (cursorPosition.y - svgOffsetY) / viewport.scale + parsedViewBox.y
- return `${magnifiedViewBoxX} ${magnifiedViewBoxY} ${magnifiedWidth} ${magnifiedHeight}`
- })}
- style={{
- width: '100%',
- height: '100%',
- // Apply "disabled" visual effect when at threshold but not in precision mode
- // Uses precisionCalcs.isAtThreshold from usePrecisionCalculations hook
- filter:
- precisionCalcs.isAtThreshold && !pointerLocked
- ? 'brightness(0.6) saturate(0.5)'
- : 'none',
- }}
- >
- {/* Sea/ocean background for magnifier - solid color to match container */}
-
+ // Magnified view: adjust dimensions to match magnifier container aspect ratio
+ // This eliminates letterboxing and ensures outline matches what's visible
+ // Use leftover dimensions for magnifier sizing
+ const leftoverW =
+ containerRect.width - SAFE_ZONE_MARGINS.left - SAFE_ZONE_MARGINS.right
+ const leftoverH =
+ containerRect.height - SAFE_ZONE_MARGINS.top - SAFE_ZONE_MARGINS.bottom
+ const { width: magnifiedWidth, height: magnifiedHeight } =
+ getAdjustedMagnifiedDimensions(
+ parsedViewBox.width,
+ parsedViewBox.height,
+ zoom,
+ leftoverW,
+ leftoverH
+ )
- {/* Render all regions in magnified view */}
-
+ >
+ {/* Sea/ocean background for magnifier - solid color to match container */}
+
- {/* Crosshair at center position (cursor or reveal center during animation) */}
-
+ {/* Render all regions in magnified view */}
+
- {/* Pixel grid overlay - shows when approaching/at/above precision mode threshold */}
- {/* Uses precisionCalcs.screenPixelRatio from usePrecisionCalculations hook */}
-
+ {/* Crosshair at center position (cursor or reveal center during animation) */}
+
- {/* Debug: Bounding boxes for detected regions in magnifier */}
-
+
+ {/* Debug: Bounding boxes for detected regions in magnifier */}
+
+
+
+ {/* Debug: Bounding box labels as HTML overlays - positioned using animated values */}
+
-
- {/* Debug: Bounding box labels as HTML overlays - positioned using animated values */}
-
-
- {/* Magnifier label */}
-
-
- {/* Scrim overlay - shows when at threshold to indicate barrier */}
- {/* Uses precisionCalcs.isAtThreshold from usePrecisionCalculations hook */}
- {precisionCalcs.isAtThreshold && !pointerLocked && (
-
- )}
- {/* Mobile magnifier controls (Expand, Select, Full Map buttons) */}
- setIsMagnifierExpanded(false)}
- onExpand={() => setIsMagnifierExpanded(true)}
- />
-
- )
- })()}
+ {/* Scrim overlay - shows when at threshold to indicate barrier */}
+ {/* Uses precisionCalcs.isAtThreshold from usePrecisionCalculations hook */}
+ {precisionCalcs.isAtThreshold && !pointerLocked && (
+
+ )}
- {/* Zoom lines connecting indicator to magnifier - creates "pop out" effect */}
- {showMagnifier && cursorPosition && svgRef.current && containerRef.current && (
- setIsMagnifierExpanded(false)}
+ onExpand={() => setIsMagnifierExpanded(true)}
+ />
+
+ )
+ })()}
+
+ {/* Zoom lines connecting indicator to magnifier - creates "pop out" effect */}
+ {showMagnifier && cursorPosition && svgRef.current && containerRef.current && (
+
+ )}
+
+ {/* Debug: Auto zoom detection visualization (dev only) */}
+
- )}
- {/* Debug: Auto zoom detection visualization (dev only) */}
-
-
- {/* Hot/Cold Debug Panel - shows enable conditions and current state */}
-
-
- {/* Other players' cursors - show in multiplayer when not exclusively our turn */}
-
-
- {/* Dev-only crop tool for getting custom viewBox coordinates */}
-
-
- {/* Debug overlay showing safe zone rectangles */}
- {/* Safe Zone Debug Panel */}
-
-
- {/* Celebration overlay - shows confetti and sound when region is found */}
- {celebration && (
-
- )}
+
+ {/* Other players' cursors - show in multiplayer when not exclusively our turn */}
+
+
+ {/* Dev-only crop tool for getting custom viewBox coordinates */}
+
+
+ {/* Debug overlay showing safe zone rectangles */}
+ {/* Safe Zone Debug Panel */}
+
+
+ {/* Celebration overlay - shows confetti and sound when region is found */}
+ {celebration && (
+
+ )}
)
diff --git a/apps/web/src/arcade-games/know-your-world/features/interaction/index.ts b/apps/web/src/arcade-games/know-your-world/features/interaction/index.ts
index c24080fd..ae6cebca 100644
--- a/apps/web/src/arcade-games/know-your-world/features/interaction/index.ts
+++ b/apps/web/src/arcade-games/know-your-world/features/interaction/index.ts
@@ -6,11 +6,11 @@
*/
export type {
- InteractionState,
- InteractionEvent,
InteractionContext,
- TouchPoint,
+ InteractionEvent,
+ InteractionState,
MachineState,
+ TouchPoint,
UseInteractionStateMachineReturn,
} from './useInteractionStateMachine'
diff --git a/apps/web/src/arcade-games/know-your-world/features/interaction/useInteractionStateMachine.ts b/apps/web/src/arcade-games/know-your-world/features/interaction/useInteractionStateMachine.ts
index 11bd3f92..42801515 100644
--- a/apps/web/src/arcade-games/know-your-world/features/interaction/useInteractionStateMachine.ts
+++ b/apps/web/src/arcade-games/know-your-world/features/interaction/useInteractionStateMachine.ts
@@ -10,7 +10,7 @@
'use client'
-import { useReducer, useCallback, useRef, useMemo } from 'react'
+import { useCallback, useMemo, useReducer, useRef } from 'react'
// ============================================================================
// State Types
@@ -680,10 +680,7 @@ export function useInteractionStateMachine(): UseInteractionStateMachineReturn {
]
)
- const showCursor = useMemo(
- () => isHovering || showMagnifier,
- [isHovering, showMagnifier]
- )
+ const showCursor = useMemo(() => isHovering || showMagnifier, [isHovering, showMagnifier])
const isAnyPanning = useMemo(
() => isMagnifierPanning || isMapPanningMobile || isMapPanningDesktop,
diff --git a/apps/web/src/arcade-games/know-your-world/features/labels/useD3ForceLabels.ts b/apps/web/src/arcade-games/know-your-world/features/labels/useD3ForceLabels.ts
index 1a3b302e..4914da10 100644
--- a/apps/web/src/arcade-games/know-your-world/features/labels/useD3ForceLabels.ts
+++ b/apps/web/src/arcade-games/know-your-world/features/labels/useD3ForceLabels.ts
@@ -9,7 +9,7 @@
*/
import { forceCollide, forceSimulation, forceX, forceY, type SimulationNodeDatum } from 'd3-force'
-import { RefObject, useEffect, useState } from 'react'
+import { type RefObject, useEffect, useState } from 'react'
import type { MapData, MapRegion } from '../../types'
import { getArrowStartPoint, getRenderedViewport } from './labelUtils'
diff --git a/apps/web/src/arcade-games/know-your-world/features/letter-confirmation/LetterDisplay.tsx b/apps/web/src/arcade-games/know-your-world/features/letter-confirmation/LetterDisplay.tsx
index d3b1a212..c1f3e0e8 100644
--- a/apps/web/src/arcade-games/know-your-world/features/letter-confirmation/LetterDisplay.tsx
+++ b/apps/web/src/arcade-games/know-your-world/features/letter-confirmation/LetterDisplay.tsx
@@ -90,15 +90,7 @@ export const LetterDisplay = memo(function LetterDisplay({
const isSpace = char === ' '
if (isSpace) {
- return (
-
- )
+ return
}
// Get current index before incrementing
@@ -113,15 +105,7 @@ export const LetterDisplay = memo(function LetterDisplay({
isComplete
)
- return (
-
- )
+ return
})
}, [regionName, confirmedCount, requiredLetters, isComplete, isDark])
diff --git a/apps/web/src/arcade-games/know-your-world/features/letter-confirmation/letterUtils.ts b/apps/web/src/arcade-games/know-your-world/features/letter-confirmation/letterUtils.ts
index 97bbc9c7..4d2a4cc3 100644
--- a/apps/web/src/arcade-games/know-your-world/features/letter-confirmation/letterUtils.ts
+++ b/apps/web/src/arcade-games/know-your-world/features/letter-confirmation/letterUtils.ts
@@ -89,10 +89,7 @@ export function getLetterStatus(
* @param isDark - Whether dark mode is active
* @returns CSS properties for the letter
*/
-export function getLetterStyles(
- status: LetterStatus,
- isDark: boolean
-): React.CSSProperties {
+export function getLetterStyles(status: LetterStatus, isDark: boolean): React.CSSProperties {
const baseStyles: React.CSSProperties = {
transition: 'all 0.15s ease-out',
}
@@ -129,10 +126,7 @@ export function getLetterStyles(
* @param requiredLetters - Number of letters required
* @returns Progress value (0 = none, 1 = complete)
*/
-export function calculateProgress(
- confirmedCount: number,
- requiredLetters: number
-): number {
+export function calculateProgress(confirmedCount: number, requiredLetters: number): number {
if (requiredLetters === 0) return 1
return Math.min(1, confirmedCount / requiredLetters)
}
diff --git a/apps/web/src/arcade-games/know-your-world/features/letter-confirmation/useLetterConfirmation.ts b/apps/web/src/arcade-games/know-your-world/features/letter-confirmation/useLetterConfirmation.ts
index 70d78c7f..c77f152c 100644
--- a/apps/web/src/arcade-games/know-your-world/features/letter-confirmation/useLetterConfirmation.ts
+++ b/apps/web/src/arcade-games/know-your-world/features/letter-confirmation/useLetterConfirmation.ts
@@ -91,12 +91,7 @@ export function useLetterConfirmation({
// Get letter status for display
const getLetterStatus = useCallback(
(nonSpaceIndex: number): LetterStatus => {
- return getLetterStatusUtil(
- nonSpaceIndex,
- confirmedCount,
- requiredLetters,
- isComplete
- )
+ return getLetterStatusUtil(nonSpaceIndex, confirmedCount, requiredLetters, isComplete)
},
[confirmedCount, requiredLetters, isComplete]
)
@@ -109,10 +104,7 @@ export function useLetterConfirmation({
const handleKeyDown = (e: KeyboardEvent) => {
// Ignore if typing in an input or textarea
- if (
- e.target instanceof HTMLInputElement ||
- e.target instanceof HTMLTextAreaElement
- ) {
+ if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
return
}
diff --git a/apps/web/src/arcade-games/know-your-world/features/magnifier/MagnifierControls.tsx b/apps/web/src/arcade-games/know-your-world/features/magnifier/MagnifierControls.tsx
index 9f89965a..329adf35 100644
--- a/apps/web/src/arcade-games/know-your-world/features/magnifier/MagnifierControls.tsx
+++ b/apps/web/src/arcade-games/know-your-world/features/magnifier/MagnifierControls.tsx
@@ -211,11 +211,7 @@ export const MagnifierControls = memo(function MagnifierControls({
<>
{/* Expand button - top-right corner (when not expanded and not pointer locked) */}
{!isExpanded && !pointerLocked && (
-
+
)}
@@ -234,11 +230,7 @@ export const MagnifierControls = memo(function MagnifierControls({
{/* Full Map button - bottom-left corner (when expanded) */}
{isExpanded && showSelectButton && (
-
+
Full Map
)}
diff --git a/apps/web/src/arcade-games/know-your-world/features/magnifier/MagnifierRegions.tsx b/apps/web/src/arcade-games/know-your-world/features/magnifier/MagnifierRegions.tsx
index 7e35442f..61b2ebf6 100644
--- a/apps/web/src/arcade-games/know-your-world/features/magnifier/MagnifierRegions.tsx
+++ b/apps/web/src/arcade-games/know-your-world/features/magnifier/MagnifierRegions.tsx
@@ -50,7 +50,12 @@ export interface MagnifierRegionsProps {
/** Get player ID who found a region */
getPlayerWhoFoundRegion: (regionId: string) => string | null
/** Get region fill color */
- getRegionColor: (regionId: string, isFound: boolean, isHovered: boolean, isDark: boolean) => string
+ getRegionColor: (
+ regionId: string,
+ isFound: boolean,
+ isHovered: boolean,
+ isDark: boolean
+ ) => string
/** Get region stroke color */
getRegionStroke: (isFound: boolean, isDark: boolean) => string
/** Whether to show region outline */
@@ -80,13 +85,8 @@ export const MagnifierRegions = memo(function MagnifierRegions({
getRegionStroke,
showOutline,
}: MagnifierRegionsProps) {
- const {
- regionsFound,
- hoveredRegion,
- celebrationRegionId,
- giveUpRegionId,
- isGiveUpAnimating,
- } = regionState
+ const { regionsFound, hoveredRegion, celebrationRegionId, giveUpRegionId, isGiveUpAnimating } =
+ regionState
const { celebrationFlash, giveUpFlash } = flashProgress
return (
diff --git a/apps/web/src/arcade-games/know-your-world/features/magnifier/ZoomLines.tsx b/apps/web/src/arcade-games/know-your-world/features/magnifier/ZoomLines.tsx
index 3e13ad71..46ecdc7b 100644
--- a/apps/web/src/arcade-games/know-your-world/features/magnifier/ZoomLines.tsx
+++ b/apps/web/src/arcade-games/know-your-world/features/magnifier/ZoomLines.tsx
@@ -16,7 +16,10 @@
import { memo, useMemo } from 'react'
import { getRenderedViewport } from '../labels'
-import { getAdjustedMagnifiedDimensions, getMagnifierDimensions } from '../../utils/magnifierDimensions'
+import {
+ getAdjustedMagnifiedDimensions,
+ getMagnifierDimensions,
+} from '../../utils/magnifierDimensions'
// ============================================================================
// Types
@@ -124,12 +127,7 @@ export const ZoomLines = memo(function ZoomLines({
isDark,
}: ZoomLinesProps) {
// Memoize all the calculated values
- const {
- paths,
- visibleCorners,
- lineColor,
- glowColor,
- } = useMemo(() => {
+ const { paths, visibleCorners, lineColor, glowColor } = useMemo(() => {
// Calculate leftover rectangle dimensions (area not covered by UI elements)
const leftoverWidth = containerRect.width - safeZoneMargins.left - safeZoneMargins.right
const leftoverHeight = containerRect.height - safeZoneMargins.top - safeZoneMargins.bottom
@@ -210,14 +208,7 @@ export const ZoomLines = memo(function ZoomLines({
magTop + magnifierHeight
)
// Check if line passes through indicator
- const passesThroughInd = linePassesThroughRect(
- from,
- to,
- indTL.x,
- indTL.y,
- indBR.x,
- indBR.y
- )
+ const passesThroughInd = linePassesThroughRect(from, to, indTL.x, indTL.y, indBR.x, indBR.y)
return !passesThroughMag && !passesThroughInd
})
@@ -233,9 +224,7 @@ export const ZoomLines = memo(function ZoomLines({
: isDark
? '#60a5fa'
: '#3b82f6' // blue
- const calculatedGlowColor = isHighZoom
- ? 'rgba(251, 191, 36, 0.6)'
- : 'rgba(96, 165, 250, 0.6)'
+ const calculatedGlowColor = isHighZoom ? 'rgba(251, 191, 36, 0.6)' : 'rgba(96, 165, 250, 0.6)'
return {
paths: calculatedPaths,
diff --git a/apps/web/src/arcade-games/know-your-world/features/magnifier/useMagnifierTouch.ts b/apps/web/src/arcade-games/know-your-world/features/magnifier/useMagnifierTouch.ts
index 151d0a19..cb60af10 100644
--- a/apps/web/src/arcade-games/know-your-world/features/magnifier/useMagnifierTouch.ts
+++ b/apps/web/src/arcade-games/know-your-world/features/magnifier/useMagnifierTouch.ts
@@ -12,7 +12,12 @@
'use client'
-import React, { useCallback, useRef, type RefObject, type TouchEvent as ReactTouchEvent } from 'react'
+import React, {
+ useCallback,
+ useRef,
+ type RefObject,
+ type TouchEvent as ReactTouchEvent,
+} from 'react'
import type { UseMagnifierStateReturn } from './useMagnifierState'
@@ -223,8 +228,10 @@ export function useMagnifierTouch(options: UseMagnifierTouchOptions): UseMagnifi
totalDeltaRef.current.y += deltaY
// Track if user has moved significantly
- if (Math.abs(totalDeltaRef.current.x) > moveThreshold ||
- Math.abs(totalDeltaRef.current.y) > moveThreshold) {
+ if (
+ Math.abs(totalDeltaRef.current.x) > moveThreshold ||
+ Math.abs(totalDeltaRef.current.y) > moveThreshold
+ ) {
magnifierState.didMoveRef.current = true
}
diff --git a/apps/web/src/arcade-games/know-your-world/features/shared/MapRendererContext.tsx b/apps/web/src/arcade-games/know-your-world/features/shared/MapRendererContext.tsx
index dce28a77..2c9e63b5 100644
--- a/apps/web/src/arcade-games/know-your-world/features/shared/MapRendererContext.tsx
+++ b/apps/web/src/arcade-games/know-your-world/features/shared/MapRendererContext.tsx
@@ -103,9 +103,7 @@ export function MapRendererProvider({ children, value }: MapRendererProviderProp
]
)
- return (
- {children}
- )
+ return {children}
}
// ============================================================================
diff --git a/apps/web/src/arcade-games/know-your-world/features/shared/types.ts b/apps/web/src/arcade-games/know-your-world/features/shared/types.ts
index 3f97a2cf..1621c617 100644
--- a/apps/web/src/arcade-games/know-your-world/features/shared/types.ts
+++ b/apps/web/src/arcade-games/know-your-world/features/shared/types.ts
@@ -177,14 +177,7 @@ export interface FlashProgress {
/**
* Hot/cold feedback type for visual indicators
*/
-export type HotColdFeedbackType =
- | 'freezing'
- | 'cold'
- | 'cool'
- | 'warm'
- | 'hot'
- | 'burning'
- | null
+export type HotColdFeedbackType = 'freezing' | 'cold' | 'cool' | 'warm' | 'hot' | 'burning' | null
/**
* Heat-based styling for borders and glows
diff --git a/apps/web/src/arcade-games/know-your-world/features/shared/viewportUtils.ts b/apps/web/src/arcade-games/know-your-world/features/shared/viewportUtils.ts
index 46539692..e95473d0 100644
--- a/apps/web/src/arcade-games/know-your-world/features/shared/viewportUtils.ts
+++ b/apps/web/src/arcade-games/know-your-world/features/shared/viewportUtils.ts
@@ -96,13 +96,7 @@ export function screenToSVG(
svgRect: DOMRect,
viewBox: ViewBoxComponents
): SVGPosition {
- const viewport = getRenderedViewport(
- svgRect,
- viewBox.x,
- viewBox.y,
- viewBox.width,
- viewBox.height
- )
+ const viewport = getRenderedViewport(svgRect, viewBox.x, viewBox.y, viewBox.width, viewBox.height)
// Calculate offset from container origin to SVG rendered content
const svgOffsetX = svgRect.left - containerRect.left + viewport.letterboxX
@@ -130,13 +124,7 @@ export function svgToScreen(
svgRect: DOMRect,
viewBox: ViewBoxComponents
): CursorPosition {
- const viewport = getRenderedViewport(
- svgRect,
- viewBox.x,
- viewBox.y,
- viewBox.width,
- viewBox.height
- )
+ const viewport = getRenderedViewport(svgRect, viewBox.x, viewBox.y, viewBox.width, viewBox.height)
// Calculate offset from container origin to SVG rendered content
const svgOffsetX = svgRect.left - containerRect.left + viewport.letterboxX