Bug fixes:
1. Celebration/confetti timers would restart when mouse moved during animation
- Root cause: onComplete callback in useEffect dependency array
- Fix: Store callback in ref to prevent timer restart on re-render
- Fixed in: Confetti.tsx (both components), CelebrationOverlay.tsx
2. Mobile magnifier would dismiss on every other drag release
- Root cause: handleMapTouchEnd only checked map dragging, not magnifier dragging
- Fix: Also check isMagnifierDragging and isPinching before dismissing
- Touch events can escape magnifier bounds and reach map container
Also adds debug console logging for touch end handlers.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Remove e.preventDefault() calls from touch handlers that were causing
"Unable to preventDefault inside passive event listener" warnings.
The container already has touchAction: 'none' CSS which prevents
browser gestures (scrolling, zooming) without needing preventDefault().
React marks touch events as passive by default, so preventDefault()
doesn't work and just triggers console warnings.
Affected handlers:
- handleContainerTouchMove (mobile map drag)
- handleMagnifierTouchStart (pinch start, drag start)
- handleMagnifierTouchMove (pinch zoom, pan)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Create consolidated animation controller hook for give-up, hint, and celebration animations:
- Create features/animations/useAnimationController.ts:
- Manages state for all 3 animation types in one place
- Give-up animation: 3 pulses over 2s + 500ms cooldown
- Hint animation: 2 pulses over 1.5s
- Celebration animation: 2-4 pulses based on type (lightning/standard/hard-earned)
- Exposes unified API:
- State: giveUpProgress, isGiveUpAnimating, hintProgress, isHintAnimating, celebrationProgress
- Actions: startGiveUp, cancelGiveUp, startHint, cancelHint, startCelebration, cancelCelebration, cancelAll
- Uses usePulsingAnimation internally for consistent animation behavior
- Ready to replace scattered useState calls in MapRenderer
- Update features/animations/index.ts with new exports
Part of MapRenderer refactoring Phase 6.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Deduplicate compass crosshair code by creating reusable HeatCrosshair component:
- Create features/cursor/HeatCrosshair.tsx with size-configurable compass crosshair
- Proportional sizing calculations based on size prop
- Outer ring, 12 compass tick marks (cardinals highlighted in white)
- Center dot, fixed north indicator (red triangle)
- Spring-animated rotation with configurable shadow intensity
- Update CustomCursor.tsx to use HeatCrosshair component
- Simplified from inline SVG (~65 lines) to component usage (~1 line)
- Update MapRenderer.tsx heat crosshair overlay to use HeatCrosshair
- Replaced ~73 lines of inline SVG with 6-line component usage
- Uses size=40 and shadowIntensity=0.6 to match original styling
- Update features/cursor/index.ts with HeatCrosshair exports
Net reduction: ~135 lines of duplicated compass SVG code
Part of MapRenderer refactoring Phase 6.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add Phase 2.5 to refactoring plan:
- Design for useInteractionStateMachine hook
- ASCII state diagrams for desktop and mobile modes
- Type definitions for states and events
- Migration path from boolean flags to explicit states
Key insight: The state machine manages "what kind of input am I processing?"
not all state (animations, positions stay separate).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Memoize useInteractionStateMachine return object to prevent unnecessary
re-renders in consumers that depend on interactionMachine
- Remove debug logging effect that fired on every state change
These changes reduce the number of callback recreations and console output,
which could affect performance.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Replace all runtime usages of showMagnifier boolean with
interactionMachine.showMagnifier from the interaction state machine:
- useHotColdFeedback enabled condition
- Mobile touch end handler (tap to dismiss)
- Magnifier region indicator conditional
- ZoomLines visibility conditional
- HotColdDebugPanel prop
The old showMagnifier variable remains temporarily for debug comparison
logging during the migration period.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Update setShowMagnifier and setIsMagnifierExpanded to dispatch events
directly to both old state AND state machine during migration:
- setShowMagnifier now dispatches SHOW_MAGNIFIER/DISMISS_MAGNIFIER
- setIsMagnifierExpanded now dispatches EXPAND_MAGNIFIER/COLLAPSE_MAGNIFIER
- Removed redundant sync effects for magnifier visibility and expanded state
- This eliminates the one-render-cycle lag from useEffect sync
The wrappers ensure both systems stay in perfect sync, enabling safe
replacement of rendering conditionals with state machine values.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add state machine integration with sync effects for incremental migration:
- Call useInteractionStateMachine hook in MapRenderer
- Add sync effects to drive state machine from existing boolean state:
- Precision mode sync (pointerLocked, isReleasingPointerLock)
- Magnifier visibility sync (showMagnifier)
- Magnifier expanded sync (isMagnifierExpanded)
- Add debug logging to compare machine state vs boolean state
- Clean up unused imports (CompassCrosshair, FeedbackType)
- Organize imports per Biome rules
The state machine now runs in parallel with existing state. Next steps:
1. Test sync effects work correctly in dev console
2. Migrate handlers to dispatch events directly to state machine
3. Replace rendering conditionals with state machine checks
4. Remove old boolean state once migration is complete
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Design and implement state machine to replace scattered boolean flags:
- INTERACTION_STATE_MACHINE.md: Full design doc with states and transitions
- useInteractionStateMachine.ts: Hook with 10 explicit states, events, and reducer
States: IDLE, HOVERING, MAGNIFIER_VISIBLE, MAGNIFIER_PANNING,
MAGNIFIER_PINCHING, MAGNIFIER_EXPANDED, MAP_PANNING_MOBILE,
MAP_PANNING_DESKTOP, PRECISION_MODE, RELEASING_PRECISION
This replaces 9 independent booleans (512 combinations) with ~10 valid states.
Next: Wire up handlers to dispatch events to machine.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Reverts previous extraction attempts that went down a problematic path:
- Remove DebugAutoZoomPanel component
- Remove OtherPlayerCursors component
- Remove useUserPreferences hook
- Restore inline implementations in MapRenderer
MapRenderer is at 3912 lines. Will proceed with a more careful
extraction strategy based on updated deep dive analysis.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Update MAPRENDERER_DEEP_DIVE.md with completed extractions:
- OtherPlayerCursors ✅ (-148 lines)
- DebugAutoZoomPanel ✅ (-128 lines)
- useUserPreferences ✅ (-43 lines)
Total reduction now 1086 lines (23.2%) from original 4679 to 3593.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Create comprehensive documentation structure:
- docs/ARCHITECTURE.md: System overview with ASCII diagrams, data flow,
component responsibilities
- docs/FEATURES.md: Complete feature inventory with file references and
test coverage status
- docs/PATTERNS.md: Code conventions including component size limits
(500 hard/300 soft), feature module pattern, hook composition,
test co-location, naming conventions
Consolidate implementation docs:
- Move .claude/*.md to docs/implementation/
- Move hidden .md files to docs/implementation/
- Move MAGNIFIER_ARCHITECTURE.md and PRECISION_CONTROLS.md to docs/
Update README.md:
- Add documentation table with links to all docs
- Streamline content to focus on quick start
- Add maintenance notes section
This establishes the foundation for Phase 2 refactoring of large
components (MapRenderer 6285 lines, GameInfoPanel 2090 lines) into
feature modules following documented patterns.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Add ?debug=1 URL param to unlock debug mode in production
- Debug mode persists in localStorage once unlocked
- Add hot/cold debug panel showing all enable conditions:
- Assistance level, user toggle, fine pointer, magnifier, mobile dragging
- Overall enabled/disabled status
- Current feedback type and target region
- Change debug flags from build-time to runtime gating
- Fix isDevelopment reference in AppNavBar dropdown menu
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Broadcast cursor position during mobile drag gesture for magnifier
- Key cursors by userId (session ID) instead of playerId to support
multiple devices per player in cooperative mode
- Enable hot/cold feedback during initial mobile drag (not just magnifier pan)
- Fall back to memberPlayers lookup for remote player metadata when
rendering cursors (fixes cursor visibility for remote players)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Modify useRegionHint hook to support cycling through multiple hints
- Returns { currentHint, hintIndex, totalHints, nextHint, hasMoreHints }
- Starts with random hint, advances on nextHint() call
- Resets cycle when region changes
- Add useRegionHintSimple for backwards compatibility
- Add struggle detection in MapRenderer
- Monitors time spent searching for region
- Gives next hint every 30 seconds of struggle
- Announces new hint via speech synthesis when it changes
- Fix Part 1 hint announcement regression
- Was passing null instead of hintText to speakWithRegionName
- Now correctly announces region name followed by hint text
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Speech announcements:
- Announce region name when takeover shows (Part 1)
- Announce "You found {region}" at start of celebration (Part 2)
- Add 2-second breather delay between celebrations and next region
- Delay starts after BOTH celebration AND speech finish (async tracking)
Compass-style crosshairs:
- Replace simple crosshairs with compass design on all cursors
- Add 12 tick marks around ring (cardinal directions more prominent)
- Add red "N" indicator that counter-rotates to always point up
- Cardinal ticks are white with dark shadow for contrast
- Magnifier compass has precise rotating crosshair lines
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Crosshair improvements:
- Add wind-back animation: crosshairs smoothly return to upright when not rotating
- Remove fire particles (broken animation)
- Remove glow effects from crosshairs
- Increase crosshair ring radius for better visibility
- Remove hot/cold emoji badge (spinning crosshairs are superior feedback)
Mobile Select button fix:
- Fix intermittent Select button not working by updating hoveredRegion state
during mobile map drag and magnifier drag operations
- Previously detectRegions was called but setHoveredRegion was not,
causing Select button to use stale hover state
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Replace CSS animation with spring-for-speed, manual-integration-for-angle pattern:
- Spring animates rotation SPEED for smooth transitions between heat levels
- requestAnimationFrame loop integrates angle from speed (no jumps)
- useSpringValue binds angle directly to animated SVG elements
This solves position jumps that occurred when CSS animation duration changed.
Add documentation:
- .claude/ANIMATION_PATTERNS.md - complete pattern explanation
- Reference in CLAUDE.md for future similar tasks
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
React-spring was lagging 1000+ degrees behind the target rotation value due to
internal batching/queueing when called 60fps from requestAnimationFrame while
React was also re-rendering from cursor movement.
CSS animations run on the browser's compositor thread, completely independent
of JavaScript execution and React re-renders, eliminating the wild spinning bug.
Key changes:
- Remove useSpring and requestAnimationFrame-based rotation loop
- Use CSS @keyframes crosshairSpin animation with variable duration
- Duration calculated from heat level: 360 / (speed * 60) seconds
- animation-play-state controls running/paused state
- Debounced shouldRotate state prevents flicker from feedback type flickering
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Show hot/cold toggle on mobile devices (removed hasAnyFinePointer gate)
- Enable hot/cold feedback when magnifier is visible on touch devices
- Add checkHotCold calls to map touch and magnifier pan handlers
- Heat-tinted magnifier border based on temperature feedback
- Hot/cold emoji badge in magnifier corner showing current state
- Respects existing hot/cold game setting on all devices
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Pass startTime in addition to type from Provider to MusicProvider.
The previous fix required startTime for deduplication but Provider
was only passing type, causing the effect to never trigger.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
UI Improvements:
- Add frosted glass backdrop + enhanced text shadow for region name contrast
- Heat effect on region outline: blue → purple → orange → gold as letters typed
- Puzzle piece now uses gold styling to match map celebration
- Consistent gold styling from takeover through to map celebration
Bug Fix:
- Fix celebration sound playing 30+ times layered on top of each other
- Remove duplicate sound trigger from CelebrationOverlay (MusicContext handles it)
- Use celebration.startTime as stable identifier to prevent re-triggers
- Track last played celebration in ref to ensure single playback
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Add hidden SVG to measure accurate bounding box via getBBox() for part 1
- Both takeover parts now use getBBox() instead of different calculation methods
- Add vectorEffect="non-scaling-stroke" to takeover region shapes
- Add stroke to takeover region shape for visual consistency
- Add vectorEffect to label pointer lines in MapRenderer
- Document failure pattern in CLAUDE.md: verify agreed approach is
implemented everywhere, not just obvious cases
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add animated region silhouette that flies from takeover screen to map position
when user correctly identifies a region in learning mode:
- Add PuzzlePieceTarget interface with sourceRect for animation positioning
- Capture takeover region position with ref and getBoundingClientRect
- Use react-spring to animate from takeover to map position
- Preserve aspect ratio during takeover display
- Fix flash at animation end by checking isInTakeoverLocal
- Add debug logging for accent normalization
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
When a game starts with the "learning" assistance level, automatically
enable hot/cold audio feedback for all players. This ensures the full
learning experience is available without requiring manual toggle.
The setting is also persisted to localStorage so it remains enabled
if the player navigates away and returns.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
In turn-based learning mode:
- Show current player's emoji next to typing instruction
- Only allow the current player to type letters
- Show "Waiting for [player] to type..." when it's not your turn
- Display "Not your turn!" notice when attempting to type during another player's turn
This makes it clear whose turn it is and prevents confusion in multiplayer games.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
In turn-based multiplayer, hot/cold feedback should only be active for
the player whose turn it is. Previously, all players with fine pointers
would get hot/cold feedback regardless of turn, which could be confusing.
Added turn check: (gameMode !== 'turn-based' || currentPlayer === localPlayerId)
This matches the pattern used elsewhere in the component for cursor
broadcasting and other turn-based restrictions.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Added stories to test the normalizeToBaseLetter function:
- Côte d'Ivoire (ô → o)
- São Tomé and Príncipe (ã → a)
- Curaçao (ç → c)
- Réunion (é → e)
- México (é → e)
- Perú (ú → u)
- Saint Barthélemy (space + accent)
Also updated the demo component to use the same normalization
logic as the actual game.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Added normalizeToBaseLetter() function that converts accented characters
to their base ASCII equivalents (e.g., 'é' → 'e', 'ñ' → 'n', 'ç' → 'c').
This allows users to type region names like "Côte d'Ivoire" or "São Tomé"
using a regular keyboard without needing to input accented characters.
Applied to both physical keyboard and virtual keyboard handlers.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
The previous check for `/arcade/` (with trailing slash) missed:
- /arcade (the arcade landing page)
- /arcade-rooms/* (actual game rooms)
Changed to `/arcade` prefix check which catches all arcade routes.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Magnifier improvements:
- Add auto-zoom when dragging on magnifier (mobile)
- Add desktop click-and-drag to show magnifier (like shift key)
- Add fullscreen expand button (mobile only, top-right corner)
- Add Select button inside magnifier (mobile only, bottom-right)
- Add Full Map button to exit fullscreen (mobile only, bottom-left)
- Select button disabled when crosshair is over ocean or found region
- All magnifier buttons only appear on touch devices
- Click-drag magnifier works in pointer lock mode
Abacus visibility:
- Hide floating abacus on all /arcade/* routes by default
- Games can opt-in via setShowInGame(true) context
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Create useDeviceCapabilities.ts with three hooks:
- useIsTouchDevice(): detect touch-only devices
- useCanUsePrecisionMode(): check pointer lock + fine pointer support
- useHasAnyFinePointer(): detect any fine pointer (for hybrid devices)
- Update usePointerLock to accept canUsePrecisionMode option:
- Prevents pointer lock on unsupported devices
- Auto-exits pointer lock when switching to mobile mode (DevTools)
- Update MapRenderer to use new hooks:
- Replace manual isTouchDevice detection with hooks
- Use canUsePrecisionMode for precision mode UI visibility
- Use hasAnyFinePointer for hot/cold feedback
- Add pinch-to-zoom magnifier expansion:
- Magnifier expands to fill leftover area during pinch gesture
- Tap outside dismisses and resets size
- Update SimpleLetterKeyboard to import from shared hooks file
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add loading guard in GameComponent to prevent crash when state is
undefined during session initialization.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Use findOptimalZoom for mobile (same algorithm as desktop) instead of hardcoded 2.5-4x
- Keep magnifier visible after drag ends so user can confirm selection
- Add green "Select ✓" button below magnifier for confirming region selection
- Tap elsewhere on map to dismiss magnifier without selecting
- Disable pull-to-refresh with touchAction: none and overscrollBehavior: none
- Add defensive check for undefined includeSizes in filterRegionsBySizes
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Add mobile drag gesture detection to show magnifier when dragging on map
- Constrain magnifier to leftover rectangle (below nav/floating UI)
- Size magnifier based on leftover area dimensions, not full viewport
- Use leftover rectangle center for positioning decisions
- Prevent text selection during drag with CSS and preventDefault()
- Fix runtime error in filterRegionsBySizes with undefined check
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Detect touch drag gestures on the map to show the magnifier on mobile:
- Track touch start position and detect drag when moved past threshold
- Show magnifier when user drags on the map (not just on the magnifier)
- Position magnifier in opposite corner from touch point
- Use adaptive zoom based on region detection
- Hide magnifier when touch ends
This makes the magnifier discoverable on mobile by appearing automatically
when the user starts dragging on the map to search for regions.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add procedurally generated background music using Strudel.cc:
- MusicContext/MusicProvider for global music state
- useMusicEngine hook with Strudel.cc integration
- Regional music presets (continental themes + hyper-local variations)
- MusicControls and MusicControlPanel for user control
- MusicDebugPanel for development testing
- Region-to-music mapping system for adaptive soundtrack
- Integration with CelebrationOverlay and PlayingPhase
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Add SimpleLetterKeyboard component for touch devices (react-simple-keyboard)
- Skip spaces when counting letters for name confirmation
(e.g., "US Virgin Islands" → type U, S, V)
- Hide MyAbacus floating button when virtual keyboard is shown
- Add Storybook stories for testing letter confirmation flow
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add shared state for learning mode name confirmation:
- Add nameConfirmationProgress to KnowYourWorldState
- Add CONFIRM_LETTER move type for validating letter entry
- Implement validateConfirmLetter in Validator (looks up region name from ID)
- Add confirmLetter action to Provider context
- Update GameInfoPanel to use shared state with optimistic updates
Fixes race condition when typing fast by using an optimistic ref that
updates immediately before server responds, then syncs with server state.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>