Change magnifier from 1/2 width (and 1/4 height) to 1/3 of both
container dimensions. This makes the magnifier less obtrusive since
the map is centered in the pane.
- Add MAGNIFIER_SIZE_RATIO constant (1/3)
- Update magnifierWidth to use containerRect.width * MAGNIFIER_SIZE_RATIO
- Update magnifierHeight to use containerRect.height * MAGNIFIER_SIZE_RATIO
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Multiplayer improvements:
- Add unanimous give-up voting for cooperative mode (all sessions must agree)
- Track give-up votes by session (userId) not player, since local players discuss together
- Add activeUserIds to track unique sessions participating in the game
Cursor emoji fix:
- Pass userId through cursor-update socket events to identify sender's session
- Use memberPlayers from roomData (canonical source) for cursor emoji lookup
- Show all emojis from a session's players on that session's network cursor
- Build playerMetadata with correct ownership map from roomData.memberPlayers
Files changed:
- socket-server.ts: Add userId to cursor-update events
- useArcadeSocket.ts: Add userId to cursor update types and functions
- useArcadeSession.ts: Store userId with cursor positions
- Provider.tsx: Expose memberPlayers, pass userId with cursor updates
- PlayingPhase.tsx: Include viewerId in cursor updates
- MapRenderer.tsx: Use memberPlayers for emoji lookup, add voting UI props
- Validator.ts: Session-based give-up voting logic
- types.ts: Add giveUpVotes and activeUserIds to state
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Multiplayer cursor sharing:
- Add cursor-update socket event for real-time cursor position broadcasting
- Show other players' cursors with crosshair in their color + emoji
- Add network hover effects (glow + dashed border) for regions other players hover
- In turn-based mode, only broadcast/show cursor for current player's turn
Map viewport fix:
- Fix SVG not filling full viewport width by measuring container instead of SVG
- Remove aspectRatio constraint that was causing circular dependency
- SVG now properly extends to show additional map context (e.g., eastern Russia)
Cleanup:
- Remove debug crop region outline (no longer needed)
- Guard debug detection box visualization behind dev-only flag
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Shows a magenta dashed rectangle outlining the custom crop region
when SHOW_CROP_REGION_DEBUG is true (dev mode only). This helps
visualize the "must be visible" area vs the expanded fill area.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add flex: 1 to map-renderer so it expands to fill the available space
in the parent flex container. Without this, the container would shrink
to fit its content, preventing the aspect ratio calculation from using
the full available viewport.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Instead of strict cropping that causes letterboxing, custom crops now:
1. Guarantee the crop region is fully visible and centered
2. Fill remaining viewport space with more of the map
3. Stay within original map bounds
Implementation:
- Add originalViewBox and customCrop fields to MapData type
- Add parseViewBox() and calculateFitCropViewBox() utility functions
- Calculate displayViewBox dynamically in MapRenderer based on container aspect ratio
- Update all coordinate calculations to use displayViewBox
Example: Europe in a wide container will show parts of Africa and Middle East
alongside the centered Europe region, instead of wasted letterbox space.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
When clicking Give Up multiple times quickly, the previous animation's
requestAnimationFrame calls would continue running, causing the highlight
to show on the wrong region (the new one) while the zoom was still
calculated for the old region.
Now properly cancels:
- Previous requestAnimationFrame callbacks
- Pending setTimeout callbacks
- Sets isCancelled flag to stop animation loop
This prevents the "Swaziland shown at Cape Verde's location" bug.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Move containerRect and svgRect lookups inside zoomSpring.to() callbacks
so all measurements are taken at the same moment as the magnifier viewBox
calculation. This ensures perfect synchronization between the magnifier
content and the overlay labels.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Reverts SVG text approach (font sizes uncontrollable with zoom) and
restores HTML overlays, but now calculates positions using zoomSpring.to()
to ensure labels are positioned at the exact same animation frame timing
as the magnifier viewBox calculation.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
DevCropTool fix:
- Quote keys with hyphens (like 'north-america') to prevent syntax errors
- Only remove quotes from valid JS identifier keys
- Add trailing commas for consistency
Magnifier label fix:
- Move debug bounding box labels from HTML overlays to SVG text elements
- Labels now render inside the magnifier SVG using the same coordinate system
- Eliminates timing mismatch between magnifier viewBox and label positioning
- Labels stay perfectly aligned with bounding boxes during cursor movement
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Add DevCropTool component (Ctrl+Shift+B to activate, R to reset, ESC to exit)
- Add customCrops.ts for storing viewBox overrides per map/continent
- Add /api/dev/save-crop endpoint to automatically update customCrops.ts
- Integrate custom crops into calculateContinentViewBox in maps.ts
- Use proper brace-matching algorithm for safe TypeScript file editing
This allows fine-tuning map region crops (e.g., cropping Russia out of Europe view)
without manual file editing.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Add Give Up button and 'G' keyboard shortcut during gameplay
- Zoom and center main map on revealed region with pulsing animation
- Fix Give Up button position during zoom animation (save position before transform)
- Remove magnifier during give-up (zoom animation makes it redundant)
- Add regionsGivenUp tracking and re-ask logic based on difficulty
- Add giveUpReveal state for animation coordination
- Add validator tests for give-up functionality
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add context-aware magnifier visibility that only shows the magnifier when the current target region is small enough to need magnification. This prevents the magnifier from appearing unnecessarily when the player is looking for large regions.
Changes:
- Add targetNeedsMagnification state to track if current prompt region is very small
- Add useEffect to check target region size on prompt/SVG dimension changes
- Update shouldShow logic to require both targetNeedsMagnification AND hasSmallRegion
- Uses same thresholds as region detection (< 15px or < 200px² area)
Also includes cleanup from previous session:
- Remove "Give Up" feature and related animation code
- Clean up props and state related to give up functionality
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Only display the magnifier when the region the player is currently being asked to find would require magnification to be clickable. This makes the magnifier context-aware and only appears when it's actually useful for the current game objective.
- Add targetNeedsMagnification state to track if current prompt region is small
- Add useEffect to check target region size when currentPrompt changes
- Update shouldShow logic to require both targetNeedsMagnification AND hasSmallRegion
- Use same thresholds as region detection (15px width/height, 200px² area)
- Log magnification check results for debugging
Now the magnifier won't appear when looking for large countries, even when hovering near small regions like Vatican City.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Remove immediate snap threshold in useMagnifierZoom - always animate smoothly with react-spring even for large zoom changes (e.g., 14.8× → 313.8× when approaching Vatican City)
- Add smooth cubic falloff for region importance (1 - (d/50)³) to prevent sudden jumps when regions enter detection box
- Reduce size boost from 2.0× to 1.5× and make conditional (only applies when distanceWeight > 0.1) to prevent tiny edge regions from dominating
- Expand detection radius from 75px to 100px for smoother transitions
- Add minimum zoom constraint to ensure magnifier viewport never exceeds detection box size (minZoom >= svgRect.height / 50)
- Compress debug panel Region Analysis: show top 3 regions instead of 5, single-line format
- Fix React duplicate key warning by deduplicating regionDecisions before rendering and using namespaced keys
- Revert to bounding box detection for performance (geometry-based detection was too expensive)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Two critical fixes:
1. **Increase sampling density from 5×5 to 11×11 grid**
- Previous: 25 sample points across 50px box = ~12.5px spacing
- New: 121 sample points across 50px box = ~5px spacing
- Problem: Tiny regions like San Marino (1-2px) were falling between sample
points and not being detected unless cursor was directly on them
- Solution: Denser sampling ensures we don't miss sub-5px regions
2. **Show all detected regions in debug panel, not just first 5**
- Removed .slice(0, 5) limit
- Added region count to header
- Needed to see full detection list to diagnose sampling issues
The geometry-based detection introduced this regression because bounding box
detection was catching everything (including empty space), while precise
geometry detection with sparse sampling was missing tiny regions.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Enhance the debug panel to show comprehensive information about how each
detected region contributed to the final zoom level decision.
New debug information displayed:
- Final zoom level and whether it was accepted or fallback to minimum
- Which region was accepted (if any)
- Adaptive acceptance thresholds (min-max % of magnifier size)
- Per-region analysis showing:
* Region ID and importance score
* Current size on screen (width × height in pixels)
* Zoom level tested for that region
* Magnified size at that zoom
* Size ratio (what % of magnifier it would occupy)
* Rejection reason if not accepted
* Visual highlighting (green for accepted, gray for rejected)
The panel now shows the top 5 most important regions sorted by importance
score, with clear visual indication of why each was accepted or rejected.
This helps understand:
- Why a particular zoom level was chosen
- Which region drove the decision
- Why other regions were rejected (too small/large, not in viewport)
- What the acceptance thresholds are for the current situation
All behind SHOW_DEBUG_BOUNDING_BOXES dev flag.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add the same debug bounding box visualization (color-coded rectangles and
importance score labels) to the magnifier view that was previously added to
the main map.
Key features:
- SVG rectangles show bounding boxes for all detected regions
- HTML overlay labels show region IDs and importance scores
- Color-coded by importance (green=accepted, orange=high, yellow=medium, gray=low)
- Only shows labels for regions within magnified viewport
- Properly converts SVG coordinates to magnifier pixel coordinates
- Behind SHOW_DEBUG_BOUNDING_BOXES dev flag
This helps debug the adaptive zoom search algorithm by showing which regions
are being considered and their importance rankings inside the magnifier.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add comprehensive visual debugging for the adaptive zoom algorithm:
- Render bounding boxes for all detected regions (not just accepted one)
- Color-code by importance: green (accepted), orange (high), yellow (medium), gray (low)
- Display importance scores calculated from distance + size weighting
- Use HTML overlays for text labels (always readable at any zoom level)
- Enable automatically in development mode via SHOW_DEBUG_BOUNDING_BOXES flag
This helps diagnose zoom behavior issues by showing:
- Which region "won" the importance calculation (green box)
- Exact importance scores (distance × size weighting)
- Bounding box rectangles vs actual region shapes
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Fix two critical bugs in magnifier zoom system:
1. **Render loop causing freeze**: Memoized pointer lock callbacks with useCallback
- onLockAcquired and onLockReleased were recreated every render
- This caused usePointerLock effect to constantly tear down/reattach event listeners
- Fixed by wrapping callbacks in useCallback with empty deps
2. **Zoom stuck at high values**: Always resume spring before starting new animations
- Spring could get paused at threshold waiting for precision mode
- When conditions changed, spring stayed paused because resume() wasn't called
- Added explicit magnifierApi.resume() before all start() calls
- Added immediate snap for large zoom differences (>100x) to prevent slow animations
3. **Debug visualization**: Added detection box overlay showing:
- 50px yellow dashed box around cursor
- List of detected regions with sizes
- Current vs target zoom levels
- Helps diagnose auto zoom behavior
4. **Documentation**: Added CRITICAL section to .claude/CLAUDE.md about checking imports
- Mandate reading imports before using React hooks
- Prevents missing import errors that break the app
Related files:
- MapRenderer.tsx: Memoized callbacks, added debug overlay
- useMagnifierZoom.ts: Added resume() call and immediate snap logic
- .claude/CLAUDE.md: Added import checking requirement
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Replace zoom freeze debug logging with click detection logging to debug
the issue where clicking on regions doesn't register most of the time.
**Changes:**
1. **Removed freeze debug logs:**
- Removed [DEBUG FREEZE] Setting target zoom log from MapRenderer
- Removed Animation effect and Threshold checks logs from useMagnifierZoom
- Freeze issue is fixed, these logs no longer needed
2. **Added click detection logs:**
- [CLICK] Region clicked in MapRenderer.tsx:1193
- Shows regionId, regionName, isExcluded, willCall
- Logs at SVG path onClick handler
- [CLICK] clickRegion called in Provider.tsx:140
- Shows regionId, regionName, currentPlayer, viewerId
- Shows currentPrompt and isCorrect (match check)
- Logs before sendMove dispatch
- [CLICK] sendMove dispatched in Provider.tsx:159
- Confirms move was sent to server
**Debug flow:**
1. User clicks SVG path → [CLICK] Region clicked
2. onRegionClick called → [CLICK] clickRegion called
3. sendMove dispatched → [CLICK] sendMove dispatched
This will help identify where clicks are failing:
- If no [CLICK] Region clicked: SVG click handler not firing
- If no [CLICK] clickRegion called: onRegionClick not being called
- If no server response: sendMove or server issue
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Integrate the useMagnifierZoom hook into MapRenderer, completing Phase 2 of the refactoring plan. This extracts ~152 lines of zoom animation logic from the component into a reusable hook.
**Changes:**
Hook Updates:
- Updated `useMagnifierZoom` to return spring object instead of `.get()` value
- This allows MapRenderer to use `.to()` interpolation for animated values
- Return type now `any` (spring object) instead of `number`
MapRenderer Reductions:
- Replaced ~101 line zoom effect with 7-line effect
- Effect now only updates opacity/position/movementMultiplier
- Zoom animation with pause/resume handled by hook
- Removed all `magnifierSpring.zoom` references (11 occurrences)
- Replaced with `zoomSpring` from hook or `getCurrentZoom()` calls
- Added TypeScript type annotations for all zoom parameters
**Line Count Impact:**
- Before Phase 2: 2083 lines
- After Phase 2: 1931 lines (-152 lines, -7.3%)
- Combined total: 2430 → 1931 lines (-499 lines, -20.5% reduction)
**Testing:**
- TypeScript: No new errors (only pre-existing test errors)
- Formatting: Passed
- Linting: No new MapRenderer warnings
**Files Modified:**
- `hooks/useMagnifierZoom.ts` - Return spring object instead of number
- `components/MapRenderer.tsx` - Integrate zoom hook, remove zoom effect
**Next Steps:**
- User testing of magnifier behavior
- Verify zoom animation, pause/resume, capping
- Verify pointer lock integration works correctly
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Replace ~220 lines of adaptive zoom search code with call to findOptimalZoom():
**Before (lines 1330-1550):**
- Inline adaptive threshold calculation
- 220+ line zoom search loop
- Viewport clamping logic
- Region-in-viewport detection
- Bounding box tracking
**After (lines 1330-1355):**
- Call findOptimalZoom() with context (~25 lines)
- Extract zoom and boundingBoxes from result
- Set debug bounding boxes
**Impact:**
- Reduced MapRenderer from 2430 → 2236 lines (-194 lines, -8%)
- Removed code duplication
- Algorithm now testable in isolation
- Improved maintainability
**Changes:**
- Import findOptimalZoom from utils/adaptiveZoomSearch
- Remove unused imports (getRegionStrokeWidth, createZoomContext)
- Change adaptiveZoom from const to let (still needs capping afterward)
- Keep zoom capping logic (uses adaptiveZoom after search)
The adaptive zoom search algorithm is now cleanly separated from the
component, making it easier to test, document, and modify.
Part of Phase 3: Integrate extracted utilities into MapRenderer.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Completed replacing ALL screen pixel ratio calculations with utility
functions. This eliminates all remaining code duplication.
Changes:
- Replaced 3 more calculations in rendering sections:
1. Pixel grid overlay fade calculation (line ~2217)
2. Magnifier label text calculation (line ~2365)
3. Gold scrim overlay threshold check (line ~2399)
- Added back intermediate variables needed for grid calculations:
- mainMapSvgUnitsPerScreenPixel (for grid spacing)
- magnifiedViewBoxWidth (for grid bounds)
Results:
- ALL 8 screen pixel ratio calculations now use utility functions
- Zero code duplication remaining
- All variables properly scoped
- TypeScript passes with no new errors
Next: Extract zoom capping logic to separate utility module.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Extract duplicated screen pixel ratio calculations into reusable utility
functions. This is Phase 1 of the magnifier zoom refactoring.
Changes:
- Created utils/screenPixelRatio.ts with pure functions:
- calculateScreenPixelRatio(): Calculate ratio from zoom context
- isAboveThreshold(): Check if ratio exceeds threshold
- calculateMaxZoomAtThreshold(): Calculate max zoom for capping
- createZoomContext(): Helper to build context from DOM elements
- Replaced 5 duplicated calculations in MapRenderer.tsx:
1. Pointer lock release zoom capping (line ~290)
2. Current zoom threshold check in pause/resume effect (line ~493)
3. Target zoom threshold check in pause/resume effect (line ~517)
4. Zoom capping in handleMouseMove (line ~1581)
5. Magnifier filter visual effect check (line ~2112)
- Also changed isNaN() to Number.isNaN() (best practice)
Benefits:
- Eliminates code duplication (5 instances → 1 utility)
- Adds comprehensive documentation of the formula
- Pure functions are easily testable
- Reduces MapRenderer.tsx complexity
- No behavior changes - purely structural refactor
Next Phase: Continue replacing remaining occurrences and extract more
zoom-related utilities.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Added detailed console logging to debug zoom issues when exiting
pointer lock mode (via Escape key or moving cursor outside map).
Logging added:
1. Pointer lock release handler:
- Current zoom state (current, target, uncapped)
- Zoom recalculation context (magnifier width, viewBox, svg width)
- Screen pixel ratio check and threshold comparison
- Whether capping was applied or not
- Missing refs diagnostics
2. Pause/resume effect:
- Current state (current zoom, target zoom, animating, pointer locked)
- Threshold checks for both current and target zoom
- Whether animation should pause
- Animation start/resume actions
This will help diagnose why zoom gets "stuck" when exiting precision mode.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
When user pressed Escape to exit precision mode, the zoom would stay
at the uncapped value (e.g., 50×) instead of animating back down to
the capped threshold (20 px/px). This was because:
- Zoom was set to uncapped value when precision mode activated
- When pointer lock released, no zoom recalculation occurred
- Zoom remained "stuck" at the uncapped value
Solution:
- When pointer lock is released, recalculate zoom with capping applied
- If uncapped zoom exceeds threshold, cap it at threshold
- This triggers animation to zoom back down to the threshold
Changes:
- Added zoom recalculation logic in handlePointerLockChange when !isLocked
- Calculate screen pixel ratio for uncapped zoom
- Cap zoom at threshold if it would exceed it
- Set targetZoom to capped value, triggering smooth zoom-out animation
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
When user clicked to activate precision mode (pointer lock), the zoom
animation stayed paused until the mouse moved. This was because:
- Zoom is capped at threshold (20 px/px) when not in pointer lock
- When pointer lock activates, no mouse movement occurs
- handleMouseMove never runs, so zoom never recalculates without cap
Solution:
- Store uncapped adaptive zoom in ref before capping logic runs
- When pointer lock activates, directly set targetZoom to uncapped value
- This triggers pause/resume effect which sees target is no longer at
threshold and resumes animation immediately
Changes:
- Added uncappedAdaptiveZoomRef to store zoom before capping
- Store uncapped value in handleMouseMove (line 1468)
- Set targetZoom to uncapped value when pointer lock acquired (lines 256-263)
- Removed synthetic mousemove event approach (didn't work)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Fixed issue where zoom animation would stay paused after activating
precision mode until the user moved the mouse.
**Problem:**
- User activates precision mode (pointer lock)
- Zoom animation stays paused at threshold
- Doesn't resume until mouse moves
- Feels unresponsive and broken
**Root cause:**
- Zoom capping logic runs in handleMouseMove
- When pointer lock activates, no mouse movement occurs
- targetZoom stays at capped value
- Spring update effect runs but with old (capped) target
- Animation doesn't resume
**Solution:**
- Trigger synthetic mousemove event when pointer lock acquired
- Event has movementX/movementY = 0 (no cursor movement)
- Triggers handleMouseMove to recalculate zoom WITHOUT capping
- New uncapped targetZoom triggers spring to resume animation
- Zoom smoothly continues from pause point
**User experience:**
- Click to activate precision mode
- Zoom immediately resumes animating toward target
- No need to move mouse to "wake up" the animation
- Smooth, responsive transition into precision mode
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Fixed issue where zoom animation would stay paused even when auto-zoom
wanted to zoom back out below the precision mode threshold.
**Problem:**
- Animation paused when hitting 20 px/px threshold
- If user moved to larger region, target zoom would drop
- But animation stayed paused, not resuming to zoom out
**Solution:**
- Check both CURRENT zoom and TARGET zoom thresholds
- Only pause if: current at threshold AND target also at/above threshold
- Resume if: current at threshold BUT target below threshold (zooming out)
**Pause conditions:**
- `shouldPause = currentIsAtThreshold && zoomIsAnimating && targetIsAtThreshold`
- If current is at threshold but target is safe → resume and zoom out
- If current is at threshold and target also at threshold → stay paused
- Added console log when resuming due to zoom-out
**User experience:**
- Zoom pauses at threshold when trying to zoom IN further
- Zoom resumes smoothly when auto-zoom wants to zoom OUT
- No stuck animations - always responsive to target changes
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Instead of ending the zoom animation when we hit the precision mode
threshold, now we pause it and wait for the user to activate pointer
lock. Once precision mode is activated, the animation resumes
smoothly from where it was paused.
**Implementation:**
- Use useSpringRef and imperative API (magnifierApi) for control
- Added useEffect to detect when zoom is at threshold
- Calls magnifierApi.pause() when at threshold without pointer lock
- Calls magnifierApi.start() to resume when pointer lock activates
- Check screenPixelRatio >= PRECISION_MODE_THRESHOLD to detect pause state
**User experience:**
- Zoom smoothly eases toward threshold
- Pauses at threshold with visual barriers (dim, scrim, grid)
- User clicks to activate precision mode
- Zoom animation seamlessly continues from pause point
- No jarring stop-start, just a smooth pause in the easement
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Implement smooth pause/resume behavior for zoom animation when hitting
the precision mode threshold, instead of hard-capping the zoom.
**Changes:**
- Added `useSpringRef` API to control spring animation
- Track desired (uncapped) zoom in ref for later resume
- Pause zoom animation when threshold is reached (not in pointer lock)
- Resume animation when pointer lock is activated
- Animation smoothly continues from paused position to desired zoom
**Behavior:**
1. User zooms in, animation smoothly increases zoom
2. At 20 px/px threshold (without pointer lock): animation pauses
3. Visual indicators appear: dimmed magnifier, gold scrim, pixel grid
4. User clicks to activate precision mode (pointer lock)
5. Animation resumes from current zoom to desired zoom
6. Smooth continuation rather than jarring jump or hard stop
**Technical implementation:**
- `desiredZoomRef`: Stores uncapped zoom user wants to reach
- `springApi.pause()`: Pauses animation at threshold
- `springApi.resume()`: Resumes when pointer lock activated
- `setTargetZoom(desiredZoomRef.current)`: Updates target for resume
This creates a "pause at barrier, resume when cleared" UX rather than
a hard wall that prevents further zooming.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Instead of hard-capping the zoom at the precision mode threshold, now the
zoom animation pauses smoothly at the threshold and resumes when pointer
lock is activated.
**Implementation:**
- Changed useSpring to useSpring() to get API access (magnifierSpringApi)
- When approaching threshold without pointer lock:
- Set target zoom to threshold level (animation continues smoothly)
- Once at threshold, call magnifierSpringApi.pause()
- When pointer lock activates:
- Call magnifierSpringApi.resume() to continue zoom animation
- Animation resumes from paused state with same easing
**User experience:**
- Zoom smoothly animates towards threshold
- Pauses at threshold with visual indicators (grid, scrim, dimming)
- User clicks to activate precision mode
- Zoom animation seamlessly resumes from pause
- Continues zooming in with same smooth easing
This creates a continuous, flowing experience rather than a hard stop.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
**Problem:**
When precision mode activated, the spring had already reached its uncapped
target. Removing the render-time clamping caused an instant jump to the
target value.
**Solution:**
Added useEffect that watches for pointerLocked changes. When precision mode
activates:
1. Get current spring value (clamped at threshold)
2. Set spring target to current value
3. This resets the spring, preventing jump
4. Next mouse move will set new uncapped target
5. Spring smoothly animates from threshold to new target
**Flow:**
1. Not in precision mode: Spring targets 100×, renders at ~40× (clamped)
2. User clicks to activate precision mode
3. useEffect fires: setTargetZoom(currentZoom) // ~40×
4. Spring is now at ~40× targeting ~40× (no movement)
5. User moves mouse: setTargetZoom(100×)
6. Spring smoothly animates from ~40× to 100×
**Result:**
✅ Fast easing to threshold
✅ No jump when activating precision mode
✅ Smooth zoom continuation after activation
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
**Problem:**
Previous fix prevented the zoom jump, but brought back slow easing
to the threshold (spring was targeting the capped value).
**Solution - Best of both worlds:**
1. Spring always targets UNCAPPED zoom (animates quickly to threshold)
2. Rendering clamps zoom at threshold when not in precision mode
3. When precision mode activates, clamping is removed
4. Spring continues smoothly from current value to target (no jump)
**How it works:**
- **Easing to threshold:** Spring targets 100×, but rendering shows max 20 px/px
→ Fast animation, immediate visual feedback at threshold
- **Activating precision mode:** pointerLocked becomes true
→ Clamping removed, spring continues from ~40× to 100×
→ Smooth continuation, no jarring jump
**Result:**
✅ Fast easing to precision mode threshold
✅ Smooth transition through threshold when activated
✅ No instantaneous zoom jumps
✅ Visual effects appear immediately at threshold
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
**Problem:**
When clicking to activate precision mode, there was a huge instantaneous
jump in zoom level. This happened because the spring was targeting the
uncapped zoom value, then immediately jumped to it when precision mode
activated.
**Solution:**
- Cap the spring target (setTargetZoom) when not in precision mode
- When precision mode activates, the target updates to uncapped value
- Spring smoothly animates from capped to uncapped value
- No more instantaneous jump
**How it works:**
1. Not in precision mode: spring targets capped zoom at threshold
2. User clicks to activate precision mode
3. pointerLocked becomes true
4. Next mouse move updates target to uncapped zoom value
5. Spring smoothly animates from threshold to final zoom
**Removed:**
- Deferred capping in viewBox calculation (no longer needed)
- Now using simple zoom value directly in viewBox
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
**Problem:**
The spring animation was easing slowly to the capped zoom value,
taking many seconds to reach the 20 px/px threshold. This made the
precision mode notices appear to activate very slowly.
**Solution:**
- Let spring animate to full target zoom (uncapped)
- Defer zoom capping until viewBox calculation (where zoom is used)
- Spring reaches threshold quickly, then "pauses" waiting for precision mode
- When precision mode activates, spring continues from threshold to final target
**Result:**
- Fast animation to precision mode threshold
- Visual effects (grid, scrim, dimming) appear quickly at threshold
- Smooth transition through threshold when precision mode activates
- No slow easing toward capped value
**Technical details:**
- Removed capping from setTargetZoom() call
- Added effectiveZoom calculation in viewBox render function
- effectiveZoom = min(zoom, maxZoom) when not in pointer lock
- effectiveZoom = zoom when in pointer lock (no cap)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
**Problem:**
The spring was animating toward the capped zoom value, causing it to
slowly ease toward the threshold over many seconds. This made the
precision mode notices appear too early and interfere with gameplay.
**Solution:**
- Spring now animates toward the FULL target zoom (uncapped)
- Zoom cap is applied when READING the zoom value in viewBox calculation
- This creates a "pause" effect at the threshold
- Spring continues animating toward target but display stays capped
- When precision mode activates, zoom immediately resumes from threshold
**User experience:**
- Zoom quickly reaches threshold and pauses (not slow crawl)
- Grid/scrim/notices appear exactly at threshold
- Clicking activates precision mode
- Zoom resumes smooth animation from threshold to target
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
**Better messaging:**
- Changed from confusing "Click here (not map)" to clearer "Click to activate precision mode"
- Message indicates clicking anywhere will activate precision mode
- Focuses on the action (click) and result (precision mode) rather than location
**Visual scrim overlay:**
- Added semi-transparent gold overlay (rgba(251, 191, 36, 0.15)) on magnifier
- Only appears when at or above precision mode threshold
- Acts as visual "barrier" indicating precision mode is needed
- Removed when pointer lock is active
- Combines with existing dimmed/desaturated magnifier effect
**User experience:**
When zoom reaches threshold:
- Magnifier dims (60% brightness, 50% saturation)
- Gold scrim appears over magnifier
- Gold pixel grid visible
- Message: "Click to activate precision mode"
- Clicking anywhere on map activates pointer lock
- All visual barriers removed once precision mode active
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Remove the background rect from the main map SVG (not magnifier).
This was causing the mispositioned blue rectangle to appear on the left.
The map container already has its own background styling, so the SVG
rect is unnecessary.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Fix TypeScript error caused by having two style attributes on the
magnifier SVG element. Merged width/height and filter into a single
style object.
Error: TS17001: JSX elements cannot have multiple attributes with the same name.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Remove the background rect element from the magnifier SVG that was
appearing incorrectly positioned to the left. The magnifier container
already has its own background styling, so the SVG rect is unnecessary.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add precision mode threshold system for know-your-world magnifier:
**Precision Mode Threshold System:**
- Constant PRECISION_MODE_THRESHOLD = 20 px/px
- Caps zoom when not in pointer lock to prevent exceeding threshold
- Shows clickable notice when threshold reached
- Activates pointer lock for precision control
**Pixel Grid Visualization:**
- Shows gold grid overlay aligned with crosshair
- Each grid cell = 1 screen pixel of mouse movement on main map
- Fades in from 70% to 100% of threshold (14-20 px/px)
- Fades out from 100% to 130% of threshold (20-26 px/px)
- Visible in both normal and precision modes
**Visual "Disabled" State:**
- Magnifier dims (60% brightness, 50% saturation) when at threshold
- Indicates zoom is capped until precision mode activated
- Returns to normal appearance in precision mode
**User Experience:**
- Below 14 px/px: Normal magnifier
- 14-20 px/px: Grid fades in as warning
- At 20 px/px: Full grid, dimmed magnifier, "Click here (not map) for precision mode"
- Click magnifier label (not map) to activate pointer lock
- In precision mode: Grid fades out (20-26 px/px), magnifier returns to normal
- e.stopPropagation() prevents accidental region clicks
**Debug Mode:**
- SHOW_MAGNIFIER_DEBUG_INFO flag (dev only)
- Shows technical info: zoom level and px/px ratio
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
**Problem:**
- Left/right edge squish-through escape wasn't working when SVG didn't fill container width
- Cursor would jump when releasing pointer lock (managed cursor position ≠ real cursor position)
**Root Cause:**
- Boundary detection used container rect instead of SVG rect
- SVG may be smaller than container due to aspect ratio constraints
- No animation back to initial capture position before releasing pointer lock
**Solution:**
1. **Use SVG boundaries for edge detection:**
- Calculate SVG offset within container (lines 796-797)
- Measure distances from SVG edges, not container edges (lines 806-809)
- Use SVG bounds for dampened distance checks (lines 831-834)
- Clamp cursor to SVG bounds (lines 914-915)
- Added debug logging showing SVG size/offset (lines 842-843)
2. **Smooth release animation:**
- Store initial cursor position when pointer lock acquired (line 185, 244-246)
- Track release animation state (line 192)
- Animate cursor back to capture position before releasing (lines 890-921)
- 200ms cubic ease-out interpolation
- Block mouse input during animation (lines 773-774)
- Real cursor appears at same position where user clicked - no jump!
**Result:**
- Squish-through escape works on all four edges regardless of map aspect ratio
- Seamless transition when releasing pointer lock
- Cursor smoothly returns to original position before handoff to real cursor
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Fixes production errors where Socket.IO server failed with "Unexpected token 'export'"
when loading map data from @svg-maps/world and @svg-maps/usa packages.
**Changes:**
- **maps.ts**: Use dynamic `import()` for ES modules instead of JSON workaround
- Async loading via `ensureMapSourcesLoaded()` works in both browser and Node.js
- Sync Proxy exports (`WORLD_MAP`, `USA_MAP`) for client components
- Async functions (`getMapData()`, `getFilteredMapData()`) for server-side use
- Remove JSON data files (world.json, usa.json) - now load directly from npm packages
- **Client components**: Use sync map exports instead of async functions
- MapRenderer, PlayingPhase, ResultsPhase, SetupPhase, StudyPhase, MapRenderer.stories
- Import `WORLD_MAP`/`USA_MAP` or use `getFilteredMapDataSync()`
- No more Promise handling needed in React components
- **Page component**: Disable SSR to prevent build-time errors
- Use Next.js `dynamic()` import with `ssr: false`
- Prevents race condition during static generation
- **tsconfig.server.json**: Include maps.ts and related files for server compilation
**Testing:**
- ✅ Docker build succeeds
- ✅ Socket.IO server loads: "Socket server module loaded successfully"
- ✅ Zero ES module errors in production build
- ✅ Maps load correctly from @svg-maps packages via dynamic imports
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>