Skip to content

Commit 4eaccf4

Browse files
committed
Enhance GameScreen and GuessMap functionality
- Added state management for map mounting in GameScreen to prevent initialization at height 0. - Implemented animated transitions for map buttons in GameScreen, improving user experience. - Updated GuessMap to handle fast tap detection for map interactions, bypassing the default delay. - Refactored map press handling to improve responsiveness and reliability. Also, added a new command "mcp__ide__getDiagnostics" in settings.local.json.
1 parent fc26485 commit 4eaccf4

File tree

3 files changed

+110
-22
lines changed

3 files changed

+110
-22
lines changed

.claude/settings.local.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
"Bash(do echo \"=== $lang ===\")",
1515
"Bash(done)",
1616
"Bash(find:*)",
17-
"Bash(cat:*)"
17+
"Bash(cat:*)",
18+
"mcp__ide__getDiagnostics"
1819
],
1920
"deny": []
2021
}

mobile/app/game/[id].tsx

Lines changed: 58 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -86,12 +86,22 @@ export default function GameScreen() {
8686
const [guessPosition, setGuessPosition] = useState<{ lat: number; lng: number } | null>(null);
8787
// Map is HIDDEN by default on mobile, matching web behavior
8888
const [miniMapShown, setMiniMapShown] = useState(false);
89+
// Only mount the map once it's first shown (avoids initializing at height 0)
90+
const [mapMounted, setMapMounted] = useState(false);
8991

9092
// Animation values
9193
const mapSlideAnim = useRef(new Animated.Value(0)).current; // 0 = hidden, 1 = shown
94+
const mapBtnsAnim = useRef(new Animated.Value(0)).current; // 0 = hidden, 1 = shown
9295
const bannerSlideAnim = useRef(new Animated.Value(300)).current;
9396
const fabScaleAnim = useRef(new Animated.Value(1)).current;
9497

98+
// Mount map on first show so it initializes with actual size
99+
useEffect(() => {
100+
if (miniMapShown || gameState.isShowingResult) {
101+
setMapMounted(true);
102+
}
103+
}, [miniMapShown, gameState.isShowingResult]);
104+
95105
// Animate map slide in/out
96106
useEffect(() => {
97107
if (gameState.isShowingResult) {
@@ -119,6 +129,25 @@ export default function GameScreen() {
119129
}
120130
}, [miniMapShown, gameState.isShowingResult]);
121131

132+
// Map buttons (Guess + collapse) slide up with map
133+
useEffect(() => {
134+
if (miniMapShown && !gameState.isShowingResult) {
135+
mapBtnsAnim.setValue(0);
136+
Animated.spring(mapBtnsAnim, {
137+
toValue: 1,
138+
friction: 8,
139+
tension: 50,
140+
useNativeDriver: true,
141+
}).start();
142+
} else {
143+
Animated.timing(mapBtnsAnim, {
144+
toValue: 0,
145+
duration: 150,
146+
useNativeDriver: true,
147+
}).start();
148+
}
149+
}, [miniMapShown, gameState.isShowingResult]);
150+
122151
// Banner slide animation
123152
useEffect(() => {
124153
if (gameState.isShowingResult) {
@@ -397,22 +426,38 @@ export default function GameScreen() {
397426
]}
398427
pointerEvents={miniMapShown || gameState.isShowingResult ? 'auto' : 'none'}
399428
>
400-
<GuessMap
401-
guessPosition={guessPosition}
402-
actualPosition={
403-
gameState.isShowingResult
404-
? { lat: currentLocation.lat, lng: currentLocation.long }
405-
: undefined
406-
}
407-
onMapPress={handleMapPress}
408-
isExpanded={true}
409-
/>
429+
{mapMounted && (
430+
<GuessMap
431+
guessPosition={guessPosition}
432+
actualPosition={
433+
gameState.isShowingResult
434+
? { lat: currentLocation.lat, lng: currentLocation.long }
435+
: undefined
436+
}
437+
onMapPress={handleMapPress}
438+
isExpanded={true}
439+
/>
440+
)}
410441
</Animated.View>
411442

412443
{/* ═══ MOBILE GUESS BUTTONS - above map when map is open ═══ */}
413444
{/* Matches web: .mobile_minimap__btns.miniMapShown */}
414445
{miniMapShown && !gameState.isShowingResult && (
415-
<View style={[styles.mapButtonsRow, { bottom: height * 0.7 + 8 }]}>
446+
<Animated.View
447+
style={[
448+
styles.mapButtonsRow,
449+
{ bottom: height * 0.7 + 8 },
450+
{
451+
opacity: mapBtnsAnim,
452+
transform: [{
453+
translateY: mapBtnsAnim.interpolate({
454+
inputRange: [0, 1],
455+
outputRange: [40, 0],
456+
}),
457+
}],
458+
},
459+
]}
460+
>
416461
{/* Guess submit button (blue) */}
417462
<Pressable
418463
onPress={handleSubmitGuess}
@@ -455,7 +500,7 @@ export default function GameScreen() {
455500
<Ionicons name="arrow-down" size={24} color={colors.white} />
456501
</LinearGradient>
457502
</Pressable>
458-
</View>
503+
</Animated.View>
459504
)}
460505

461506
{/* ═══ FLOATING GUESS FAB - bottom right when map hidden ═══ */}
@@ -662,9 +707,7 @@ const styles = StyleSheet.create({
662707
borderRadius: 14,
663708
overflow: 'hidden',
664709
},
665-
guessSubmitBtnDisabled: {
666-
opacity: 0.6,
667-
},
710+
guessSubmitBtnDisabled: {},
668711
guessSubmitBtnGradient: {
669712
flex: 1,
670713
justifyContent: 'center',

mobile/src/components/game/GuessMap.tsx

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { useRef, useEffect } from 'react';
2-
import { View, StyleSheet, Platform } from 'react-native';
1+
import { useRef, useEffect, useCallback } from 'react';
2+
import { View, StyleSheet, Platform, GestureResponderEvent } from 'react-native';
33
import MapView, { Marker, Polyline, PROVIDER_GOOGLE } from 'react-native-maps';
44
import { colors } from '../../shared';
55

@@ -10,13 +10,18 @@ interface GuessMapProps {
1010
isExpanded?: boolean;
1111
}
1212

13+
const TAP_SLOP = 10; // max px movement to count as tap
14+
const TAP_MAX_MS = 300; // max duration to count as tap
15+
1316
export default function GuessMap({
1417
guessPosition,
1518
actualPosition,
1619
onMapPress,
1720
isExpanded,
1821
}: GuessMapProps) {
1922
const mapRef = useRef<MapView>(null);
23+
const touchStart = useRef({ x: 0, y: 0, time: 0 });
24+
const lastFastTap = useRef(0);
2025

2126
// When showing result, fit both markers in view
2227
useEffect(() => {
@@ -33,15 +38,54 @@ export default function GuessMap({
3338
}
3439
}, [actualPosition, guessPosition]);
3540

36-
const handleMapPress = (event: any) => {
37-
if (actualPosition) return; // Don't allow new guesses when showing result
41+
// Fast tap detection via raw touch events (bypasses MapView's ~300ms onPress delay)
42+
const handleTouchStart = useCallback((e: GestureResponderEvent) => {
43+
touchStart.current = {
44+
x: e.nativeEvent.pageX,
45+
y: e.nativeEvent.pageY,
46+
time: Date.now(),
47+
};
48+
}, []);
49+
50+
const handleTouchEnd = useCallback(async (e: GestureResponderEvent) => {
51+
if (actualPosition) return;
52+
53+
const dx = Math.abs(e.nativeEvent.pageX - touchStart.current.x);
54+
const dy = Math.abs(e.nativeEvent.pageY - touchStart.current.y);
55+
const dt = Date.now() - touchStart.current.time;
56+
57+
if (dx < TAP_SLOP && dy < TAP_SLOP && dt < TAP_MAX_MS && mapRef.current) {
58+
try {
59+
const coord = await (mapRef.current as any).coordinateForPoint({
60+
x: e.nativeEvent.locationX,
61+
y: e.nativeEvent.locationY,
62+
});
63+
if (coord) {
64+
lastFastTap.current = Date.now();
65+
onMapPress(coord.latitude, coord.longitude);
66+
}
67+
} catch {
68+
// coordinateForPoint failed, fall through to MapView onPress
69+
}
70+
}
71+
}, [actualPosition, onMapPress]);
72+
73+
// Fallback: MapView's built-in onPress (slower but reliable)
74+
const handleMapPress = useCallback((event: any) => {
75+
if (actualPosition) return;
76+
// Skip if fast tap already handled this touch
77+
if (Date.now() - lastFastTap.current < 500) return;
3878

3979
const { latitude, longitude } = event.nativeEvent.coordinate;
4080
onMapPress(latitude, longitude);
41-
};
81+
}, [actualPosition, onMapPress]);
4282

4383
return (
44-
<View style={styles.container}>
84+
<View
85+
style={styles.container}
86+
onTouchStart={handleTouchStart}
87+
onTouchEnd={handleTouchEnd}
88+
>
4589
<MapView
4690
ref={mapRef}
4791
style={styles.map}

0 commit comments

Comments
 (0)