- 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>
The name confirmation feature (type first 3 letters) was rejecting
space characters, making it impossible to confirm names like
"New York" or "Sri Lanka".
Updated regex from /[a-z]/i to /[a-z ]/i to allow spaces.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Auto-generated fresh SVG examples and unified gallery from latest templates.
Includes comprehensive crop mark demonstrations with before/after comparisons.
Files updated:
- packages/templates/gallery-unified.html
🤖 Generated with GitHub Actions
Co-Authored-By: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Add helper functions for SVG path analysis:
- parsePathToSubpaths: Parse SVG path into separate subpaths
- calculatePolygonArea: Shoelace formula for polygon area
- calculatePolygonCentroid: Shoelace formula for centroid
- getLargestSubpathCentroid: Find center of largest subpath
These will be useful for region centering improvements later.
Also adds celebration feature planning document.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
The confidence threshold was gating both audio AND visual feedback,
causing noticeable lag in the emoji updates under the cursor. Now:
- Visual emoji updates immediately on every sample (100ms)
- Confidence threshold only gates audio speech
This makes the hot/cold feedback feel much more responsive.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add celebratory feedback when kids find regions:
- Gold flash effect on the found region (main map + magnifier)
- Confetti burst from region center
- Sound effects using Web Audio API (no audio files)
- Three celebration types based on search behavior:
- Lightning: Fast direct find (< 3 sec) - quick sparkle
- Standard: Normal discovery - two-note chime
- Hard-earned: Perseverance (> 20 sec, wandering) - triumphant arpeggio + encouraging message
Uses search metrics from hot/cold feedback (path distance, direction reversals, near-misses) to classify celebration type.
Game waits for celebration to complete before advancing to next region.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add isInTakeover state to context so MapRenderer knows when GameInfoPanel
has a scrim covering the map. Suppress hot/cold audio feedback when:
- Learning mode takeover is active (user can't see the map)
- Give-up animation is playing
This prevents confusing audio hints when the user can't interact with the map.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Magnifier outline fix:
- Create shared magnifierDimensions.ts utility for responsive magnifier sizing
- Adjust magnifier viewBox to match container aspect ratio (eliminates letterboxing)
- Fix dotted outline dimensions to match magnifier's actual visible region
- Fix zoom lines to connect to correct corners matching the adjusted aspect ratio
- Fix useMagnifierZoom.ts to use actual magnifier dimensions instead of hardcoded 0.5
Visual debug toggle:
- Add VisualDebugContext for global debug flag control (dev mode only)
- Add Developer section to hamburger menu with visual debug toggle
- Wire up all debug visualizations (bounding boxes, safe zones, magnifier info)
- Persist preference to localStorage
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Display SVG silhouette of target region during takeover animation
- Use CSS centering for takeover text (more robust than position animation)
- Restructure takeover overlay as parent container to fix stacking context
- Region shape shows between scrim and text with semi-transparent blue fill
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Add react-spring powered takeover animation in learning mode
- Region name scales up and centers on screen
- Progress-based transition (letters typed / letters needed)
- Blurry backdrop scrim with smooth CSS transitions
- Nav bar remains accessible above scrim
- Fix give-up sequence timing
- Show given-up region name during animation instead of next region
- Hide type-to-unlock instruction during give-up
- Suppress takeover animation during give-up to avoid conflict
- Add remaining count display at top of prompt pane
- Add progress gradient background to prompt pane
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Guidance dropdown improvements:
- Hide dropdown entirely in "No Assistance" mode (no options to configure)
- Hide Auto-Show Hints toggle when hints unavailable (hintsMode: none)
- Hide Auto Speak toggle when hints unavailable
- Rename "Show Hints" to "Auto-Show Hints" for clarity
Setup screen badges:
- Add "⌨️ Type to unlock" badge for Learning mode
- Add "👁️ Shows names" badge when wrong clicks reveal region name
- Extract getFeatureBadges to shared utility for testability
Cursor hot/cold emoji:
- Only show emoji when effectiveHotColdEnabled (respects both
assistance level AND user toggle setting)
Code cleanup:
- Remove pointer lock button code and documentation
- Add guidanceVisibility utility with pure functions
- Add 30 unit tests for guidance visibility logic
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Bring back the travel-themed design:
- Decorative flag strip at top
- Stacked travel icons (✈️🌍 etc)
- "Start {Region}" text with context
- Region count subtitle
- Gradient background with hover effects
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Move Mode/Guidance/Start panel to top-center for better visual balance
and prominence, matching the position of the gameplay prompt.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
All setup controls (Mode, Guidance, Start) now in a single top-right panel,
matching the position of gameplay controls for seamless transition.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Move setup controls to match gameplay UI positions for minimal transition:
- Move Mode/Guidance selectors to top-right (same position as gameplay controls)
- Keep Start button at bottom-center (prominent, no conflict)
- Adjust selector sizes for vertical stack on mobile, horizontal on desktop
This minimizes map movement when transitioning from setup to gameplay
since both phases now use the same safe zone margins effectively.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Change from fixed 550px to calc(100vh - 200px) for maxHeight
- Ensures assistance level selector is visible on all screen sizes
- The inline region list on larger screens was pushing content out of view
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Increase maxHeight from 450px to 550px to accommodate game settings
- Change overflow from 'hidden' to 'auto' for scrollable content
- Game mode and assistance level selectors were being cut off
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Move game mode and assistance level selectors to right panel in DrillDownMapSelector
- Remove redundant selectors from SetupPhase bottom bar (now just Start button)
- Setup and gameplay now use same safe zone margins for consistent map positioning
- Selectors integrate with existing region filter panel in unified right-side controls
This reduces visual transition when starting game - controls are already positioned
where gameplay controls will appear.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Update DrillDownMapSelector to use same safe zone calculation as MapRenderer
- When fillContainer is true, use custom crop (or full map bounds) with SAFE_ZONE_MARGINS
- Setup and gameplay phases now show maps positioned identically
- Non-fillContainer mode keeps expanded viewBox for browsing context
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Add runtime crop override system for live DevCropTool updates without page reload
- Fix SVG letterboxing in DevCropTool coordinate conversion (screenToSvg/svgToScreen)
- Hide all UI (nav, GameInfoPanel) during crop mode for unobstructed drawing
- Show debug overlay (leftover/crop rectangles) even when no custom crop defined
- Use full map bounds as implicit crop when no custom crop exists
- Ensure map always fits within leftover area (not under UI elements)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add explicit pointerEvents: 'all' to region paths so the entire filled
area is clickable, not just the visible stroke/fill. This may fix click
detection issues on certain states (like Oregon) on older Safari/iPad.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
On devices where pointer lock isn't supported (like iPad), clicking
the map would try to request pointer lock on every click and return
early, preventing region clicks from being processed. Now we check
if pointer lock is supported before trying to request it.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Replace isTouchDevice detection with hasFinePointer using
(any-pointer: fine) media query. This properly detects if a mouse
is connected, so iPads with attached mice will show the hot/cold
button, but pure touch devices won't.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Excluded regions (filtered out by size/importance/population) were
showing labels, causing hundreds of labels to clutter the world map.
Now excluded regions are just grayed out without labels.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>