hopefully fixing some map issues due to trying to have player data persistance between game leaves, trying to fix claiming issues with caches too. Can't test on pc with this, pushing to test on my laptop :(

This commit is contained in:
bobbert committed 2026-04-09 08:33:26 +01:00
1 parent 13abdcdaa6
commit a41f8ae6f8
4 files changed
+75 -28

No files matched your search

+27 -7
View File
@@ -2,12 +2,9 @@ import {StyleSheet, Text, View} from 'react-native';
import Card from '../UI/Card'; import Card from '../UI/Card';
import {Button} from '../UI/Button'; import {Button} from '../UI/Button';
const CacheCardItem = ({cache, isAdmin, isSelected, onEdit, onDelete, onSelect}) => { const CacheCardItem = ({cache, isAdmin, isClaimed, isSelected, onEdit, onDelete, onSelect}) => {
// Initialisation ------------- // Initialisation -------------
const hasClaims = (cache.Claims || []).length > 0;
const statusText = hasClaims ? 'Claimed' : 'Available';
const statusColor = hasClaims ? '#9ca3af' : '#16a34a';
const displayName = cache.name || cache.clue || 'Unnamed Cache'; const displayName = cache.name || cache.clue || 'Unnamed Cache';
// State ---------------------- // State ----------------------
@@ -15,15 +12,20 @@ const CacheCardItem = ({cache, isAdmin, isSelected, onEdit, onDelete, onSelect})
// View ----------------------- // View -----------------------
return ( return (
<Card style={isSelected ? styles.selectedCard : null}> <Card style={[isSelected && styles.selectedCard, isClaimed && styles.claimedCard]}>
<View style={styles.topRow}> <View style={styles.topRow}>
<View style={styles.nameWrap}> <View style={styles.nameWrap}>
<Text style={styles.name} numberOfLines={1}>{displayName}</Text> <Text style={[styles.name, isClaimed && styles.claimedText]} numberOfLines={1}>{displayName}</Text>
{cache.name ? ( {cache.name ? (
<Text style={styles.clue} numberOfLines={1}>{cache.clue}</Text> <Text style={styles.clue} numberOfLines={1}>{cache.clue}</Text>
) : null} ) : null}
</View> </View>
<Text style={[styles.status, {color: statusColor}]}>{statusText}</Text> {/* Players see team-specific claim status; admins see no status */}
{!isAdmin && (
<Text style={[styles.status, {color: isClaimed ? '#9ca3af' : '#16a34a'}]}>
{isClaimed ? 'Claimed' : 'Available'}
</Text>
)}
</View> </View>
<View style={styles.actions}> <View style={styles.actions}>
{isAdmin ? ( {isAdmin ? (
@@ -41,6 +43,14 @@ const CacheCardItem = ({cache, isAdmin, isSelected, onEdit, onDelete, onSelect})
styleLabel={styles.btnLabel} styleLabel={styles.btnLabel}
/> />
</> </>
) : isClaimed ? (
<Button
label="Already Claimed"
onClick={() => {}}
disabled={true}
styleButton={styles.claimedBtn}
styleLabel={styles.claimedBtnLabel}
/>
) : isSelected ? ( ) : isSelected ? (
<Button <Button
label="Cache Currently Selected" label="Cache Currently Selected"
@@ -71,6 +81,7 @@ const styles = StyleSheet.create({
clue: {fontSize: 13, color: '#6b7280', marginTop: 2}, clue: {fontSize: 13, color: '#6b7280', marginTop: 2},
nameWrap: {flex: 1, marginRight: 8}, nameWrap: {flex: 1, marginRight: 8},
name: {fontSize: 15, fontWeight: '600', color: '#1f2937'}, name: {fontSize: 15, fontWeight: '600', color: '#1f2937'},
claimedText: {color: '#9ca3af'},
status: {fontSize: 13, fontWeight: '600'}, status: {fontSize: 13, fontWeight: '600'},
actions: {flexDirection: 'row', gap: 8}, actions: {flexDirection: 'row', gap: 8},
editBtn: { editBtn: {
@@ -101,8 +112,17 @@ const styles = StyleSheet.create({
paddingHorizontal: 14, paddingHorizontal: 14,
flex: 1, flex: 1,
}, },
claimedBtn: {
backgroundColor: '#d1d5db',
borderColor: '#d1d5db',
minHeight: 36,
paddingHorizontal: 14,
flex: 1,
},
claimedBtnLabel: {color: '#6b7280', fontWeight: '600', fontSize: 13},
selectedBtnLabel: {color: '#ffffff', fontWeight: '600', fontSize: 13}, selectedBtnLabel: {color: '#ffffff', fontWeight: '600', fontSize: 13},
selectedCard: {borderColor: '#2563eb', borderWidth: 2}, selectedCard: {borderColor: '#2563eb', borderWidth: 2},
claimedCard: {opacity: 0.5},
btnLabel: {color: '#ffffff', fontWeight: '600', fontSize: 13}, btnLabel: {color: '#ffffff', fontWeight: '600', fontSize: 13},
}); });
+2 -2
View File
@@ -17,8 +17,8 @@ const PlayerMapView = ({userLocation, visibleCaches, heading, claimDistance}) =>
return ( return (
<MapView <MapView
style={{flex: 1}} style={{flex: 1}}
region={{...userLocation, latitudeDelta: 0.01, longitudeDelta: 0.01}} initialRegion={{...userLocation, latitudeDelta: 0.01, longitudeDelta: 0.01}}
scrollEnabled={false} scrollEnabled={true}
zoomEnabled={true} zoomEnabled={true}
rotateEnabled={false} rotateEnabled={false}
pitchEnabled={false} pitchEnabled={false}
+1 -1
View File
@@ -5,7 +5,7 @@ import Screen from '../layout/Screen';
import {Button, ButtonTray} from '../UI/Button'; import {Button, ButtonTray} from '../UI/Button';
const LoginScreen = ({ navigation }) => { const LoginScreen = ({ navigation }) => {
// Initialisation -------------> // Initialisation -------------
const { email, password, error, isLoading, setEmail, setPassword, login } = useAuthHook(); const { email, password, error, isLoading, setEmail, setPassword, login } = useAuthHook();
+45 -18
View File
@@ -1,4 +1,4 @@
import React, {useCallback, useEffect, useMemo, useState} from 'react'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {ActivityIndicator, Pressable, ScrollView, StyleSheet, Text, TextInput, View} from 'react-native'; import {ActivityIndicator, Pressable, ScrollView, StyleSheet, Text, TextInput, View} from 'react-native';
import MapView, {Circle, Marker, Polygon} from 'react-native-maps'; import MapView, {Circle, Marker, Polygon} from 'react-native-maps';
import * as Location from 'expo-location'; import * as Location from 'expo-location';
@@ -10,7 +10,7 @@ import ClaimTimerView from '../gameplay/ClaimTimerView';
import PlayerMapView from '../gameplay/PlayerMapView'; import PlayerMapView from '../gameplay/PlayerMapView';
import useGameHook from '../../hooks/useGameHook'; import useGameHook from '../../hooks/useGameHook';
import usePlayerGame from '../../hooks/usePlayerGame'; import usePlayerGame from '../../hooks/usePlayerGame';
import {getSession, setSelectedCache, setSessionGroup, setSessionUser} from '../../hooks/SessionStore'; import {getSession, setSelectedCache, setSessionGroup, setSessionTeam, setSessionUser} from '../../hooks/SessionStore';
import {getFovCone} from '../../utils/geoMath'; import {getFovCone} from '../../utils/geoMath';
const DEFAULT_REGION = {latitude: 51.5074, longitude: -0.1278, latitudeDelta: 0.05, longitudeDelta: 0.05}; const DEFAULT_REGION = {latitude: 51.5074, longitude: -0.1278, latitudeDelta: 0.05, longitudeDelta: 0.05};
@@ -22,6 +22,11 @@ const MapScreen = ({navigation}) => {
const session = getSession(); const session = getSession();
const {getCaches, claimCache, upsertCache, deleteCache, joinPrivateGame, createPrivateGame, getLobby, getUser, getSubgroups} = useGameHook(); const {getCaches, claimCache, upsertCache, deleteCache, joinPrivateGame, createPrivateGame, getLobby, getUser, getSubgroups} = useGameHook();
// Stable ref to getCaches so loadCaches never gets a new identity on re-render.
// This prevents the useEffect/useFocusEffect from looping infinitely.
const getCachesRef = useRef(getCaches);
getCachesRef.current = getCaches;
// State ---------------------- // State ----------------------
const [inGame, setInGame] = useState(Boolean(session.currentGid)); const [inGame, setInGame] = useState(Boolean(session.currentGid));
@@ -88,16 +93,17 @@ const MapScreen = ({navigation}) => {
const defaultMemberSGid = subgroups.find((sg) => !sg.IsAdminGroup)?.SGid || null; const defaultMemberSGid = subgroups.find((sg) => !sg.IsAdminGroup)?.SGid || null;
// Always call getSession() fresh inside the callback so we never read a // Always call getSession() fresh inside the callback so we never read a
// stale currentGid from the closure — matches the server's /caches endpoint // stale currentGid from the closure — matches the server's /caches endpoint.
// Uses getCachesRef so this callback has a stable identity (no infinite loop).
const loadCaches = useCallback(async () => { const loadCaches = useCallback(async () => {
const currentGid = getSession().currentGid; const currentGid = getSession().currentGid;
if (!currentGid) { if (!currentGid) {
setCacheRecords([]); setCacheRecords([]);
return; return;
} }
const rows = await getCaches(currentGid, null); const rows = await getCachesRef.current(currentGid, null);
setCacheRecords(rows || []); setCacheRecords(rows || []);
}, [getCaches]); }, []);
// Run once on mount // Run once on mount
useEffect(() => { loadCaches(); }, [loadCaches]); useEffect(() => { loadCaches(); }, [loadCaches]);
@@ -108,16 +114,16 @@ const MapScreen = ({navigation}) => {
useCallback(() => { loadCaches(); }, [loadCaches]) useCallback(() => { loadCaches(); }, [loadCaches])
); );
// Auto-select the first cache for the player when caches load and none is selected // Auto-select the first unclaimed cache for the player when caches load
useEffect(() => { useEffect(() => {
if (!isPlayer || cacheRecords.length === 0) return; if (!isPlayer || activeCachesForPlayer.length === 0) return;
// If persisted selection still exists in the list, keep it // If persisted selection still exists in the unclaimed list, keep it
if (selectedCacheId && cacheRecords.some((c) => c.id === selectedCacheId)) return; if (selectedCacheId && activeCachesForPlayer.some((c) => c.id === selectedCacheId)) return;
// Otherwise auto-select the first cache // Otherwise auto-select the first unclaimed cache
const firstId = cacheRecords[0].id; const firstId = activeCachesForPlayer[0].id;
setSelectedCacheIdState(firstId); setSelectedCacheIdState(firstId);
setSelectedCache(firstId); setSelectedCache(firstId);
}, [cacheRecords, isPlayer]); }, [activeCachesForPlayer, isPlayer]);
// Location tracking // Location tracking
useEffect(() => { useEffect(() => {
@@ -168,6 +174,15 @@ const MapScreen = ({navigation}) => {
const result = await joinPrivateGame({JoinCode: gameCode.trim().toUpperCase(), Uid: session.currentUid}); const result = await joinPrivateGame({JoinCode: gameCode.trim().toUpperCase(), Uid: session.currentUid});
if (!result) return; if (!result) return;
setSessionGroup(result.Gid, result.SGid); setSessionGroup(result.Gid, result.SGid);
// Refresh user from server so we pick up any existing TGid
// (handles re-joining a game while still in a team)
const freshUser = await getUser(session.currentUid);
if (freshUser) {
setSessionUser(freshUser);
setSessionTeam(freshUser.TGid ?? null);
}
setInGame(true); setInGame(true);
setIsAdmin(false); setIsAdmin(false);
navigation.navigate('TeamScreen'); navigation.navigate('TeamScreen');
@@ -267,12 +282,19 @@ const MapScreen = ({navigation}) => {
}); });
}; };
// Player — select cache (persist selection) // Player — select cache (persist selection); only allow selecting unclaimed caches
const handleSelectCache = (cache) => { const handleSelectCache = (cache) => {
setSelectedCacheIdState(cache.id); setSelectedCacheIdState(cache.id);
setSelectedCache(cache.id); setSelectedCache(cache.id);
}; };
// Helper — check whether a cache has been claimed by the current user's team (or user)
const isCacheClaimedByMe = (cache) => {
const claims = cache.Claims || [];
if (session.currentTid) return claims.some((c) => c.Tid === session.currentTid);
return claims.some((c) => c.Uid === session.currentUid);
};
// View ----------------------- // View -----------------------
// Not in a game // Not in a game
@@ -410,13 +432,14 @@ const MapScreen = ({navigation}) => {
} }
// Admin — normal view (map + create button + cache list) // Admin — normal view (map + create button + cache list)
// Uses initialRegion so manual zoom/pan is not reset by location updates
if (isAdmin) { if (isAdmin) {
return ( return (
<Screen style={styles.containerMap}> <Screen style={styles.containerMap}>
<View style={styles.mapContainer}> <View style={styles.mapContainer}>
<MapView <MapView
style={{flex: 1}} style={{flex: 1}}
region={mapRegion} initialRegion={mapRegion}
scrollEnabled={true} scrollEnabled={true}
zoomEnabled={true} zoomEnabled={true}
showsUserLocation showsUserLocation
@@ -520,15 +543,19 @@ const MapScreen = ({navigation}) => {
/> />
<View style={styles.cacheSection}> <View style={styles.cacheSection}>
<ScrollView> <ScrollView>
{cacheRecords.map((cache) => ( {cacheRecords.map((cache) => {
const claimed = isCacheClaimedByMe(cache);
return (
<CacheCardItem <CacheCardItem
key={cache.id} key={cache.id}
cache={cache} cache={cache}
isAdmin={false} isAdmin={false}
isSelected={selectedCacheId === cache.id} isClaimed={claimed}
onSelect={handleSelectCache} isSelected={!claimed && selectedCacheId === cache.id}
onSelect={claimed ? undefined : handleSelectCache}
/> />
))} );
})}
{cacheRecords.length === 0 && ( {cacheRecords.length === 0 && (
<Text style={styles.emptyText}>No caches available yet.</Text> <Text style={styles.emptyText}>No caches available yet.</Text>
)} )}