mirror of
https://github.com/mudabbir-ahmad/MAD-Treasure-Hunt.git
synced 2026-10-08 03:00:20 +00:00
Enhance cache management and UI: implement multi-team cache claiming, add admin waitlist functionality, and improve leaderboard display for better user experience
This commit is contained in:
1 parent
e34cbba9b5
commit
9a9db74369
16 files changed
+636
-129
No files matched your search
@@ -2,12 +2,12 @@ import {StyleSheet, Text, View} from 'react-native';
|
||||
import Card from '../UI/Card';
|
||||
import {Button} from '../UI/Button';
|
||||
|
||||
const CacheCardItem = ({cache, isAdmin, onEdit, onDelete, onSelect}) => {
|
||||
const CacheCardItem = ({cache, isAdmin, isSelected, onEdit, onDelete, onSelect}) => {
|
||||
// Initialisation -------------
|
||||
|
||||
const isClaimed = Boolean(cache.ClaimedByUid);
|
||||
const statusText = isClaimed ? 'Claimed' : 'Available';
|
||||
const statusColor = isClaimed ? '#9ca3af' : '#16a34a';
|
||||
const hasClaims = (cache.Claims || []).length > 0;
|
||||
const statusText = hasClaims ? 'Claimed' : 'Available';
|
||||
const statusColor = hasClaims ? '#9ca3af' : '#16a34a';
|
||||
const displayName = cache.name || cache.clue || 'Unnamed Cache';
|
||||
|
||||
// State ----------------------
|
||||
@@ -15,7 +15,7 @@ const CacheCardItem = ({cache, isAdmin, onEdit, onDelete, onSelect}) => {
|
||||
// View -----------------------
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Card style={isSelected ? styles.selectedCard : null}>
|
||||
<View style={styles.topRow}>
|
||||
<View style={styles.nameWrap}>
|
||||
<Text style={styles.name} numberOfLines={1}>{displayName}</Text>
|
||||
@@ -41,6 +41,13 @@ const CacheCardItem = ({cache, isAdmin, onEdit, onDelete, onSelect}) => {
|
||||
styleLabel={styles.btnLabel}
|
||||
/>
|
||||
</>
|
||||
) : isSelected ? (
|
||||
<Button
|
||||
label="Cache Currently Selected"
|
||||
onClick={() => {}}
|
||||
styleButton={styles.selectedBtn}
|
||||
styleLabel={styles.selectedBtnLabel}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
label="Select Cache"
|
||||
@@ -87,6 +94,15 @@ const styles = StyleSheet.create({
|
||||
paddingHorizontal: 14,
|
||||
flex: 1,
|
||||
},
|
||||
selectedBtn: {
|
||||
backgroundColor: '#6b7280',
|
||||
borderColor: '#6b7280',
|
||||
minHeight: 36,
|
||||
paddingHorizontal: 14,
|
||||
flex: 1,
|
||||
},
|
||||
selectedBtnLabel: {color: '#ffffff', fontWeight: '600', fontSize: 13},
|
||||
selectedCard: {borderColor: '#2563eb', borderWidth: 2},
|
||||
btnLabel: {color: '#ffffff', fontWeight: '600', fontSize: 13},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,30 +1,42 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import React, {useEffect, useRef, useState} from 'react';
|
||||
import {StyleSheet, Text, View} from 'react-native';
|
||||
import {CACHE_CLAIM_TIMER} from '../../utils/geoMath';
|
||||
|
||||
const ClaimTimerView = ({ cache, onClaimSuccess, isClaiming }) => {
|
||||
const ClaimTimerView = ({ cache, onClaimSuccess, isClaiming, showClaimedPopup }) => {
|
||||
// Initialisation -------------
|
||||
// State ----------------------
|
||||
|
||||
const [timeLeft, setTimeLeft] = useState(CACHE_CLAIM_TIMER);
|
||||
// Use a ref for the callback so it does not appear in the dependency array
|
||||
const onClaimRef = useRef(onClaimSuccess);
|
||||
onClaimRef.current = onClaimSuccess;
|
||||
|
||||
// Handlers -------------------
|
||||
|
||||
useEffect(() => {
|
||||
let timer;
|
||||
if (isClaiming && timeLeft > 0) {
|
||||
if (isClaiming && cache && timeLeft > 0) {
|
||||
timer = setTimeout(() => setTimeLeft((prev) => prev - 1), 1000);
|
||||
} else if (isClaiming && timeLeft === 0 && cache) {
|
||||
onClaimSuccess(cache.id);
|
||||
} else {
|
||||
onClaimRef.current(cache.id);
|
||||
} else if (!isClaiming) {
|
||||
setTimeLeft(CACHE_CLAIM_TIMER);
|
||||
}
|
||||
return () => clearTimeout(timer);
|
||||
}, [isClaiming, timeLeft, cache, onClaimSuccess]);
|
||||
}, [isClaiming, timeLeft, cache]);
|
||||
|
||||
// View -----------------------
|
||||
|
||||
if (!isClaiming) {
|
||||
// Show "Cache Claimed!" popup after a successful claim
|
||||
if (showClaimedPopup) {
|
||||
return (
|
||||
<View style={styles.claimedOverlay}>
|
||||
<Text style={styles.claimedText}>Cache Claimed!</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isClaiming || !cache) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -49,6 +61,19 @@ const styles = StyleSheet.create({
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
},
|
||||
claimedOverlay: {
|
||||
alignSelf: 'center',
|
||||
backgroundColor: '#16a34a',
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 14,
|
||||
borderRadius: 10,
|
||||
marginTop: 8,
|
||||
},
|
||||
claimedText: {
|
||||
color: '#ffffff',
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
},
|
||||
});
|
||||
|
||||
export default ClaimTimerView;
|
||||
@@ -1,7 +1,8 @@
|
||||
import MapView, {Circle, Polygon} from 'react-native-maps';
|
||||
import React from 'react';
|
||||
import MapView, {Circle, Marker, Polygon} from 'react-native-maps';
|
||||
import {getFovCone} from '../../utils/geoMath';
|
||||
|
||||
const PlayerMapView = ({userLocation, visibleCache, heading, claimDistance}) => {
|
||||
const PlayerMapView = ({userLocation, visibleCaches, heading, claimDistance}) => {
|
||||
// Initialisation -------------
|
||||
// State ----------------------
|
||||
// Handlers -------------------
|
||||
@@ -23,14 +24,22 @@ const PlayerMapView = ({userLocation, visibleCache, heading, claimDistance}) =>
|
||||
pitchEnabled={false}
|
||||
showsUserLocation
|
||||
>
|
||||
{visibleCache && (
|
||||
<Circle
|
||||
center={visibleCache.coordinates}
|
||||
radius={claimDistance}
|
||||
fillColor="rgba(250, 204, 21, 0.20)"
|
||||
strokeColor="rgba(250, 204, 21, 0.90)"
|
||||
/>
|
||||
)}
|
||||
{/* Show cache markers for all caches currently inside the FOV cone */}
|
||||
{(visibleCaches || []).map((cache) => (
|
||||
<React.Fragment key={cache.id}>
|
||||
<Marker
|
||||
coordinate={cache.coordinates}
|
||||
title={cache.clue}
|
||||
pinColor="#facc15"
|
||||
/>
|
||||
<Circle
|
||||
center={cache.coordinates}
|
||||
radius={claimDistance}
|
||||
fillColor="rgba(250, 204, 21, 0.20)"
|
||||
strokeColor="rgba(250, 204, 21, 0.90)"
|
||||
/>
|
||||
</React.Fragment>
|
||||
))}
|
||||
{coneCoords && (
|
||||
<Polygon
|
||||
coordinates={coneCoords}
|
||||
|
||||
@@ -81,6 +81,7 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-around',
|
||||
paddingHorizontal: 8,
|
||||
zIndex: 10,
|
||||
},
|
||||
tab: {
|
||||
flex: 1,
|
||||
|
||||
@@ -48,6 +48,8 @@ const styles = StyleSheet.create({
|
||||
content: {
|
||||
flex: 1,
|
||||
padding: 15,
|
||||
zIndex: 1,
|
||||
overflow: "hidden",
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ const styles = StyleSheet.create({
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: 12,
|
||||
backgroundColor: '#000000',
|
||||
zIndex: 10,
|
||||
},
|
||||
leftSlot: {
|
||||
width: 72,
|
||||
|
||||
@@ -3,7 +3,7 @@ import {StyleSheet, View} from 'react-native';
|
||||
import MapView, {Circle, Marker, Polygon} from 'react-native-maps';
|
||||
import * as Location from 'expo-location';
|
||||
import Screen from '../layout/Screen';
|
||||
import {getFovCone} from '../../utils/geoMath';
|
||||
import {getFovCone, isInClaimCone} from '../../utils/geoMath';
|
||||
|
||||
const DEFAULT_REGION = {latitude: 51.5074, longitude: -0.1278, latitudeDelta: 0.01, longitudeDelta: 0.01};
|
||||
|
||||
@@ -70,6 +70,11 @@ const ExpandedMapScreen = ({route}) => {
|
||||
? getFovCone(userLocation, heading)
|
||||
: null;
|
||||
|
||||
// For players, show caches that currently fall within the FOV cone
|
||||
const visiblePlayerCaches = (!isAdmin && userLocation && heading !== null)
|
||||
? caches.filter((c) => isInClaimCone(heading, userLocation, c.coordinates, claimDistance))
|
||||
: [];
|
||||
|
||||
return (
|
||||
<Screen showBack={true} style={styles.container}>
|
||||
<View style={styles.mapWrap}>
|
||||
@@ -78,7 +83,7 @@ const ExpandedMapScreen = ({route}) => {
|
||||
initialRegion={region}
|
||||
showsUserLocation
|
||||
>
|
||||
{/* Only admins see cache pin locations */}
|
||||
{/* Admins see all cache pin locations */}
|
||||
{isAdmin && caches.map((cache) => (
|
||||
<React.Fragment key={cache.id}>
|
||||
<Marker
|
||||
@@ -94,6 +99,22 @@ const ExpandedMapScreen = ({route}) => {
|
||||
/>
|
||||
</React.Fragment>
|
||||
))}
|
||||
{/* Players only see caches that are inside the FOV cone */}
|
||||
{!isAdmin && visiblePlayerCaches.map((cache) => (
|
||||
<React.Fragment key={cache.id}>
|
||||
<Marker
|
||||
coordinate={cache.coordinates}
|
||||
title={cache.clue}
|
||||
pinColor="#facc15"
|
||||
/>
|
||||
<Circle
|
||||
center={cache.coordinates}
|
||||
radius={claimDistance}
|
||||
fillColor="rgba(250, 204, 21, 0.20)"
|
||||
strokeColor="rgba(250, 204, 21, 0.90)"
|
||||
/>
|
||||
</React.Fragment>
|
||||
))}
|
||||
{/* Heading FOV cone */}
|
||||
{coneCoords && (
|
||||
<Polygon
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {ActivityIndicator, ScrollView, StyleSheet, Switch, Text, TextInput, View} from 'react-native';
|
||||
import {ActivityIndicator, Alert, ScrollView, StyleSheet, Switch, Text, TextInput, View} from 'react-native';
|
||||
import Screen from '../layout/Screen';
|
||||
import Card from '../UI/Card';
|
||||
import {Button, ButtonTray} from '../UI/Button';
|
||||
import useGameHook from '../../hooks/useGameHook';
|
||||
import {getSession, setSessionTeamsEnabled} from '../../hooks/SessionStore';
|
||||
@@ -9,7 +10,7 @@ const GameSettingsScreen = () => {
|
||||
// Initialisation ------------
|
||||
|
||||
const session = getSession();
|
||||
const {getLobby, updateGroup, getSubgroups} = useGameHook();
|
||||
const {getLobby, updateGroup, getSubgroups, resetGame, getAdminWaitlist, approveAdmin, rejectAdmin, getUser} = useGameHook();
|
||||
const isBusiness = Boolean(session.isBusiness);
|
||||
|
||||
// State ----------------------
|
||||
@@ -22,9 +23,23 @@ const GameSettingsScreen = () => {
|
||||
const [cacheTriggerMeters, setCacheTriggerMeters] = useState('20');
|
||||
const [adminJoinCode, setAdminJoinCode] = useState('');
|
||||
const [memberJoinCode, setMemberJoinCode] = useState('');
|
||||
const [waitlist, setWaitlist] = useState([]);
|
||||
const [waitlistNames, setWaitlistNames] = useState({});
|
||||
|
||||
// Handlers -------------------
|
||||
|
||||
const loadWaitlist = async () => {
|
||||
if (!session.currentGid) return;
|
||||
const rows = await getAdminWaitlist(session.currentGid);
|
||||
setWaitlist(rows || []);
|
||||
const names = {};
|
||||
for (const entry of (rows || [])) {
|
||||
const u = await getUser(entry.Uid);
|
||||
if (u) names[entry.Uid] = u.username;
|
||||
}
|
||||
setWaitlistNames(names);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
if (!session.currentGid) {
|
||||
@@ -45,6 +60,7 @@ const GameSettingsScreen = () => {
|
||||
const sgs = await getSubgroups(session.currentGid);
|
||||
const memberSg = (sgs || []).find((sg) => !sg.IsAdminGroup);
|
||||
if (memberSg) setMemberJoinCode(memberSg.JoinCode || '');
|
||||
await loadWaitlist();
|
||||
setLoading(false);
|
||||
};
|
||||
load();
|
||||
@@ -64,6 +80,30 @@ const GameSettingsScreen = () => {
|
||||
setSaving(false);
|
||||
};
|
||||
|
||||
const handleResetGame = () => {
|
||||
Alert.alert('Reset Entire Game', 'This will clear ALL cache claims for every player. Are you sure?', [
|
||||
{text: 'Cancel', style: 'cancel'},
|
||||
{
|
||||
text: 'Reset',
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
await resetGame(session.currentGid);
|
||||
Alert.alert('Done', 'All cache claims have been reset.');
|
||||
},
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const handleApproveAdmin = async (entry) => {
|
||||
await approveAdmin(entry.id);
|
||||
await loadWaitlist();
|
||||
};
|
||||
|
||||
const handleRejectAdmin = async (entry) => {
|
||||
await rejectAdmin(entry.id);
|
||||
await loadWaitlist();
|
||||
};
|
||||
|
||||
// View -----------------------
|
||||
|
||||
if (loading) {
|
||||
@@ -150,6 +190,46 @@ const GameSettingsScreen = () => {
|
||||
/>
|
||||
</ButtonTray>
|
||||
</View>
|
||||
|
||||
{/* Admin Waitlist */}
|
||||
{waitlist.length > 0 && (
|
||||
<View style={styles.waitlistSection}>
|
||||
<Text style={styles.sectionTitle}>Admin Waitlist</Text>
|
||||
{waitlist.map((entry) => (
|
||||
<Card key={entry.id}>
|
||||
<View style={styles.waitlistRow}>
|
||||
<Text style={styles.waitlistName}>
|
||||
{waitlistNames[entry.Uid] || `User ${entry.Uid}`}
|
||||
</Text>
|
||||
<View style={styles.waitlistActions}>
|
||||
<Button
|
||||
label="Approve"
|
||||
onClick={() => handleApproveAdmin(entry)}
|
||||
styleButton={styles.approveButton}
|
||||
styleLabel={styles.approveBtnLabel}
|
||||
/>
|
||||
<Button
|
||||
label="Reject"
|
||||
onClick={() => handleRejectAdmin(entry)}
|
||||
styleButton={styles.rejectButton}
|
||||
styleLabel={styles.rejectBtnLabel}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</Card>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Reset Entire Game */}
|
||||
<View style={styles.resetSection}>
|
||||
<Button
|
||||
label="Reset Entire Game"
|
||||
onClick={handleResetGame}
|
||||
styleButton={styles.resetButton}
|
||||
styleLabel={styles.resetLabel}
|
||||
/>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</Screen>
|
||||
);
|
||||
@@ -196,6 +276,23 @@ const styles = StyleSheet.create({
|
||||
saveWrap: {marginTop: 24},
|
||||
saveButton: {backgroundColor: '#16a34a', borderColor: '#16a34a'},
|
||||
saveLabel: {color: '#ffffff', fontWeight: '600'},
|
||||
// Admin waitlist styles
|
||||
waitlistSection: {marginTop: 30},
|
||||
waitlistRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
},
|
||||
waitlistName: {fontSize: 15, fontWeight: '600', color: '#1f2937', flex: 1},
|
||||
waitlistActions: {flexDirection: 'row', gap: 6},
|
||||
approveButton: {backgroundColor: '#16a34a', borderColor: '#16a34a', minHeight: 36, flex: 0, paddingHorizontal: 10},
|
||||
approveBtnLabel: {color: '#ffffff', fontWeight: '600', fontSize: 13},
|
||||
rejectButton: {backgroundColor: '#dc2626', borderColor: '#dc2626', minHeight: 36, flex: 0, paddingHorizontal: 10},
|
||||
rejectBtnLabel: {color: '#ffffff', fontWeight: '600', fontSize: 13},
|
||||
// Reset game styles
|
||||
resetSection: {marginTop: 30},
|
||||
resetButton: {backgroundColor: '#dc2626', borderColor: '#dc2626'},
|
||||
resetLabel: {color: '#ffffff', fontWeight: '600'},
|
||||
});
|
||||
|
||||
export default GameSettingsScreen;
|
||||
|
||||
@@ -9,15 +9,17 @@ const LeaderboardScreen = () => {
|
||||
// Initialisation ------------
|
||||
|
||||
const session = getSession();
|
||||
const isAdmin = session.isAcceptedAdmin;
|
||||
const {getLobby, getTeams, getTeamMembers, getGroupMembers, getCaches, getUser} = useGameHook();
|
||||
|
||||
// State ----------------------
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [teamsEnabled, setTeamsEnabled] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState('global');
|
||||
const [globalRanking, setGlobalRanking] = useState([]);
|
||||
const [teamRanking, setTeamRanking] = useState([]);
|
||||
const [activeTab, setActiveTab] = useState('game');
|
||||
const [teamRankings, setTeamRankings] = useState([]);
|
||||
const [playerRankings, setPlayerRankings] = useState([]);
|
||||
const [myTeamRanking, setMyTeamRanking] = useState([]);
|
||||
const [expandedTeam, setExpandedTeam] = useState(null);
|
||||
|
||||
// Handlers -------------------
|
||||
@@ -30,7 +32,6 @@ const LeaderboardScreen = () => {
|
||||
setTeamsEnabled(isTeams);
|
||||
|
||||
const caches = await getCaches(session.currentGid, null);
|
||||
const claimedCaches = (caches || []).filter((c) => c.ClaimedByUid);
|
||||
|
||||
if (isTeams) {
|
||||
// Build team rankings
|
||||
@@ -42,7 +43,9 @@ const LeaderboardScreen = () => {
|
||||
const memberData = [];
|
||||
for (const m of (members || [])) {
|
||||
const user = await getUser(m.Uid);
|
||||
const count = claimedCaches.filter((c) => c.ClaimedByUid === m.Uid).length;
|
||||
const count = (caches || []).filter((c) =>
|
||||
(c.Claims || []).some((cl) => cl.Uid === m.Uid)
|
||||
).length;
|
||||
memberData.push({
|
||||
uid: m.Uid,
|
||||
name: user?.username || `Player ${m.Uid}`,
|
||||
@@ -51,7 +54,9 @@ const LeaderboardScreen = () => {
|
||||
});
|
||||
}
|
||||
memberData.sort((a, b) => b.caches - a.caches);
|
||||
const totalCaches = memberData.reduce((sum, m) => sum + m.caches, 0);
|
||||
const totalCaches = (caches || []).filter((c) =>
|
||||
(c.Claims || []).some((cl) => cl.Tid === team.Tid)
|
||||
).length;
|
||||
teamData.push({
|
||||
tid: team.Tid,
|
||||
name: team.TeamName || 'Unnamed Team',
|
||||
@@ -62,20 +67,22 @@ const LeaderboardScreen = () => {
|
||||
}
|
||||
|
||||
teamData.sort((a, b) => b.totalCaches - a.totalCaches);
|
||||
setGlobalRanking(teamData);
|
||||
setTeamRankings(teamData);
|
||||
|
||||
// Build current team ranking
|
||||
// Build current user's team ranking (for TEAM tab)
|
||||
if (session.currentTid) {
|
||||
const myTeam = teamData.find((t) => t.tid === session.currentTid);
|
||||
setTeamRanking(myTeam ? myTeam.members : []);
|
||||
setMyTeamRanking(myTeam ? myTeam.members : []);
|
||||
}
|
||||
} else {
|
||||
// Build player rankings (no teams)
|
||||
const allMembers = await getGroupMembers(session.currentGid);
|
||||
const playerData = [];
|
||||
for (const m of (allMembers || []).filter((m) => !m.IsAcceptedAdmin)) {
|
||||
for (const m of (allMembers || []).filter((mem) => !mem.IsAcceptedAdmin)) {
|
||||
const user = await getUser(m.Uid);
|
||||
const count = claimedCaches.filter((c) => c.ClaimedByUid === m.Uid).length;
|
||||
const count = (caches || []).filter((c) =>
|
||||
(c.Claims || []).some((cl) => cl.Uid === m.Uid)
|
||||
).length;
|
||||
playerData.push({
|
||||
uid: m.Uid,
|
||||
name: user?.username || `Player ${m.Uid}`,
|
||||
@@ -83,7 +90,7 @@ const LeaderboardScreen = () => {
|
||||
});
|
||||
}
|
||||
playerData.sort((a, b) => b.caches - a.caches);
|
||||
setGlobalRanking(playerData);
|
||||
setPlayerRankings(playerData);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
@@ -109,64 +116,109 @@ const LeaderboardScreen = () => {
|
||||
);
|
||||
}
|
||||
|
||||
// Teams enabled — tabbed view
|
||||
// Helper: render an expandable team card
|
||||
const renderTeamCard = (team, index) => (
|
||||
<Pressable key={team.tid} onPress={() => setExpandedTeam(expandedTeam === team.tid ? null : team.tid)}>
|
||||
<Card>
|
||||
<View style={styles.rankRow}>
|
||||
<Text style={styles.rank}>#{index + 1}</Text>
|
||||
<Text style={styles.rankName}>{team.name}</Text>
|
||||
<Text style={styles.score}>{team.totalCaches} cache(s)</Text>
|
||||
</View>
|
||||
{expandedTeam === team.tid && team.members.map((m) => (
|
||||
<View key={m.uid} style={styles.subRow}>
|
||||
<Text style={styles.subName}>
|
||||
{m.name}{m.isLeader ? ' (Team Leader)' : ''}
|
||||
</Text>
|
||||
<Text style={styles.subScore}>{m.caches}</Text>
|
||||
</View>
|
||||
))}
|
||||
</Card>
|
||||
</Pressable>
|
||||
);
|
||||
|
||||
// Helper: render a simple player card
|
||||
const renderPlayerCard = (player, index) => (
|
||||
<Card key={player.uid}>
|
||||
<View style={styles.rankRow}>
|
||||
<Text style={styles.rank}>#{index + 1}</Text>
|
||||
<Text style={styles.rankName}>{player.name}</Text>
|
||||
<Text style={styles.score}>{player.caches} cache(s)</Text>
|
||||
</View>
|
||||
</Card>
|
||||
);
|
||||
|
||||
// ---------- ADMIN LEADERBOARD ----------
|
||||
if (isAdmin) {
|
||||
// Teams enabled: show team list (no tabs for admin)
|
||||
if (teamsEnabled) {
|
||||
return (
|
||||
<Screen style={styles.container}>
|
||||
<ScrollView style={styles.listSection}>
|
||||
{teamRankings.map((team, index) => renderTeamCard(team, index))}
|
||||
{teamRankings.length === 0 && (
|
||||
<Text style={styles.emptyText}>No teams yet.</Text>
|
||||
)}
|
||||
</ScrollView>
|
||||
</Screen>
|
||||
);
|
||||
}
|
||||
|
||||
// Teams disabled: show player list
|
||||
return (
|
||||
<Screen style={styles.container}>
|
||||
<ScrollView style={styles.listSection}>
|
||||
{playerRankings.map((player, index) => renderPlayerCard(player, index))}
|
||||
{playerRankings.length === 0 && (
|
||||
<Text style={styles.emptyText}>No players yet.</Text>
|
||||
)}
|
||||
</ScrollView>
|
||||
</Screen>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------- PLAYER LEADERBOARD ----------
|
||||
|
||||
// Teams enabled — tabbed view (GAME / TEAM)
|
||||
if (teamsEnabled) {
|
||||
return (
|
||||
<Screen style={styles.container}>
|
||||
<View style={styles.tabRow}>
|
||||
<Pressable
|
||||
style={[styles.tabButton, activeTab === 'global' && styles.tabButtonActive]}
|
||||
onPress={() => setActiveTab('global')}
|
||||
style={[styles.tabButton, activeTab === 'game' && styles.tabButtonActive]}
|
||||
onPress={() => setActiveTab('game')}
|
||||
>
|
||||
<Text style={[styles.tabLabel, activeTab === 'global' && styles.tabLabelActive]}>Global</Text>
|
||||
<Text style={[styles.tabLabel, activeTab === 'game' && styles.tabLabelActive]}>GAME</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
style={[styles.tabButton, activeTab === 'team' && styles.tabButtonActive]}
|
||||
onPress={() => setActiveTab('team')}
|
||||
>
|
||||
<Text style={[styles.tabLabel, activeTab === 'team' && styles.tabLabelActive]}>Team</Text>
|
||||
<Text style={[styles.tabLabel, activeTab === 'team' && styles.tabLabelActive]}>TEAM</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
{activeTab === 'global' ? (
|
||||
{activeTab === 'game' ? (
|
||||
<ScrollView style={styles.listSection}>
|
||||
{globalRanking.map((team, index) => (
|
||||
<Pressable key={team.tid} onPress={() => setExpandedTeam(expandedTeam === team.tid ? null : team.tid)}>
|
||||
<Card>
|
||||
<View style={styles.rankRow}>
|
||||
<Text style={styles.rank}>#{index + 1}</Text>
|
||||
<Text style={styles.rankName}>{team.name}</Text>
|
||||
<Text style={styles.score}>{team.totalCaches} cache(s)</Text>
|
||||
</View>
|
||||
{expandedTeam === team.tid && team.members.map((m) => (
|
||||
<View key={m.uid} style={styles.subRow}>
|
||||
<Text style={styles.subName}>
|
||||
{m.name}{m.isLeader ? ' (Leader)' : ''}
|
||||
</Text>
|
||||
<Text style={styles.subScore}>{m.caches}</Text>
|
||||
</View>
|
||||
))}
|
||||
</Card>
|
||||
</Pressable>
|
||||
))}
|
||||
{globalRanking.length === 0 && (
|
||||
{teamRankings.map((team, index) => renderTeamCard(team, index))}
|
||||
{teamRankings.length === 0 && (
|
||||
<Text style={styles.emptyText}>No teams yet.</Text>
|
||||
)}
|
||||
</ScrollView>
|
||||
) : (
|
||||
<ScrollView style={styles.listSection}>
|
||||
{teamRanking.map((m, index) => (
|
||||
{myTeamRanking.map((m, index) => (
|
||||
<Card key={m.uid}>
|
||||
<View style={styles.rankRow}>
|
||||
<Text style={styles.rank}>#{index + 1}</Text>
|
||||
<Text style={styles.rankName}>
|
||||
{m.name}{m.isLeader ? ' (Leader)' : ''}
|
||||
{m.name}{m.isLeader ? ' (Team Leader)' : ''}
|
||||
</Text>
|
||||
<Text style={styles.score}>{m.caches} cache(s)</Text>
|
||||
</View>
|
||||
</Card>
|
||||
))}
|
||||
{teamRanking.length === 0 && (
|
||||
{myTeamRanking.length === 0 && (
|
||||
<Text style={styles.emptyText}>Join a team to see your team leaderboard.</Text>
|
||||
)}
|
||||
</ScrollView>
|
||||
@@ -175,20 +227,12 @@ const LeaderboardScreen = () => {
|
||||
);
|
||||
}
|
||||
|
||||
// No teams — simple player ranking
|
||||
// No teams — simple player ranking (GAME tab only, no TEAM tab)
|
||||
return (
|
||||
<Screen style={styles.container}>
|
||||
<ScrollView style={styles.listSection}>
|
||||
{globalRanking.map((player, index) => (
|
||||
<Card key={player.uid}>
|
||||
<View style={styles.rankRow}>
|
||||
<Text style={styles.rank}>#{index + 1}</Text>
|
||||
<Text style={styles.rankName}>{player.name}</Text>
|
||||
<Text style={styles.score}>{player.caches} cache(s)</Text>
|
||||
</View>
|
||||
</Card>
|
||||
))}
|
||||
{globalRanking.length === 0 && (
|
||||
{playerRankings.map((player, index) => renderPlayerCard(player, index))}
|
||||
{playerRankings.length === 0 && (
|
||||
<Text style={styles.emptyText}>No players yet.</Text>
|
||||
)}
|
||||
</ScrollView>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, {useCallback, useEffect, useState} from 'react';
|
||||
import React, {useCallback, useEffect, useMemo, useState} from 'react';
|
||||
import {ActivityIndicator, Pressable, ScrollView, StyleSheet, Text, TextInput, View} from 'react-native';
|
||||
import MapView, {Circle, Marker, Polygon} from 'react-native-maps';
|
||||
import * as Location from 'expo-location';
|
||||
@@ -9,7 +9,7 @@ import ClaimTimerView from '../gameplay/ClaimTimerView';
|
||||
import PlayerMapView from '../gameplay/PlayerMapView';
|
||||
import useGameHook from '../../hooks/useGameHook';
|
||||
import usePlayerGame from '../../hooks/usePlayerGame';
|
||||
import {getSession, setSessionGroup, setSessionUser} from '../../hooks/SessionStore';
|
||||
import {getSession, setSelectedCache, setSessionGroup, setSessionUser} from '../../hooks/SessionStore';
|
||||
import {getFovCone} from '../../utils/geoMath';
|
||||
|
||||
const DEFAULT_REGION = {latitude: 51.5074, longitude: -0.1278, latitudeDelta: 0.05, longitudeDelta: 0.05};
|
||||
@@ -32,6 +32,8 @@ const MapScreen = ({navigation}) => {
|
||||
const [error, setError] = useState('');
|
||||
const [gameCode, setGameCode] = useState('');
|
||||
const [groupInfo, setGroupInfo] = useState(null);
|
||||
const [selectedCacheId, setSelectedCacheIdState] = useState(session.selectedCacheId);
|
||||
const [claimedPopupVisible, setClaimedPopupVisible] = useState(false);
|
||||
|
||||
// Admin cache creation / editing state
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
@@ -44,10 +46,23 @@ const MapScreen = ({navigation}) => {
|
||||
const claimDistance = groupInfo?.CacheTriggerMeters || 20;
|
||||
|
||||
const isPlayer = inGame && !isAdmin;
|
||||
const {visibleCache, isClaiming, setIsClaiming} = usePlayerGame(
|
||||
|
||||
// Filter out caches already claimed by the current user's team (or by the user if no team)
|
||||
const activeCachesForPlayer = useMemo(() => {
|
||||
if (!isPlayer) return [];
|
||||
return cacheRecords.filter((cache) => {
|
||||
const claims = cache.Claims || [];
|
||||
if (session.currentTid) {
|
||||
return !claims.some((c) => c.Tid === session.currentTid);
|
||||
}
|
||||
return !claims.some((c) => c.Uid === session.currentUid);
|
||||
});
|
||||
}, [cacheRecords, isPlayer, session.currentTid, session.currentUid]);
|
||||
|
||||
const {visibleCaches, isClaiming, setIsClaiming} = usePlayerGame(
|
||||
isPlayer ? userLocation : null,
|
||||
isPlayer ? heading : null,
|
||||
isPlayer ? cacheRecords : [],
|
||||
isPlayer ? activeCachesForPlayer : [],
|
||||
claimDistance,
|
||||
);
|
||||
|
||||
@@ -73,6 +88,17 @@ const MapScreen = ({navigation}) => {
|
||||
|
||||
useEffect(() => { loadCaches(); }, [loadCaches]);
|
||||
|
||||
// Auto-select the first cache for the player when caches load and none is selected
|
||||
useEffect(() => {
|
||||
if (!isPlayer || cacheRecords.length === 0) return;
|
||||
// If persisted selection still exists in the list, keep it
|
||||
if (selectedCacheId && cacheRecords.some((c) => c.id === selectedCacheId)) return;
|
||||
// Otherwise auto-select the first cache
|
||||
const firstId = cacheRecords[0].id;
|
||||
setSelectedCacheIdState(firstId);
|
||||
setSelectedCache(firstId);
|
||||
}, [cacheRecords, isPlayer]);
|
||||
|
||||
// Location tracking
|
||||
useEffect(() => {
|
||||
if (!inGame) return;
|
||||
@@ -118,6 +144,7 @@ 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);
|
||||
@@ -143,17 +170,22 @@ const MapScreen = ({navigation}) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleClaim = async (cacheId) => {
|
||||
const handleClaim = useCallback(async (cacheId) => {
|
||||
if (!session.currentGid) return;
|
||||
await claimCache({
|
||||
const result = await claimCache({
|
||||
gid: session.currentGid,
|
||||
cacheId,
|
||||
uid: session.currentUid,
|
||||
tid: session.currentTid,
|
||||
});
|
||||
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 = () => {
|
||||
@@ -215,9 +247,10 @@ const MapScreen = ({navigation}) => {
|
||||
});
|
||||
};
|
||||
|
||||
// Player — select cache
|
||||
// Player — select cache (persist selection)
|
||||
const handleSelectCache = (cache) => {
|
||||
// Future: scroll map to cache location
|
||||
setSelectedCacheIdState(cache.id);
|
||||
setSelectedCache(cache.id);
|
||||
};
|
||||
|
||||
// View -----------------------
|
||||
@@ -232,7 +265,7 @@ const MapScreen = ({navigation}) => {
|
||||
placeholder="Enter Game Code"
|
||||
placeholderTextColor="#9ca3af"
|
||||
value={gameCode}
|
||||
onChangeText={setGameCode}
|
||||
onChangeText={(text) => setGameCode(text.toUpperCase())}
|
||||
autoCapitalize="characters"
|
||||
/>
|
||||
<Button
|
||||
@@ -439,8 +472,9 @@ const MapScreen = ({navigation}) => {
|
||||
);
|
||||
}
|
||||
|
||||
// Player in game
|
||||
// Player in game — use the first visible cache for the claim timer
|
||||
const teamsWarning = groupInfo?.TeamsEnabled && !session.currentTid;
|
||||
const claimTarget = visibleCaches.length > 0 ? visibleCaches[0] : null;
|
||||
|
||||
return (
|
||||
<Screen style={styles.containerMap}>
|
||||
@@ -450,7 +484,7 @@ const MapScreen = ({navigation}) => {
|
||||
<View style={styles.mapContainer}>
|
||||
<PlayerMapView
|
||||
userLocation={userLocation}
|
||||
visibleCache={visibleCache}
|
||||
visibleCaches={visibleCaches}
|
||||
heading={heading}
|
||||
claimDistance={claimDistance}
|
||||
/>
|
||||
@@ -459,9 +493,10 @@ const MapScreen = ({navigation}) => {
|
||||
</Pressable>
|
||||
</View>
|
||||
<ClaimTimerView
|
||||
cache={visibleCache}
|
||||
cache={claimTarget}
|
||||
isClaiming={isClaiming}
|
||||
onClaimSuccess={handleClaim}
|
||||
showClaimedPopup={claimedPopupVisible}
|
||||
/>
|
||||
<View style={styles.cacheSection}>
|
||||
<ScrollView>
|
||||
@@ -470,6 +505,7 @@ const MapScreen = ({navigation}) => {
|
||||
key={cache.id}
|
||||
cache={cache}
|
||||
isAdmin={false}
|
||||
isSelected={selectedCacheId === cache.id}
|
||||
onSelect={handleSelectCache}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, {useCallback, useEffect, useState} from 'react';
|
||||
import {ActivityIndicator, ScrollView, StyleSheet, Text, View} from 'react-native';
|
||||
import {ActivityIndicator, Alert, ScrollView, StyleSheet, Text, View} from 'react-native';
|
||||
import Screen from '../layout/Screen';
|
||||
import Card from '../UI/Card';
|
||||
import {Button} from '../UI/Button';
|
||||
@@ -10,7 +10,7 @@ const PlayersScreen = () => {
|
||||
// Initialisation ------------
|
||||
|
||||
const session = getSession();
|
||||
const {getTeams, getTeamMembers, getGroupMembers, removeMember, getUser} = useGameHook();
|
||||
const {getTeams, getTeamMembers, getGroupMembers, removeMember, resetPlayerProgress, getUser} = useGameHook();
|
||||
|
||||
// State ----------------------
|
||||
|
||||
@@ -63,6 +63,24 @@ const PlayersScreen = () => {
|
||||
await loadData();
|
||||
};
|
||||
|
||||
const handleResetPlayerProgress = (player) => {
|
||||
Alert.alert(
|
||||
'Reset Progress',
|
||||
`Reset all cache claims for ${player.username}?`,
|
||||
[
|
||||
{text: 'Cancel', style: 'cancel'},
|
||||
{
|
||||
text: 'Reset',
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
await resetPlayerProgress(session.currentGid, player.Uid);
|
||||
Alert.alert('Done', `Progress for ${player.username} has been reset.`);
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
};
|
||||
|
||||
// View -----------------------
|
||||
|
||||
if (loading) {
|
||||
@@ -97,11 +115,19 @@ const PlayersScreen = () => {
|
||||
<Text style={styles.teamCodeBadge}>{player.team.teamCode}</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.actionRow}>
|
||||
<Button
|
||||
label="Reset Progress"
|
||||
onClick={() => handleResetPlayerProgress(player)}
|
||||
styleButton={styles.resetButton}
|
||||
styleLabel={styles.actionLabel}
|
||||
/>
|
||||
<Button
|
||||
label="Kick"
|
||||
onClick={() => handleRemovePlayer(player.membershipId)}
|
||||
styleButton={styles.removeButton}
|
||||
styleLabel={styles.removeLabel}
|
||||
styleLabel={styles.actionLabel}
|
||||
/>
|
||||
</View>
|
||||
</Card>
|
||||
@@ -132,14 +158,16 @@ const styles = StyleSheet.create({
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
marginBottom: 8,
|
||||
},
|
||||
playerName: {fontSize: 15, fontWeight: '600', color: '#1f2937', flex: 1},
|
||||
playerMeta: {flexDirection: 'row', alignItems: 'center', gap: 6},
|
||||
leaderBadge: {fontSize: 11, color: '#2563eb', fontWeight: '700'},
|
||||
teamCodeBadge: {fontSize: 12, fontWeight: '700', color: '#16a34a', letterSpacing: 1},
|
||||
removeButton: {backgroundColor: '#dc2626', borderColor: '#dc2626', minHeight: 36, flex: 0, paddingHorizontal: 12},
|
||||
removeLabel: {color: '#ffffff', fontWeight: '600', fontSize: 13},
|
||||
actionRow: {flexDirection: 'row', gap: 8},
|
||||
resetButton: {backgroundColor: '#f59e0b', borderColor: '#f59e0b', minHeight: 36, flex: 1, paddingHorizontal: 10},
|
||||
removeButton: {backgroundColor: '#dc2626', borderColor: '#dc2626', minHeight: 36, flex: 1, paddingHorizontal: 10},
|
||||
actionLabel: {color: '#ffffff', fontWeight: '600', fontSize: 13},
|
||||
});
|
||||
|
||||
export default PlayersScreen;
|
||||
|
||||
@@ -11,7 +11,7 @@ const TeamScreen = () => {
|
||||
|
||||
const session = getSession();
|
||||
const isAdmin = session.isAcceptedAdmin;
|
||||
const {getTeam, createTeam, joinTeamByCode, getTeamMembers, leaveTeam, getUser, updateUser} = useGameHook();
|
||||
const {getTeam, createTeam, joinTeamByCode, getTeamMembers, leaveTeam, getUser, updateUser, getAdminWaitlist} = useGameHook();
|
||||
|
||||
// State ----------------------
|
||||
|
||||
@@ -21,6 +21,7 @@ const TeamScreen = () => {
|
||||
const [members, setMembers] = useState([]);
|
||||
const [memberNames, setMemberNames] = useState({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [onAdminWaitlist, setOnAdminWaitlist] = useState(false);
|
||||
|
||||
// Handlers -------------------
|
||||
|
||||
@@ -39,6 +40,15 @@ const TeamScreen = () => {
|
||||
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
// Check if the user is on the admin waitlist
|
||||
if (session.currentGid && !activeTid) {
|
||||
const waitlist = await getAdminWaitlist(session.currentGid, session.currentUid);
|
||||
if (waitlist.length > 0) {
|
||||
setOnAdminWaitlist(true);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (activeTid) await loadTeamData(activeTid);
|
||||
setLoading(false);
|
||||
};
|
||||
@@ -47,8 +57,17 @@ const TeamScreen = () => {
|
||||
|
||||
const handleJoinTeam = async () => {
|
||||
if (!teamCode.trim()) return;
|
||||
const result = await joinTeamByCode({JoinCode: teamCode.trim().toUpperCase(), Uid: session.currentUid});
|
||||
// Force uppercase just in case a lowercase code is pasted
|
||||
const code = teamCode.trim().toUpperCase();
|
||||
const result = await joinTeamByCode({JoinCode: code, Uid: session.currentUid});
|
||||
if (!result) return;
|
||||
|
||||
// If the server indicates this code matched the admin join code
|
||||
if (result.adminWaitlist) {
|
||||
setOnAdminWaitlist(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setSessionTeam(result.Tid);
|
||||
setActiveTid(result.Tid);
|
||||
};
|
||||
@@ -95,6 +114,15 @@ const TeamScreen = () => {
|
||||
);
|
||||
}
|
||||
|
||||
// Waiting for admin approval
|
||||
if (onAdminWaitlist) {
|
||||
return (
|
||||
<Screen style={styles.center}>
|
||||
<Text style={styles.waitlistText}>Waiting to be approved for Admin Team</Text>
|
||||
</Screen>
|
||||
);
|
||||
}
|
||||
|
||||
// Not in a team
|
||||
if (!activeTid || !team) {
|
||||
return (
|
||||
@@ -105,7 +133,7 @@ const TeamScreen = () => {
|
||||
placeholder="Enter Team Code"
|
||||
placeholderTextColor="#9ca3af"
|
||||
value={teamCode}
|
||||
onChangeText={setTeamCode}
|
||||
onChangeText={(text) => setTeamCode(text.toUpperCase())}
|
||||
autoCapitalize="characters"
|
||||
/>
|
||||
<Button
|
||||
@@ -195,6 +223,13 @@ const styles = StyleSheet.create({
|
||||
joinLabel: {color: '#ffffff', fontWeight: '600'},
|
||||
createButton: {backgroundColor: '#16a34a', borderColor: '#16a34a'},
|
||||
createLabel: {color: '#ffffff', fontWeight: '600'},
|
||||
waitlistText: {
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
color: '#374151',
|
||||
textAlign: 'center',
|
||||
paddingHorizontal: 30,
|
||||
},
|
||||
teamHeader: {
|
||||
backgroundColor: '#374151',
|
||||
paddingVertical: 14,
|
||||
|
||||
@@ -12,6 +12,7 @@ const session = {
|
||||
isBusiness: null,
|
||||
isAcceptedAdmin: false,
|
||||
teamsEnabled: false,
|
||||
selectedCacheId: null,
|
||||
};
|
||||
|
||||
// --- Handlers ---
|
||||
@@ -44,6 +45,11 @@ const setSessionTeamsEnabled = (val) => {
|
||||
persistSession();
|
||||
};
|
||||
|
||||
const setSelectedCache = (cacheId) => {
|
||||
session.selectedCacheId = cacheId ?? null;
|
||||
persistSession();
|
||||
};
|
||||
|
||||
const getSession = () => ({ ...session });
|
||||
|
||||
const clearSession = () => {
|
||||
@@ -63,6 +69,7 @@ const clearGameSession = () => {
|
||||
session.currentTid = null;
|
||||
session.isAcceptedAdmin = false;
|
||||
session.teamsEnabled = false;
|
||||
session.selectedCacheId = null;
|
||||
persistSession();
|
||||
};
|
||||
|
||||
@@ -85,4 +92,4 @@ const loadSession = async () => {
|
||||
return { ...session };
|
||||
};
|
||||
|
||||
export { setSessionUser, setSessionGroup, setSessionTeam, setSessionTeamsEnabled, getSession, clearSession, clearGameSession, loadSession };
|
||||
export { setSessionUser, setSessionGroup, setSessionTeam, setSessionTeamsEnabled, setSelectedCache, getSession, clearSession, clearGameSession, loadSession };
|
||||
@@ -10,6 +10,7 @@ const useGameHook = () => {
|
||||
const teamsEndpoint = `${API_BASE_URL}/teams`;
|
||||
const teamMembersEndpoint = `${API_BASE_URL}/team-members`;
|
||||
const gameDataEndpoint = `${API_BASE_URL}/game-data`;
|
||||
const adminWaitlistEndpoint = `${API_BASE_URL}/admin-waitlist`;
|
||||
|
||||
// Handlers -------------------
|
||||
|
||||
@@ -116,10 +117,11 @@ const useGameHook = () => {
|
||||
return response.isSuccess ? response.result : null;
|
||||
};
|
||||
|
||||
// Claim a cache via the multi-team claim endpoint
|
||||
const claimCache = async (payload) => {
|
||||
const response = await API.put(
|
||||
`${gameDataEndpoint}/${payload.gid}/caches/${payload.cacheId}`,
|
||||
{ClaimedByUid: payload.uid, ClaimedByTid: payload.tid},
|
||||
const response = await API.post(
|
||||
`${gameDataEndpoint}/${payload.gid}/caches/${payload.cacheId}/claim`,
|
||||
{Uid: payload.uid, Tid: payload.tid},
|
||||
);
|
||||
return response.isSuccess ? response.result : null;
|
||||
};
|
||||
@@ -129,6 +131,36 @@ const useGameHook = () => {
|
||||
return response.isSuccess;
|
||||
};
|
||||
|
||||
// Game reset — clear all claims for a game
|
||||
const resetGame = async (gid) => {
|
||||
const response = await API.post(`${gameDataEndpoint}/${gid}/reset`, {});
|
||||
return response.isSuccess;
|
||||
};
|
||||
|
||||
// Player progress reset — clear all claims by a specific user
|
||||
const resetPlayerProgress = async (gid, uid) => {
|
||||
const response = await API.post(`${gameDataEndpoint}/${gid}/reset-player/${uid}`, {});
|
||||
return response.isSuccess;
|
||||
};
|
||||
|
||||
// Admin waitlist
|
||||
const getAdminWaitlist = async (gid, uid = null) => {
|
||||
let url = `${adminWaitlistEndpoint}?Gid=${gid}`;
|
||||
if (uid !== null) url += `&Uid=${uid}`;
|
||||
const response = await API.get(url);
|
||||
return response.isSuccess ? response.result : [];
|
||||
};
|
||||
|
||||
const approveAdmin = async (waitlistId) => {
|
||||
const response = await API.post(`${adminWaitlistEndpoint}/${waitlistId}/approve`, {});
|
||||
return response.isSuccess ? response.result : null;
|
||||
};
|
||||
|
||||
const rejectAdmin = async (waitlistId) => {
|
||||
const response = await API.delete(`${adminWaitlistEndpoint}/${waitlistId}`);
|
||||
return response.isSuccess;
|
||||
};
|
||||
|
||||
// Return ---------------------
|
||||
|
||||
return {
|
||||
@@ -151,6 +183,11 @@ const useGameHook = () => {
|
||||
upsertCache,
|
||||
claimCache,
|
||||
deleteCache,
|
||||
resetGame,
|
||||
resetPlayerProgress,
|
||||
getAdminWaitlist,
|
||||
approveAdmin,
|
||||
rejectAdmin,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
+18
-10
@@ -1,34 +1,42 @@
|
||||
import {useEffect, useState} from 'react';
|
||||
import {useEffect, useRef, useState} from 'react';
|
||||
import {isInClaimCone} from '../utils/geoMath';
|
||||
|
||||
const usePlayerGame = (playerLocation, playerHeading, activeCaches, claimDistance) => {
|
||||
// State ----------------------
|
||||
|
||||
const [visibleCache, setVisibleCache] = useState(null);
|
||||
const [visibleCaches, setVisibleCaches] = useState([]);
|
||||
const [isClaiming, setIsClaiming] = useState(false);
|
||||
const prevIdsRef = useRef('');
|
||||
|
||||
// Handlers -------------------
|
||||
|
||||
useEffect(() => {
|
||||
if (!playerLocation || playerHeading === null || playerHeading === undefined) {
|
||||
setVisibleCache(null);
|
||||
setIsClaiming(false);
|
||||
if (prevIdsRef.current !== '') {
|
||||
prevIdsRef.current = '';
|
||||
setVisibleCaches([]);
|
||||
setIsClaiming(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the first cache that falls inside the invisible claim cone
|
||||
// (same FOV angle as the visible cone, but radius = admin-set claimDistance)
|
||||
const cacheInCone = (activeCaches || []).find((cache) =>
|
||||
// Find all caches that fall inside the claim cone
|
||||
const inCone = (activeCaches || []).filter((cache) =>
|
||||
isInClaimCone(playerHeading, playerLocation, cache.coordinates, claimDistance)
|
||||
);
|
||||
|
||||
setVisibleCache(cacheInCone || null);
|
||||
setIsClaiming(Boolean(cacheInCone));
|
||||
// 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);
|
||||
setIsClaiming(inCone.length > 0);
|
||||
}
|
||||
}, [playerLocation, playerHeading, activeCaches, claimDistance]);
|
||||
|
||||
// Return ---------------------
|
||||
|
||||
return {visibleCache, isClaiming, setIsClaiming};
|
||||
return {visibleCaches, isClaiming, setIsClaiming};
|
||||
};
|
||||
|
||||
export default usePlayerGame;
|
||||
|
||||
Reference in new issue
Block a user