Commit Graph

87 Commits

Author SHA1 Message Date
Thomas Hallock 61a438dd31 fix(know-your-world): reduce magnifier size to 1/3 of pane dimensions
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>
2025-11-26 09:25:25 -06:00
Thomas Hallock bb2d6fc7d8 feat(know-your-world): add session-based give-up voting and fix cursor emojis
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>
2025-11-26 09:18:58 -06:00
Thomas Hallock c3b94bea3d feat(know-your-world): add multiplayer cursor sharing and fix map viewport
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>
2025-11-26 08:25:25 -06:00
Thomas Hallock 9c89aadb17 feat: add debug indicator for custom crop region (dev only)
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>
2025-11-26 06:59:21 -06:00
Thomas Hallock 18b14766b2 fix: make map-renderer fill parent container for fit-crop-with-fill
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>
2025-11-26 06:10:19 -06:00
Thomas Hallock b6569ed4e1 feat: implement fit-crop-with-fill for custom map crops
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>
2025-11-26 05:53:43 -06:00
Thomas Hallock c01cb7f384 fix: cancel previous give-up animation when new give-up starts
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>
2025-11-25 18:55:03 -06:00
Thomas Hallock 2191e0732b fix: take all measurements inside animation callback for label sync
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>
2025-11-25 16:57:10 -06:00
Thomas Hallock 94d1cdfcb5 fix: use animated spring for magnifier label positioning
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>
2025-11-25 16:55:07 -06:00
Thomas Hallock 2e4f22a522 fix: DevCropTool key quoting and magnifier label positioning
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>
2025-11-25 16:53:32 -06:00
Thomas Hallock 855e5df2c0 feat: add dev-only crop tool for custom map region cropping
- 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>
2025-11-25 16:29:44 -06:00
Thomas Hallock 94cff4374f feat: add give up with zoom animation for Know Your World
- 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>
2025-11-25 16:12:19 -06:00
Thomas Hallock c6997ac9a7 feat: show magnifier only when target region needs it
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>
2025-11-25 11:37:50 -06:00
Thomas Hallock 33faccdf60 wip: add give up feature (client-side UI and state) 2025-11-25 11:37:50 -06:00
Thomas Hallock 996c973774 feat: show magnifier only when current target region needs magnification
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>
2025-11-25 11:37:50 -06:00
Thomas Hallock 639e662d76 fix: improve magnifier zoom smoothness and debug panel
- 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>
2025-11-25 11:37:50 -06:00
Thomas Hallock 2c9f760ae9 fix: increase sampling density for tiny region detection and show all detected regions
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>
2025-11-25 05:46:11 -06:00
Thomas Hallock cb57f1585a feat: add detailed zoom decision debug panel
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>
2025-11-24 21:14:03 -06:00
Thomas Hallock 9c7d2fab5f feat: add debug bounding boxes to magnifier view
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>
2025-11-24 19:06:11 -06:00
Thomas Hallock e60a2c09c0 feat: add visual debugging for zoom importance scoring
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>
2025-11-24 18:52:11 -06:00
Thomas Hallock 0aee60d8d1 fix: resolve auto zoom freeze and stuck zoom issues
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>
2025-11-24 18:52:11 -06:00
Thomas Hallock 0d546dcd2d debug: add click detection logging, remove freeze debug logs
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>
2025-11-24 08:57:37 -06:00
Thomas Hallock 7683948a48 debug: add instrumentation for auto zoom freeze issue
Add Phase 1 debugging instrumentation to diagnose intermittent freeze where
auto zoom gets stuck and stops changing.

**Changes:**

1. **Created .debugging-auto-zoom-freeze.md** - Comprehensive debugging guide:
   - Problem description and call chain documentation
   - 4 potential root causes (spring pause, detection stop, effect deps, race condition)
   - Debug logging strategy (3 phases)
   - Testing scenarios and expected behaviors

