mirror of
https://github.com/mudabbir-ahmad/MAD-Treasure-Hunt.git
synced 2026-10-08 03:00:20 +00:00
Refactor cache claim logic to use admin-defined claim distance; update related components for consistency
This commit is contained in:
1 parent
ecb61c4858
commit
2646c7b9bc
11 files changed
+98
-74
No files matched your search
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import MapView, {Circle, Marker} from 'react-native-maps';
|
||||
|
||||
const AdminCacheEditorView = ({ userLocation, caches, onAddCache, onMoveCache }) => {
|
||||
const AdminCacheEditorView = ({ userLocation, caches, claimDistance, onAddCache, onMoveCache }) => {
|
||||
// Initialisation -------------
|
||||
// State ----------------------
|
||||
// Handlers -------------------
|
||||
@@ -34,7 +34,7 @@ const AdminCacheEditorView = ({ userLocation, caches, onAddCache, onMoveCache })
|
||||
/>
|
||||
<Circle
|
||||
center={cache.coordinates}
|
||||
radius={cache.radius}
|
||||
radius={claimDistance}
|
||||
fillColor="rgba(59, 130, 246, 0.15)"
|
||||
strokeColor="rgba(59, 130, 246, 0.85)"
|
||||
/>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {StyleSheet, Text, View} from 'react-native';
|
||||
import {CACHE_CLAIM_TIMER} from '../../utils/geoMath';
|
||||
|
||||
const ClaimTimerView = ({ cache, onClaimSuccess, isClaiming }) => {
|
||||
// Initialisation -------------
|
||||
// State ----------------------
|
||||
|
||||
const [timeLeft, setTimeLeft] = useState(5);
|
||||
const [timeLeft, setTimeLeft] = useState(CACHE_CLAIM_TIMER);
|
||||
|
||||
// Handlers -------------------
|
||||
|
||||
@@ -16,7 +17,7 @@ const ClaimTimerView = ({ cache, onClaimSuccess, isClaiming }) => {
|
||||
} else if (isClaiming && timeLeft === 0 && cache) {
|
||||
onClaimSuccess(cache.id);
|
||||
} else {
|
||||
setTimeLeft(5);
|
||||
setTimeLeft(CACHE_CLAIM_TIMER);
|
||||
}
|
||||
return () => clearTimeout(timer);
|
||||
}, [isClaiming, timeLeft, cache, onClaimSuccess]);
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import MapView, {Circle, Polygon} from 'react-native-maps';
|
||||
import {getFovCone} from '../../utils/geoMath';
|
||||
|
||||
const PlayerMapView = ({ userLocation, visibleCache, heading }) => {
|
||||
const PlayerMapView = ({ userLocation, visibleCache, heading, claimDistance }) => {
|
||||
// Initialisation -------------
|
||||
// State ----------------------
|
||||
// Handlers -------------------
|
||||
@@ -31,7 +31,7 @@ const PlayerMapView = ({ userLocation, visibleCache, heading }) => {
|
||||
{visibleCache ? (
|
||||
<Circle
|
||||
center={visibleCache.coordinates}
|
||||
radius={visibleCache.radius}
|
||||
radius={claimDistance}
|
||||
fillColor="rgba(250, 204, 21, 0.20)"
|
||||
strokeColor="rgba(250, 204, 21, 0.90)"
|
||||
/>
|
||||
|
||||
@@ -11,8 +11,9 @@ const DEFAULT_REGION = {latitude: 51.5074, longitude: -0.1278, latitudeDelta: 0.
|
||||
const ExpandedMapScreen = ({route}) => {
|
||||
// Initialisation ------------
|
||||
|
||||
const {isAdmin, cacheRecords: cacheStr} = route.params || {};
|
||||
const {isAdmin, cacheRecords: cacheStr, claimDistance: routeClaimDistance} = route.params || {};
|
||||
const caches = cacheStr ? JSON.parse(cacheStr) : [];
|
||||
const claimDistance = routeClaimDistance || 20;
|
||||
|
||||
// State ----------------------
|
||||
|
||||
@@ -74,9 +75,9 @@ const ExpandedMapScreen = ({route}) => {
|
||||
title={cache.name || cache.clue}
|
||||
pinColor="#2563eb"
|
||||
/>
|
||||
<Circle
|
||||
<Circle
|
||||
center={cache.coordinates}
|
||||
radius={cache.radius}
|
||||
radius={claimDistance}
|
||||
fillColor="rgba(59,130,246,0.15)"
|
||||
strokeColor="rgba(59,130,246,0.85)"
|
||||
/>
|
||||
|
||||
@@ -19,6 +19,7 @@ const GameSettingsScreen = () => {
|
||||
const [groupName, setGroupName] = useState('');
|
||||
const [teamsEnabled, setTeamsEnabled] = useState(false);
|
||||
const [maxSubgroups, setMaxSubgroups] = useState('1');
|
||||
const [cacheTriggerMeters, setCacheTriggerMeters] = useState('20');
|
||||
const [adminJoinCode, setAdminJoinCode] = useState('');
|
||||
const [memberJoinCode, setMemberJoinCode] = useState('');
|
||||
|
||||
@@ -35,6 +36,7 @@ const GameSettingsScreen = () => {
|
||||
setGroupName(group.GroupName || '');
|
||||
setTeamsEnabled(Boolean(group.TeamsEnabled));
|
||||
setMaxSubgroups(String(group.MaxMemberSubgroups || 1));
|
||||
setCacheTriggerMeters(String(group.CacheTriggerMeters || 20));
|
||||
setAdminJoinCode(group.AdminJoinCode || '');
|
||||
}
|
||||
const sgs = await getSubgroups(session.currentGid);
|
||||
@@ -52,6 +54,7 @@ const GameSettingsScreen = () => {
|
||||
GroupName: groupName.trim(),
|
||||
TeamsEnabled: teamsEnabled,
|
||||
MaxMemberSubgroups: parseInt(maxSubgroups) || 1,
|
||||
CacheTriggerMeters: parseInt(cacheTriggerMeters) || 20,
|
||||
});
|
||||
setSaving(false);
|
||||
};
|
||||
@@ -112,6 +115,16 @@ const GameSettingsScreen = () => {
|
||||
</>
|
||||
)}
|
||||
|
||||
<Text style={styles.label}>Cache Claim Distance (metres)</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={cacheTriggerMeters}
|
||||
onChangeText={setCacheTriggerMeters}
|
||||
keyboardType="numeric"
|
||||
placeholder="20"
|
||||
placeholderTextColor="#9ca3af"
|
||||
/>
|
||||
|
||||
<View style={styles.codeSection}>
|
||||
<Text style={styles.codeLabel}>Admin Join Code</Text>
|
||||
<Text style={styles.codeValue}>{adminJoinCode || '—'}</Text>
|
||||
|
||||
@@ -10,7 +10,7 @@ const ManageGameScreen = () => {
|
||||
// Initialisation ------------
|
||||
|
||||
const session = getSession();
|
||||
const {getCaches, upsertCache} = useGameHook();
|
||||
const {getCaches, upsertCache, getLobby} = useGameHook();
|
||||
|
||||
// State ----------------------
|
||||
|
||||
@@ -18,6 +18,7 @@ const ManageGameScreen = () => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const [cacheRecords, setCacheRecords] = useState([]);
|
||||
const [claimDistance, setClaimDistance] = useState(20);
|
||||
|
||||
// Handlers -------------------
|
||||
|
||||
@@ -32,6 +33,13 @@ const ManageGameScreen = () => {
|
||||
|
||||
useEffect(() => { loadCaches(); }, [loadCaches]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!session.currentGid) return;
|
||||
getLobby(session.currentGid).then((group) => {
|
||||
if (group) setClaimDistance(group.CacheTriggerMeters || 20);
|
||||
});
|
||||
}, [session.currentGid]);
|
||||
|
||||
useEffect(() => {
|
||||
let locationSub;
|
||||
const start = async () => {
|
||||
@@ -62,7 +70,6 @@ const ManageGameScreen = () => {
|
||||
gid: session.currentGid,
|
||||
latitude: coordinate.latitude,
|
||||
longitude: coordinate.longitude,
|
||||
radius: 20,
|
||||
clue: `Cache ${cacheRecords.length + 1}`,
|
||||
subgroupId: 1,
|
||||
});
|
||||
@@ -78,7 +85,6 @@ const ManageGameScreen = () => {
|
||||
cacheId,
|
||||
latitude: coordinate.latitude,
|
||||
longitude: coordinate.longitude,
|
||||
radius: current.radius,
|
||||
clue: current.clue,
|
||||
subgroupId: current.subgroupId,
|
||||
});
|
||||
@@ -125,6 +131,7 @@ const ManageGameScreen = () => {
|
||||
<AdminCacheEditorView
|
||||
userLocation={userLocation}
|
||||
caches={cacheRecords}
|
||||
claimDistance={claimDistance}
|
||||
onAddCache={handleAddCache}
|
||||
onMoveCache={handleMoveCache}
|
||||
/>
|
||||
|
||||
@@ -40,13 +40,16 @@ const MapScreen = ({navigation}) => {
|
||||
const [newCoord, setNewCoord] = useState(null);
|
||||
const [newName, setNewName] = useState('');
|
||||
const [newClue, setNewClue] = useState('');
|
||||
const [newRadius, setNewRadius] = useState('20');
|
||||
|
||||
// Global claim distance comes from the group's CacheTriggerMeters setting
|
||||
const claimDistance = groupInfo?.CacheTriggerMeters || 20;
|
||||
|
||||
const isPlayer = inGame && !isAdmin;
|
||||
const {visibleCache, isClaiming, setIsClaiming} = usePlayerGame(
|
||||
isPlayer ? userLocation : null,
|
||||
isPlayer ? heading : null,
|
||||
isPlayer ? cacheRecords : [],
|
||||
claimDistance,
|
||||
);
|
||||
|
||||
const mapRegion = userLocation
|
||||
@@ -151,7 +154,6 @@ const MapScreen = ({navigation}) => {
|
||||
setNewCoord(fallback);
|
||||
setNewName('');
|
||||
setNewClue('');
|
||||
setNewRadius('20');
|
||||
setIsCreating(true);
|
||||
};
|
||||
|
||||
@@ -161,7 +163,6 @@ const MapScreen = ({navigation}) => {
|
||||
setNewCoord(cache.coordinates);
|
||||
setNewName(cache.name || '');
|
||||
setNewClue(cache.clue || '');
|
||||
setNewRadius(String(cache.radius || 20));
|
||||
setIsCreating(true);
|
||||
};
|
||||
|
||||
@@ -180,7 +181,6 @@ const MapScreen = ({navigation}) => {
|
||||
name: newName.trim(),
|
||||
latitude: newCoord.latitude,
|
||||
longitude: newCoord.longitude,
|
||||
radius: parseInt(newRadius) || 20,
|
||||
clue: newClue.trim(),
|
||||
subgroupId: 1,
|
||||
};
|
||||
@@ -202,6 +202,7 @@ const MapScreen = ({navigation}) => {
|
||||
isAdmin,
|
||||
cacheRecords: JSON.stringify(cacheRecords),
|
||||
heading: heading || 0,
|
||||
claimDistance,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -264,7 +265,7 @@ const MapScreen = ({navigation}) => {
|
||||
<Marker coordinate={cache.coordinates} pinColor="#9ca3af" title={cache.clue}/>
|
||||
<Circle
|
||||
center={cache.coordinates}
|
||||
radius={cache.radius}
|
||||
radius={claimDistance}
|
||||
fillColor="rgba(156,163,175,0.15)"
|
||||
strokeColor="rgba(156,163,175,0.6)"
|
||||
/>
|
||||
@@ -282,7 +283,7 @@ const MapScreen = ({navigation}) => {
|
||||
/>
|
||||
<Circle
|
||||
center={newCoord}
|
||||
radius={parseInt(newRadius) || 20}
|
||||
radius={claimDistance}
|
||||
fillColor="rgba(37,99,235,0.15)"
|
||||
strokeColor="rgba(37,99,235,0.85)"
|
||||
/>
|
||||
@@ -305,8 +306,7 @@ const MapScreen = ({navigation}) => {
|
||||
value={newClue}
|
||||
onChangeText={setNewClue}
|
||||
/>
|
||||
<View style={styles.coordRow}>
|
||||
<View style={styles.coordField}>
|
||||
<View style={styles.coordRow}> <View style={styles.coordField}>
|
||||
<Text style={styles.coordLabel}>Latitude</Text>
|
||||
<TextInput
|
||||
style={styles.formInput}
|
||||
@@ -327,14 +327,6 @@ const MapScreen = ({navigation}) => {
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<TextInput
|
||||
style={styles.formInput}
|
||||
placeholder="Claim Radius (metres)"
|
||||
placeholderTextColor="#9ca3af"
|
||||
value={newRadius}
|
||||
onChangeText={setNewRadius}
|
||||
keyboardType="numeric"
|
||||
/>
|
||||
<ButtonTray>
|
||||
<Button
|
||||
label={editingCacheId ? 'Update Cache' : 'Save Cache'}
|
||||
@@ -371,7 +363,7 @@ const MapScreen = ({navigation}) => {
|
||||
<Marker coordinate={cache.coordinates} title={cache.name || cache.clue}/>
|
||||
<Circle
|
||||
center={cache.coordinates}
|
||||
radius={cache.radius}
|
||||
radius={claimDistance}
|
||||
fillColor="rgba(59,130,246,0.15)"
|
||||
strokeColor="rgba(59,130,246,0.85)"
|
||||
/>
|
||||
@@ -450,6 +442,7 @@ const MapScreen = ({navigation}) => {
|
||||
userLocation={userLocation}
|
||||
visibleCache={visibleCache}
|
||||
heading={heading}
|
||||
claimDistance={claimDistance}
|
||||
/>
|
||||
<Pressable style={styles.expandButton} onPress={handleExpandMap}>
|
||||
<Text style={styles.expandIcon}>⛶</Text>
|
||||
|
||||
+10
-14
@@ -1,7 +1,7 @@
|
||||
import {useEffect, useState} from 'react';
|
||||
import {isLookingAtCache, isWithinRadius} from '../utils/geoMath';
|
||||
import {isInClaimCone} from '../utils/geoMath';
|
||||
|
||||
const usePlayerGame = (playerLocation, playerHeading, activeCaches) => {
|
||||
const usePlayerGame = (playerLocation, playerHeading, activeCaches, claimDistance) => {
|
||||
// State ----------------------
|
||||
|
||||
const [visibleCache, setVisibleCache] = useState(null);
|
||||
@@ -16,19 +16,15 @@ const usePlayerGame = (playerLocation, playerHeading, activeCaches) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const nearbyCache = (activeCaches || []).find((cache) =>
|
||||
isWithinRadius(playerLocation, cache.coordinates, cache.radius));
|
||||
// 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) =>
|
||||
isInClaimCone(playerHeading, playerLocation, cache.coordinates, claimDistance)
|
||||
);
|
||||
|
||||
if (!nearbyCache) {
|
||||
setVisibleCache(null);
|
||||
setIsClaiming(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const isTargeted = isLookingAtCache(playerHeading, playerLocation, nearbyCache.coordinates);
|
||||
setVisibleCache(nearbyCache);
|
||||
setIsClaiming(isTargeted);
|
||||
}, [playerLocation, playerHeading, activeCaches]);
|
||||
setVisibleCache(cacheInCone || null);
|
||||
setIsClaiming(Boolean(cacheInCone));
|
||||
}, [playerLocation, playerHeading, activeCaches, claimDistance]);
|
||||
|
||||
// Return ---------------------
|
||||
|
||||
|
||||
+42
-29
@@ -1,49 +1,62 @@
|
||||
import * as geolib from 'geolib';
|
||||
|
||||
// --- FOV Configuration ---
|
||||
const FOV_DISTANCE = 30; // metres — radius of arc + length of straight edges
|
||||
const FOV_ANGLE = 60; // degrees — total field of view (halved for each side)
|
||||
const FOV_SIZE = 50; // metres — radius of the visible FOV cone drawn on screen
|
||||
const FOV_ANGLE = 60; // degrees — total field of view (halved for each side)
|
||||
const CACHE_CLAIM_TIMER = 5; // seconds — how long the player must hold a cache in the claim cone to claim it
|
||||
|
||||
// Convert raw Magnetometer {x, y} reading to a 0-360° heading (0° = north)
|
||||
const toHeading = ({x, y}) => {
|
||||
const angle = Math.atan2(y, x) * (180 / Math.PI);
|
||||
const heading = 90 - angle;
|
||||
return heading < 0 ? heading + 360 : heading % 360;
|
||||
const angle = Math.atan2(y, x) * (180 / Math.PI);
|
||||
const heading = 90 - angle;
|
||||
return heading < 0 ? heading + 360 : heading % 360;
|
||||
};
|
||||
|
||||
// Build a pie-shaped FOV sector as polygon coordinates
|
||||
const getFovCone = (location, headingDeg, radiusMeters = FOV_DISTANCE, halfAngle = FOV_ANGLE / 2, arcSegments = 20) => {
|
||||
const origin = {latitude: location.latitude, longitude: location.longitude};
|
||||
const startBearing = headingDeg - halfAngle;
|
||||
const endBearing = headingDeg + halfAngle;
|
||||
const step = (endBearing - startBearing) / arcSegments;
|
||||
// radiusMeters defaults to FOV_SIZE (the visible cone) — pass a different value to build the invisible claim cone
|
||||
const getFovCone = (location, headingDeg, radiusMeters = FOV_SIZE, halfAngle = FOV_ANGLE / 2, arcSegments = 20) => {
|
||||
const origin = {latitude: location.latitude, longitude: location.longitude};
|
||||
const startBearing = headingDeg - halfAngle;
|
||||
const endBearing = headingDeg + halfAngle;
|
||||
const step = (endBearing - startBearing) / arcSegments;
|
||||
|
||||
// Tip of the sector
|
||||
const points = [location];
|
||||
// Tip of the sector
|
||||
const points = [location];
|
||||
|
||||
// Arc points at radiusMeters distance from origin
|
||||
for (let i = 0; i <= arcSegments; i++) {
|
||||
const bearing = startBearing + step * i;
|
||||
const dest = geolib.computeDestinationPoint(origin, radiusMeters, bearing);
|
||||
points.push({latitude: dest.latitude, longitude: dest.longitude});
|
||||
}
|
||||
// Arc points at radiusMeters distance from origin
|
||||
for (let i = 0; i <= arcSegments; i++) {
|
||||
const bearing = startBearing + step * i;
|
||||
const dest = geolib.computeDestinationPoint(origin, radiusMeters, bearing);
|
||||
points.push({latitude: dest.latitude, longitude: dest.longitude});
|
||||
}
|
||||
|
||||
return points;
|
||||
return points;
|
||||
};
|
||||
|
||||
const isWithinRadius = (playerCoords, cacheCoords, radius) => {
|
||||
const distance = geolib.getDistance(playerCoords, cacheCoords);
|
||||
return distance <= radius;
|
||||
const distance = geolib.getDistance(playerCoords, cacheCoords);
|
||||
return distance <= radius;
|
||||
};
|
||||
|
||||
const isLookingAtCache = (playerHeading, playerCoords, cacheCoords, tolerance = 20) => {
|
||||
if (playerHeading === null || playerHeading === undefined) {
|
||||
return false;
|
||||
}
|
||||
const bearingToCache = geolib.getRhumbLineBearing(playerCoords, cacheCoords);
|
||||
const diff = Math.abs(bearingToCache - playerHeading);
|
||||
return diff <= tolerance || diff >= 360 - tolerance;
|
||||
const isLookingAtCache = (playerHeading, playerCoords, cacheCoords, tolerance = FOV_ANGLE / 2) => {
|
||||
if (playerHeading === null || playerHeading === undefined) {
|
||||
return false;
|
||||
}
|
||||
const bearingToCache = geolib.getRhumbLineBearing(playerCoords, cacheCoords);
|
||||
const diff = Math.abs(bearingToCache - playerHeading);
|
||||
return diff <= tolerance || diff >= 360 - tolerance;
|
||||
};
|
||||
|
||||
export { toHeading, getFovCone, isWithinRadius, isLookingAtCache };
|
||||
// Checks whether a cache falls within the invisible claim sector
|
||||
// The claim sector uses the same FOV_ANGLE as the visible cone but extends to the
|
||||
// admin-defined claimDistance rather than the visible FOV_SIZE.
|
||||
// If the cache sits inside this sector the claiming countdown is triggered.
|
||||
const isInClaimCone = (playerHeading, playerCoords, cacheCoords, claimDistance) => {
|
||||
if (playerHeading === null || playerHeading === undefined) return false;
|
||||
// Cache must be within the admin-configured claim distance
|
||||
if (!isWithinRadius(playerCoords, cacheCoords, claimDistance)) return false;
|
||||
// Cache must also fall within the same angular width as the visible cone
|
||||
return isLookingAtCache(playerHeading, playerCoords, cacheCoords);
|
||||
};
|
||||
|
||||
export { toHeading, getFovCone, isWithinRadius, isLookingAtCache, isInClaimCone, FOV_ANGLE, FOV_SIZE, CACHE_CLAIM_TIMER };
|
||||
Reference in new issue
Block a user