mirror of
https://github.com/mudabbir-ahmad/MAD-Treasure-Hunt.git
synced 2026-10-07 19:00:20 +00:00
Refactor API endpoints: update subgroup membership references to subgroup members and streamline cache visibility logic based on selected cache ID.
This commit is contained in:
1 parent
4407d05fdb
commit
e3564bea08
7 files changed
+73
-189
No files matched your search
@@ -6,12 +6,32 @@ Treasure Hunt mobile app built with React Native and Expo.
|
||||
|
||||
```bash
|
||||
npm i
|
||||
npx expo start --android
|
||||
npm start
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- The app accesses game/auth data through the centralized API endpoint via `src/components/API/API.js`.
|
||||
- Navigation is handled with React Navigation native stack.
|
||||
- Sometimes you might have to switch the API Endpoint URL. This is just Because of the fact that when I'm using the app I can't access the API endpoint, from the global URL. Therefore, I need my local API Server's IP as the URL
|
||||
- Just Uncomment the `API_ENDPOINT` variable in `src/components/API/API.js` to use the actual API Server Which is `api.bobby.ip-ddns.com` [Do not add any port thing here as it uses port 80 by default].
|
||||
- IF encountering network issues, ensure the API endpoint is set to 'http://api.bobby.ip-ddns.com' and not 'http://192.168.8.100:3000'.
|
||||
- The reason for 'http://192.168.8.100:3000' is due to the API server being on my (Mudabbir's) home network. I need to use local ip due to loop back restrictions with home network(needed for security).
|
||||
- Just uncomment line 1 in `src/components/API/API.js` to use the actual API Server Which is `api.bobby.ip-ddns.com` [Do not add any port thing here as it uses port 80 by default].
|
||||
|
||||
|
||||
# DB Structure:
|
||||
|
||||
API endpoint: `http://api.bobby.ip-ddns.com/`
|
||||
|
||||
### Tables:
|
||||
- `users`
|
||||
- `teams`
|
||||
- `team-members`
|
||||
- `subgroups`
|
||||
- `subgroup-members`
|
||||
- `groups`
|
||||
- `game-types`
|
||||
- `db`
|
||||
- `caches`
|
||||
- `admin-waitlist`
|
||||
|
||||
#### Append Table to API URL to view table's json data on browser.
|
||||
I.e: `http://api.bobby.ip-ddns.com/users` or `http://api.bobby.ip-ddns.com/subgroups-members`
|
||||
@@ -1,14 +1,8 @@
|
||||
import React from 'react';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
import MapView, {Marker, Polygon} from 'react-native-maps';
|
||||
import {getFovCone} from '../../utils/geoMath';
|
||||
|
||||
const PlayerMapView = ({userLocation, visibleCaches, heading}) => {
|
||||
// Initialisation -------------
|
||||
// State ----------------------
|
||||
// Handlers -------------------
|
||||
// View -----------------------
|
||||
|
||||
if (!userLocation) return null;
|
||||
|
||||
const coneCoords = (heading !== null && heading !== undefined)
|
||||
@@ -26,25 +20,14 @@ const PlayerMapView = ({userLocation, visibleCaches, heading}) => {
|
||||
pitchEnabled={false}
|
||||
showsUserLocation
|
||||
>
|
||||
{/* Cache pin markers — appear temporarily when the cache is in the FOV
|
||||
cone AND within claim distance. Uses a custom View so the pin
|
||||
renders identically on iOS (Google Maps) and Android.
|
||||
tracksViewChanges must be true so Android captures the custom
|
||||
View as a bitmap on the first render cycle. */}
|
||||
{(visibleCaches || []).map((cache) => (
|
||||
<Marker
|
||||
key={cache.id}
|
||||
coordinate={cache.coordinates}
|
||||
pinColor="orange"
|
||||
title={cache.clue}
|
||||
anchor={{x: 0.5, y: 0.5}}
|
||||
tracksViewChanges={true}
|
||||
>
|
||||
<View style={styles.markerOuter}>
|
||||
<View style={styles.markerInner} />
|
||||
</View>
|
||||
</Marker>
|
||||
/>
|
||||
))}
|
||||
{/* FOV heading cone */}
|
||||
{coneCoords && (
|
||||
<Polygon
|
||||
coordinates={coneCoords}
|
||||
@@ -57,26 +40,6 @@ const PlayerMapView = ({userLocation, visibleCaches, heading}) => {
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
markerOuter: {
|
||||
width: 26,
|
||||
height: 26,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
markerInner: {
|
||||
width: 18,
|
||||
height: 18,
|
||||
borderRadius: 9,
|
||||
backgroundColor: '#f59e0b',
|
||||
borderWidth: 2.5,
|
||||
borderColor: '#ffffff',
|
||||
shadowColor: '#000000',
|
||||
shadowOffset: {width: 0, height: 1},
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 2,
|
||||
elevation: 3,
|
||||
},
|
||||
});
|
||||
|
||||
export default PlayerMapView;
|
||||
|
||||
|
||||
@@ -39,10 +39,10 @@ const BottomNavbar = ({navigation, routeName}) => {
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
const s = getSession();
|
||||
const endpoint = `${API_BASE_URL}/subgroup-memberships?Uid=${s.currentUid}&Gid=${s.currentGid}`;
|
||||
const endpoint = `${API_BASE_URL}/subgroup-members?Uid=${s.currentUid}&Gid=${s.currentGid}`;
|
||||
const res = await API.get(endpoint);
|
||||
if (res.isSuccess && res.result.length > 0) {
|
||||
await API.delete(`${API_BASE_URL}/subgroup-memberships/${res.result[0].id}`);
|
||||
await API.delete(`${API_BASE_URL}/subgroup-members/${res.result[0].id}`);
|
||||
}
|
||||
clearGameSession();
|
||||
navigation.reset({index: 0, routes: [{name: 'Game'}]});
|
||||
|
||||
@@ -8,20 +8,13 @@ import {getFovCone, isInClaimCone} from '../../utils/geoMath';
|
||||
const DEFAULT_REGION = {latitude: 51.5074, longitude: -0.1278, latitudeDelta: 0.01, longitudeDelta: 0.01};
|
||||
|
||||
const ExpandedMapScreen = ({route}) => {
|
||||
// Initialisation ------------
|
||||
|
||||
const {isAdmin, cacheRecords: cacheStr, claimDistance: routeClaimDistance, userLocation: routeLocation} = route.params || {};
|
||||
const {isAdmin, cacheRecords: cacheStr, claimDistance: routeClaimDistance, userLocation: routeLocation, selectedCacheId} = route.params || {};
|
||||
const caches = cacheStr ? JSON.parse(cacheStr) : [];
|
||||
const claimDistance = routeClaimDistance || 20;
|
||||
|
||||
// State ----------------------
|
||||
|
||||
// Seed with the location passed from MapScreen so the map opens centred on the user immediately
|
||||
const [userLocation, setUserLocation] = useState(routeLocation || null);
|
||||
const [heading, setHeading] = useState(null);
|
||||
|
||||
// Handlers -------------------
|
||||
|
||||
useEffect(() => {
|
||||
let locationSub;
|
||||
let headingSub;
|
||||
@@ -30,7 +23,6 @@ const ExpandedMapScreen = ({route}) => {
|
||||
const {status} = await Location.requestForegroundPermissionsAsync();
|
||||
if (status !== 'granted') return;
|
||||
|
||||
// Only do a fresh one-shot fix if MapScreen didn't supply a location
|
||||
if (!routeLocation) {
|
||||
const last = await Location.getLastKnownPositionAsync();
|
||||
if (last) {
|
||||
@@ -46,7 +38,6 @@ const ExpandedMapScreen = ({route}) => {
|
||||
(next) => setUserLocation({latitude: next.coords.latitude, longitude: next.coords.longitude}),
|
||||
);
|
||||
|
||||
// Use OS-fused heading from expo-location
|
||||
headingSub = await Location.watchHeadingAsync((headingData) => {
|
||||
const raw = headingData.trueHeading >= 0 ? headingData.trueHeading : headingData.magHeading;
|
||||
setHeading(raw);
|
||||
@@ -64,17 +55,11 @@ const ExpandedMapScreen = ({route}) => {
|
||||
? {...userLocation, latitudeDelta: 0.01, longitudeDelta: 0.01}
|
||||
: DEFAULT_REGION;
|
||||
|
||||
// View -----------------------
|
||||
|
||||
const hasHeading = heading !== null;
|
||||
const coneCoords = (userLocation && hasHeading)
|
||||
? getFovCone(userLocation, heading)
|
||||
: null;
|
||||
const coneCoords = (userLocation && hasHeading) ? getFovCone(userLocation, heading) : null;
|
||||
|
||||
// For players — heading is required so the player must point their device
|
||||
// towards a cache for it to appear (same logic as the main MapScreen)
|
||||
const visiblePlayerCaches = (!isAdmin && userLocation && hasHeading)
|
||||
? caches.filter((c) => isInClaimCone(heading, userLocation, c.coordinates, claimDistance))
|
||||
const visiblePlayerCaches = (!isAdmin && userLocation && hasHeading && selectedCacheId)
|
||||
? caches.filter((c) => c.id === selectedCacheId && isInClaimCone(heading, userLocation, c.coordinates, claimDistance))
|
||||
: [];
|
||||
|
||||
return (
|
||||
@@ -86,21 +71,9 @@ const ExpandedMapScreen = ({route}) => {
|
||||
initialRegion={region}
|
||||
showsUserLocation
|
||||
>
|
||||
{/* Admins see all cache pin locations.
|
||||
tracksViewChanges must be true so Android captures the
|
||||
custom View as a bitmap on the first render cycle. */}
|
||||
{isAdmin && caches.map((cache) => (
|
||||
<React.Fragment key={cache.id}>
|
||||
<Marker
|
||||
coordinate={cache.coordinates}
|
||||
title={cache.name || cache.clue}
|
||||
anchor={{x: 0.5, y: 0.5}}
|
||||
tracksViewChanges={true}
|
||||
>
|
||||
<View style={styles.adminMarkerOuter}>
|
||||
<View style={styles.adminMarkerInner} />
|
||||
</View>
|
||||
</Marker>
|
||||
<Marker coordinate={cache.coordinates} pinColor="#9ca3af" title={cache.name || cache.clue} />
|
||||
<Circle
|
||||
center={cache.coordinates}
|
||||
radius={claimDistance}
|
||||
@@ -109,23 +82,14 @@ const ExpandedMapScreen = ({route}) => {
|
||||
/>
|
||||
</React.Fragment>
|
||||
))}
|
||||
{/* Players — pin markers appear when cache is in FOV cone + within range.
|
||||
tracksViewChanges must be true so Android captures the
|
||||
custom View as a bitmap on the first render cycle. */}
|
||||
{!isAdmin && visiblePlayerCaches.map((cache) => (
|
||||
<Marker
|
||||
key={cache.id}
|
||||
coordinate={cache.coordinates}
|
||||
pinColor="orange"
|
||||
title={cache.clue}
|
||||
anchor={{x: 0.5, y: 0.5}}
|
||||
tracksViewChanges={true}
|
||||
>
|
||||
<View style={styles.cacheMarkerOuter}>
|
||||
<View style={styles.cacheMarkerInner} />
|
||||
</View>
|
||||
</Marker>
|
||||
/>
|
||||
))}
|
||||
{/* Heading FOV cone */}
|
||||
{coneCoords && (
|
||||
<Polygon
|
||||
coordinates={coneCoords}
|
||||
@@ -143,20 +107,8 @@ const ExpandedMapScreen = ({route}) => {
|
||||
const styles = StyleSheet.create({
|
||||
container: {padding: 0},
|
||||
mapWrap: {flex: 1},
|
||||
// Admin cache markers — blue dot
|
||||
adminMarkerOuter: {width: 26, height: 26, alignItems: 'center', justifyContent: 'center'},
|
||||
adminMarkerInner: {
|
||||
width: 18, height: 18, borderRadius: 9,
|
||||
backgroundColor: '#2563eb', borderWidth: 2.5, borderColor: '#ffffff',
|
||||
shadowColor: '#000', shadowOffset: {width: 0, height: 1}, shadowOpacity: 0.3, shadowRadius: 2, elevation: 3,
|
||||
},
|
||||
// Player cache markers — orange dot
|
||||
cacheMarkerOuter: {width: 26, height: 26, alignItems: 'center', justifyContent: 'center'},
|
||||
cacheMarkerInner: {
|
||||
width: 18, height: 18, borderRadius: 9,
|
||||
backgroundColor: '#f59e0b', borderWidth: 2.5, borderColor: '#ffffff',
|
||||
shadowColor: '#000', shadowOffset: {width: 0, height: 1}, shadowOpacity: 0.3, shadowRadius: 2, elevation: 3,
|
||||
},
|
||||
});
|
||||
|
||||
export default ExpandedMapScreen;
|
||||
|
||||
|
||||
@@ -22,8 +22,7 @@ const MapScreen = ({navigation}) => {
|
||||
const session = getSession();
|
||||
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.
|
||||
// Stable ref to prevent infinite re-render loops
|
||||
const getCachesRef = useRef(getCaches);
|
||||
getCachesRef.current = getCaches;
|
||||
|
||||
@@ -49,19 +48,12 @@ const MapScreen = ({navigation}) => {
|
||||
const [newName, setNewName] = useState('');
|
||||
const [newClue, setNewClue] = useState('');
|
||||
|
||||
// Global claim distance comes from the group's CacheTriggerMeters setting
|
||||
const claimDistance = groupInfo?.CacheTriggerMeters || 20;
|
||||
|
||||
const isPlayer = inGame && !isAdmin;
|
||||
|
||||
// Teams are required but the player hasn't joined one — block all claiming
|
||||
const requiresTeam = Boolean(isPlayer && groupInfo?.TeamsEnabled && !session.currentTid);
|
||||
|
||||
// Stable empty array — avoids creating a new reference on every render
|
||||
// which would trigger an infinite re-render loop in usePlayerGame
|
||||
const EMPTY_CACHES = useMemo(() => [], []);
|
||||
|
||||
// Filter out caches already claimed by the current user's team (or by the user if no team)
|
||||
// Caches not yet claimed by the current user's team (or user if no team)
|
||||
const activeCachesForPlayer = useMemo(() => {
|
||||
if (!isPlayer) return EMPTY_CACHES;
|
||||
return cacheRecords.filter((cache) => {
|
||||
@@ -73,8 +65,6 @@ const MapScreen = ({navigation}) => {
|
||||
});
|
||||
}, [cacheRecords, isPlayer, session.currentTid, session.currentUid, EMPTY_CACHES]);
|
||||
|
||||
// When requiresTeam is true pass null/empty so usePlayerGame is inert.
|
||||
// selectedCacheId is the 5th param — only that cache can trigger the claim countdown.
|
||||
const {visibleCaches, isClaiming, setIsClaiming} = usePlayerGame(
|
||||
(isPlayer && !requiresTeam) ? userLocation : null,
|
||||
(isPlayer && !requiresTeam) ? heading : null,
|
||||
@@ -98,9 +88,6 @@ const MapScreen = ({navigation}) => {
|
||||
// Default member subgroup SGid for cache creation (first non-admin subgroup)
|
||||
const defaultMemberSGid = subgroups.find((sg) => !sg.IsAdminGroup)?.SGid || null;
|
||||
|
||||
// Always call getSession() fresh inside the callback so we never read a
|
||||
// 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 currentGid = getSession().currentGid;
|
||||
if (!currentGid) {
|
||||
@@ -114,18 +101,15 @@ const MapScreen = ({navigation}) => {
|
||||
// Run once on mount
|
||||
useEffect(() => { loadCaches(); }, [loadCaches]);
|
||||
|
||||
// Re-fetch caches every time this screen gains focus so the list is always
|
||||
// up-to-date after navigating away and back (e.g. Settings → Map)
|
||||
// Re-fetch caches when screen gains focus
|
||||
useFocusEffect(
|
||||
useCallback(() => { loadCaches(); }, [loadCaches])
|
||||
);
|
||||
|
||||
// Auto-select the first unclaimed cache for the player when caches load
|
||||
// Auto-select first unclaimed cache when list loads
|
||||
useEffect(() => {
|
||||
if (!isPlayer || activeCachesForPlayer.length === 0) return;
|
||||
// If persisted selection still exists in the unclaimed list, keep it
|
||||
if (selectedCacheId && activeCachesForPlayer.some((c) => c.id === selectedCacheId)) return;
|
||||
// Otherwise auto-select the first unclaimed cache
|
||||
const firstId = activeCachesForPlayer[0].id;
|
||||
setSelectedCacheIdState(firstId);
|
||||
setSelectedCache(firstId);
|
||||
@@ -145,7 +129,6 @@ const MapScreen = ({navigation}) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Try cached position first for instant load, fall back to fresh fix
|
||||
const last = await Location.getLastKnownPositionAsync();
|
||||
if (last) {
|
||||
setUserLocation({latitude: last.coords.latitude, longitude: last.coords.longitude});
|
||||
@@ -160,7 +143,6 @@ const MapScreen = ({navigation}) => {
|
||||
(next) => setUserLocation({latitude: next.coords.latitude, longitude: next.coords.longitude}),
|
||||
);
|
||||
|
||||
// Use OS-fused heading from expo-location
|
||||
headingSub = await Location.watchHeadingAsync((headingData) => {
|
||||
const raw = headingData.trueHeading >= 0 ? headingData.trueHeading : headingData.magHeading;
|
||||
setHeading(raw);
|
||||
@@ -176,13 +158,10 @@ const MapScreen = ({navigation}) => {
|
||||
|
||||
const handleJoinGame = async () => {
|
||||
if (!gameCode.trim()) return;
|
||||
// Force uppercase just in case a lowercase code is pasted
|
||||
const result = await joinPrivateGame({JoinCode: gameCode.trim().toUpperCase(), Uid: session.currentUid});
|
||||
if (!result) return;
|
||||
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);
|
||||
@@ -221,14 +200,12 @@ const MapScreen = ({navigation}) => {
|
||||
});
|
||||
setIsClaiming(false);
|
||||
if (result) {
|
||||
// Show "Cache Claimed!" popup for 3 seconds
|
||||
setClaimedPopupVisible(true);
|
||||
setTimeout(() => setClaimedPopupVisible(false), 3000);
|
||||
}
|
||||
await loadCaches();
|
||||
}, [session.currentGid, session.currentUid, session.currentTid, loadCaches]);
|
||||
|
||||
// Admin — open create form
|
||||
const handleCreateCachePress = () => {
|
||||
const fallback = userLocation || {latitude: mapRegion.latitude, longitude: mapRegion.longitude};
|
||||
setEditingCacheId(null);
|
||||
@@ -238,7 +215,6 @@ const MapScreen = ({navigation}) => {
|
||||
setIsCreating(true);
|
||||
};
|
||||
|
||||
// Admin — open edit form
|
||||
const handleEditCache = (cache) => {
|
||||
setEditingCacheId(cache.id);
|
||||
setNewCoord(cache.coordinates);
|
||||
@@ -247,14 +223,12 @@ const MapScreen = ({navigation}) => {
|
||||
setIsCreating(true);
|
||||
};
|
||||
|
||||
// Admin — delete cache
|
||||
const handleDeleteCache = async (cache) => {
|
||||
if (!session.currentGid) return;
|
||||
await deleteCache(session.currentGid, cache.id);
|
||||
await loadCaches();
|
||||
};
|
||||
|
||||
// Admin — save create or edit
|
||||
const handleSaveCache = async () => {
|
||||
if (!newCoord || !newClue.trim() || !session.currentGid) return;
|
||||
const payload = {
|
||||
@@ -277,7 +251,6 @@ const MapScreen = ({navigation}) => {
|
||||
setEditingCacheId(null);
|
||||
};
|
||||
|
||||
// Navigate to expanded map view, passing the live location so it opens centred on the user
|
||||
const handleExpandMap = () => {
|
||||
navigation.navigate('ExpandedMapScreen', {
|
||||
isAdmin,
|
||||
@@ -285,16 +258,15 @@ const MapScreen = ({navigation}) => {
|
||||
heading: heading || 0,
|
||||
claimDistance,
|
||||
userLocation: userLocation || null,
|
||||
selectedCacheId,
|
||||
});
|
||||
};
|
||||
|
||||
// Player — select cache (persist selection); only allow selecting unclaimed caches
|
||||
const handleSelectCache = (cache) => {
|
||||
setSelectedCacheIdState(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);
|
||||
@@ -438,8 +410,7 @@ const MapScreen = ({navigation}) => {
|
||||
);
|
||||
}
|
||||
|
||||
// Admin — normal view (map + create button + cache list)
|
||||
// Uses initialRegion so manual zoom/pan is not reset by location updates
|
||||
// Admin view
|
||||
if (isAdmin) {
|
||||
return (
|
||||
<Screen style={styles.containerMap}>
|
||||
@@ -523,8 +494,7 @@ const MapScreen = ({navigation}) => {
|
||||
);
|
||||
}
|
||||
|
||||
// Player in game — the claim target is the selected cache (only it can trigger the countdown)
|
||||
const claimTarget = visibleCaches.find((c) => c.id === selectedCacheId) || null;
|
||||
const claimTarget = visibleCaches[0] || null;
|
||||
|
||||
return (
|
||||
<Screen style={styles.containerMap}>
|
||||
|
||||
+12
-16
@@ -6,7 +6,7 @@ const useGameHook = () => {
|
||||
const usersEndpoint = `${API_BASE_URL}/users`;
|
||||
const groupsEndpoint = `${API_BASE_URL}/groups`;
|
||||
const subgroupsEndpoint = `${API_BASE_URL}/subgroups`;
|
||||
const subgroupMembershipsEndpoint = `${API_BASE_URL}/subgroup-memberships`;
|
||||
const subgroupMembersEndpoint = `${API_BASE_URL}/subgroup-members`;
|
||||
const teamsEndpoint = `${API_BASE_URL}/teams`;
|
||||
const teamMembersEndpoint = `${API_BASE_URL}/team-members`;
|
||||
const cachesEndpoint = `${API_BASE_URL}/caches`;
|
||||
@@ -67,19 +67,19 @@ const useGameHook = () => {
|
||||
return response.isSuccess;
|
||||
};
|
||||
|
||||
// Subgroup memberships
|
||||
// Subgroup members
|
||||
const joinPrivateGame = async (payload) => {
|
||||
const response = await API.post(subgroupMembershipsEndpoint, payload);
|
||||
const response = await API.post(subgroupMembersEndpoint, payload);
|
||||
return response.isSuccess ? response.result : null;
|
||||
};
|
||||
|
||||
const getGroupMembers = async (gid) => {
|
||||
const response = await API.get(`${subgroupMembershipsEndpoint}?Gid=${gid}`);
|
||||
const response = await API.get(`${subgroupMembersEndpoint}?Gid=${gid}`);
|
||||
return response.isSuccess ? response.result : [];
|
||||
};
|
||||
|
||||
const removeMember = async (membershipId) => {
|
||||
const response = await API.delete(`${subgroupMembershipsEndpoint}/${membershipId}`);
|
||||
const response = await API.delete(`${subgroupMembersEndpoint}/${membershipId}`);
|
||||
return response.isSuccess;
|
||||
};
|
||||
|
||||
@@ -125,7 +125,6 @@ const useGameHook = () => {
|
||||
return response.isSuccess;
|
||||
};
|
||||
|
||||
// Caches (standalone resource)
|
||||
const getCaches = async (gid, sgid = null) => {
|
||||
let url = `${cachesEndpoint}?Gid=${gid}`;
|
||||
if (sgid !== null) url += `&SGid=${sgid}`;
|
||||
@@ -134,14 +133,7 @@ const useGameHook = () => {
|
||||
};
|
||||
|
||||
const upsertCache = async (payload) => {
|
||||
if (payload.cacheId) {
|
||||
const response = await API.put(
|
||||
`${cachesEndpoint}/${payload.cacheId}`,
|
||||
payload,
|
||||
);
|
||||
return response.isSuccess ? response.result : null;
|
||||
}
|
||||
const response = await API.post(cachesEndpoint, {
|
||||
const data = {
|
||||
Gid: payload.gid,
|
||||
SGid: payload.subgroupId,
|
||||
Title: payload.name,
|
||||
@@ -149,11 +141,15 @@ const useGameHook = () => {
|
||||
Latitude: payload.latitude,
|
||||
Longitude: payload.longitude,
|
||||
TriggerMeters: payload.radius,
|
||||
});
|
||||
};
|
||||
if (payload.cacheId) {
|
||||
const response = await API.put(`${cachesEndpoint}/${payload.cacheId}`, data);
|
||||
return response.isSuccess ? response.result : null;
|
||||
}
|
||||
const response = await API.post(cachesEndpoint, data);
|
||||
return response.isSuccess ? response.result : null;
|
||||
};
|
||||
|
||||
// Claim a cache — team-aware: prevents same team from claiming again
|
||||
const claimCache = async (payload) => {
|
||||
const response = await API.post(
|
||||
`${cachesEndpoint}/${payload.cacheId}/claim`,
|
||||
|
||||
+15
-32
@@ -2,52 +2,35 @@ import {useEffect, useRef, useState} from 'react';
|
||||
import {isInClaimCone} from '../utils/geoMath';
|
||||
|
||||
const usePlayerGame = (playerLocation, playerHeading, activeCaches, claimDistance, selectedCacheId) => {
|
||||
// State ----------------------
|
||||
|
||||
const [visibleCaches, setVisibleCaches] = useState([]);
|
||||
const [isClaiming, setIsClaiming] = useState(false);
|
||||
const prevIdsRef = useRef('');
|
||||
const prevIdRef = useRef('');
|
||||
|
||||
// Handlers -------------------
|
||||
|
||||
// Determine which caches fall inside the FOV claim cone
|
||||
useEffect(() => {
|
||||
if (!playerLocation || playerHeading === null || playerHeading === undefined) {
|
||||
if (prevIdsRef.current !== '') {
|
||||
prevIdsRef.current = '';
|
||||
if (!playerLocation || playerHeading === null || playerHeading === undefined || !selectedCacheId) {
|
||||
if (prevIdRef.current !== '') {
|
||||
prevIdRef.current = '';
|
||||
setVisibleCaches([]);
|
||||
setIsClaiming(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const inCone = (activeCaches || []).filter((cache) =>
|
||||
isInClaimCone(playerHeading, playerLocation, cache.coordinates, claimDistance),
|
||||
);
|
||||
const selected = (activeCaches || []).find((c) => c.id === selectedCacheId);
|
||||
const visible = selected && isInClaimCone(playerHeading, playerLocation, selected.coordinates, claimDistance);
|
||||
const newId = visible ? String(selected.id) : '';
|
||||
|
||||
// Only update state when the set of visible caches actually changes
|
||||
const newIds = inCone.map((c) => c.id).join(',');
|
||||
if (newIds !== prevIdsRef.current) {
|
||||
prevIdsRef.current = newIds;
|
||||
setVisibleCaches(inCone);
|
||||
if (newId !== prevIdRef.current) {
|
||||
prevIdRef.current = newId;
|
||||
setVisibleCaches(visible ? [selected] : []);
|
||||
setIsClaiming(Boolean(visible));
|
||||
}
|
||||
}, [playerLocation, playerHeading, activeCaches, claimDistance]);
|
||||
|
||||
// Claiming requires ALL four criteria:
|
||||
// 1. Cache not already claimed by player's team (handled by activeCaches filter)
|
||||
// 2. Cache within claim distance (handled by isInClaimCone)
|
||||
// 3. Player looking at the cache (in FOV cone) (handled by isInClaimCone)
|
||||
// 4. Cache is the one currently selected in list (checked here)
|
||||
useEffect(() => {
|
||||
const canClaim = Boolean(
|
||||
selectedCacheId && visibleCaches.some((c) => c.id === selectedCacheId),
|
||||
);
|
||||
setIsClaiming(canClaim);
|
||||
}, [visibleCaches, selectedCacheId]);
|
||||
|
||||
// Return ---------------------
|
||||
}, [playerLocation, playerHeading, activeCaches, claimDistance, selectedCacheId]);
|
||||
|
||||
return {visibleCaches, isClaiming, setIsClaiming};
|
||||
};
|
||||
|
||||
export default usePlayerGame;
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user