2. **Added [DEBUG FREEZE] logs in MapRenderer.tsx:1104**:
   - Log before setTargetZoom with current/target/uncapped zoom values
   - Include hasSmallRegion, detectedSmallestSize, pointerLocked state
   - Helps determine if setTargetZoom is being called

3. **Enhanced [DEBUG FREEZE] logs in useMagnifierZoom.ts:200**:
   - Added delta (currentZoom - targetZoom) to animation effect log
   - Enhanced threshold check logs with willPause/willStart flags
   - Helps determine if effect is running and which branch it takes

**Next Steps:**
- User reproduces freeze
- Analyze console logs to identify failing layer
- Add Phase 2/3 instrumentation as needed
- Identify and fix root cause

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-24 08:31:13 -06:00
Thomas Hallock 8ce878d03e feat(know-your-world): Phase 2 - integrate useMagnifierZoom hook
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>
2025-11-24 08:28:22 -06:00
Thomas Hallock fb55a1ce53 refactor: integrate usePointerLock hook into MapRenderer (Phase 1)
Replace pointer lock state and event listeners with usePointerLock hook:
- Removed local pointerLocked state
- Removed 134 lines of pointer lock event listeners and cleanup logic
- Added usePointerLock hook with onLockAcquired and onLockReleased callbacks
- Callbacks handle: cursor position saving, zoom updates, squish reset, zoom recalculation
- Updated handleContainerClick to use hook's requestPointerLock()

Benefits:
- Pointer lock logic now encapsulated in reusable hook
- Event listeners automatically managed by hook
- Cleaner component code with callbacks instead of inline effects
- Reduced MapRenderer from 2148 → 2083 lines (-65 lines, -3.0%)

Total reduction so far: 2430 → 2083 lines (-347 lines, -14.3%)

Part of Phase 1: Integrate usePointerLock hook (low risk).
No behavior changes - purely structural refactoring.
2025-11-24 08:28:22 -06:00
Thomas Hallock 57e9895a1b refactor: integrate useRegionDetection hook into MapRenderer (Phase 3)
Replace ~120 lines of inline region detection logic with useRegionDetection hook:
- Removed detection box calculation code
- Removed region overlap checking code
- Removed region-under-cursor detection
- Removed size tracking logic
- Removed region sorting by size

Benefits:
- Region detection logic now encapsulated in testable hook
- Hook already handles sorting by size (smallest first)
- Hook provides structured DetectedRegion objects with metadata
- Reduced MapRenderer from 2236 → 2148 lines (-88 lines, -3.9%)

Total reduction so far: 2430 → 2148 lines (-282 lines, -11.6%)

Part of Phase 3: Integrate extracted hooks into MapRenderer.
2025-11-24 08:28:22 -06:00
Thomas Hallock 1ceb5078d5 refactor: integrate adaptive zoom search utility (Phase 3)
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>
2025-11-24 08:28:22 -06:00
Thomas Hallock 360f8409d0 refactor: complete screen pixel ratio refactoring - replace all instances
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>
2025-11-24 05:52:56 -06:00
Thomas Hallock efb39b013c refactor: extract screen pixel ratio calculations to utility module (Phase 1)
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>
2025-11-24 05:47:11 -06:00
Thomas Hallock 8ae4d655d7 debug: add comprehensive logging for pointer lock zoom behavior
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>
2025-11-24 05:26:55 -06:00
Thomas Hallock 2331f1038c fix: cap zoom when releasing pointer lock (escape key)
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>
2025-11-23 21:35:41 -06:00
Thomas Hallock 7ba7e03661 fix: resume zoom animation immediately when precision mode activates
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>
2025-11-23 21:17:47 -06:00
Thomas Hallock 7c1f2e54c9 fix: resume zoom animation immediately when precision mode activates
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>
2025-11-23 21:07:03 -06:00
Thomas Hallock e73b59d510 fix: resume zoom animation when target drops below threshold
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>
2025-11-23 20:58:24 -06:00
Thomas Hallock c4989b3ab0 feat: pause zoom animation at precision mode threshold
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>
2025-11-23 20:54:59 -06:00
Thomas Hallock bdf59e571d feat: pause/resume zoom animation at precision mode threshold
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>
2025-11-23 20:50:35 -06:00
Thomas Hallock 4687820d8a feat: pause/resume zoom animation at precision mode threshold
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>
2025-11-23 20:47:03 -06:00
Thomas Hallock 33d9f15897 fix: prevent zoom jump on precision mode activation by resetting spring target
**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>
2025-11-23 20:37:15 -06:00
Thomas Hallock cab1fbff95 fix: combine fast easing with smooth precision mode transition
**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>
2025-11-23 20:27:06 -06:00
Thomas Hallock 9cb3c898ec fix: prevent zoom jump when activating precision mode
**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>
2025-11-23 20:06:28 -06:00
Thomas Hallock b355a3fc8f fix: improve zoom easing to threshold by deferring capping
**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>
2025-11-23 20:02:57 -06:00
Thomas Hallock f2ca9d1ebe fix: change zoom capping to create pause effect instead of slow easing
**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>
2025-11-23 18:55:52 -06:00
Thomas Hallock 4b20d0753f feat: add gold scrim overlay and improve precision mode messaging
**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>
2025-11-23 18:50:53 -06:00
Thomas Hallock 7d3c5c304b fix: remove background rect from main map SVG
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>
2025-11-23 14:21:53 -06:00
Thomas Hallock 3eda493051 fix: merge duplicate style attributes on magnifier SVG
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>
2025-11-23 14:04:53 -06:00
Thomas Hallock 5815cbee15 fix: remove mispositioned background rect from magnifier SVG
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>
2025-11-23 14:02:49 -06:00
Thomas Hallock 53e90414a3 feat: add precision mode system with pixel grid visualization
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>
2025-11-23 14:00:43 -06:00
Thomas Hallock a7fa858a29 fix(know-your-world): fix pointer lock escape for all edges and add smooth release animation
**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>
2025-11-22 21:50:13 -06:00
Thomas Hallock 1729418dc5 feat(know-your-world): full-screen layout with squish-through pointer lock escape
Implement full-screen, no-scrolling layout for Know Your World game with seamless
pointer lock UX:

## Layout Changes
- Add react-resizable-panels for vertical panel layout (info top, map bottom)
- Wrap playing phase with StandardGameLayout for 100vh no-scroll behavior
- Extract game info (prompt, progress, error) into compact GameInfoPanel
- Map panel fills remaining space with ResizeObserver for dynamic scaling
- SVG uses aspect-ratio to prevent distortion during panel resize

## Pointer Lock UX
- Remove obtrusive "Enable Precision Controls" prompt entirely
- First click silently enables pointer lock (seamless gameplay)
- Cursor squish-through escape at boundaries:
  - 40px dampen zone: movement slows quadratically near edges
  - 20px squish zone: cursor visually compresses (50%) and stretches (140%)
  - 2px escape threshold: pointer lock releases when squished through
- Custom cursor distortion provides visual feedback for escape progress

## Testing
- Unit tests: GameInfoPanel (20+ tests), PlayingPhase (15+ tests)
- E2E tests: Layout, panel resizing, magnifier behavior
- Update vitest config with Panda CSS aliases

## Technical Details
- ResizeObserver replaces window resize listeners for panel-aware updates
- Labels and magnifier recalculate on panel resize
- All magnifier math preserved (zoom, region indicator, coordinate transforms)
- Boundary dampening uses quadratic easing for natural feel
- Squish effect animates with 0.1s ease-out transition

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-22 21:50:13 -06:00
Thomas Hallock ab94fd350f fix: use dynamic ES module imports for @svg-maps packages in know-your-world
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>
2025-11-22 12:16:54 -06:00