diff --git a/server/server.js b/server/server.js index fe73fdc..1653266 100644 --- a/server/server.js +++ b/server/server.js @@ -4,7 +4,7 @@ const path = require('path'); // --- Configuration --- -const PORT = process.env.PORT || 80; +const PORT = process.env.PORT || 3000; const DATA_DIR = path.join(__dirname, 'data'); // --- Data Helpers --- @@ -560,7 +560,6 @@ addRoute('GET', '/game-data/:gid', (req, res, params) => { // Helper: convert storage cache to API response format const toCacheApi = (cache, gid) => ({ id: cache.CacheId, - name: cache.Name || '', coordinates: { latitude: cache.Latitude, longitude: cache.Longitude }, radius: cache.TriggerMeters, clue: cache.Title, @@ -573,7 +572,6 @@ const toCacheApi = (cache, gid) => ({ // Helper: convert API payload to storage cache format const toCacheStorage = (data, cacheId) => ({ CacheId: cacheId, - Name: data.name || data.Name || '', Title: data.clue || data.Title || '', Latitude: data.latitude != null ? data.latitude : (data.Latitude || 0), Longitude: data.longitude != null ? data.longitude : (data.Longitude || 0), diff --git a/src/components/gameplay/PlayerMapView.js b/src/components/gameplay/PlayerMapView.js index b4f3dfb..7ac9e0c 100644 --- a/src/components/gameplay/PlayerMapView.js +++ b/src/components/gameplay/PlayerMapView.js @@ -1,6 +1,6 @@ import React from 'react'; -import {StyleSheet, View} from 'react-native'; -import MapView, {Circle, Marker} from 'react-native-maps'; +import MapView, {Circle, Polygon} from 'react-native-maps'; +import {getFovCone} from '../../utils/geoMath'; const PlayerMapView = ({ userLocation, visibleCache, heading }) => { // Initialisation ------------- @@ -10,6 +10,10 @@ const PlayerMapView = ({ userLocation, visibleCache, heading }) => { if (!userLocation) return null; + const coneCoords = (heading !== null && heading !== undefined) + ? getFovCone(userLocation, heading) + : null; + return ( { strokeColor="rgba(250, 204, 21, 0.90)" /> ) : null} - {heading !== null && heading !== undefined && ( - - - - - + {coneCoords && ( + )} ); }; -const styles = StyleSheet.create({ - coneWrap: {width: 22, height: 24, alignItems: 'center'}, - cone: { - width: 0, - height: 0, - borderTopWidth: 24, - borderLeftWidth: 11, - borderRightWidth: 11, - borderTopColor: 'rgba(37,99,235,0.70)', - borderLeftColor: 'transparent', - borderRightColor: 'transparent', - }, -}); export default PlayerMapView; diff --git a/src/components/screens/ExpandedMapScreen.js b/src/components/screens/ExpandedMapScreen.js index 148e829..4f3d41e 100644 --- a/src/components/screens/ExpandedMapScreen.js +++ b/src/components/screens/ExpandedMapScreen.js @@ -1,10 +1,10 @@ import React, {useEffect, useState} from 'react'; import {StyleSheet, View} from 'react-native'; -import MapView, {Circle, Marker} from 'react-native-maps'; +import MapView, {Circle, Marker, Polygon} from 'react-native-maps'; import * as Location from 'expo-location'; import {Magnetometer} from 'expo-sensors'; import Screen from '../layout/Screen'; -import {toHeading} from '../../utils/geoMath'; +import {getFovCone, toHeading} from '../../utils/geoMath'; const DEFAULT_REGION = {latitude: 51.5074, longitude: -0.1278, latitudeDelta: 0.01, longitudeDelta: 0.01}; @@ -54,6 +54,10 @@ const ExpandedMapScreen = ({route}) => { // View ----------------------- + const coneCoords = (userLocation && heading !== null) + ? getFovCone(userLocation, heading) + : null; + return ( @@ -78,19 +82,14 @@ const ExpandedMapScreen = ({route}) => { /> ))} - {/* Heading cone for both admin and player */} - {userLocation && heading !== null && ( - - - - - + {/* Heading FOV cone */} + {coneCoords && ( + )} @@ -101,17 +100,6 @@ const ExpandedMapScreen = ({route}) => { const styles = StyleSheet.create({ container: {padding: 0}, mapWrap: {flex: 1}, - coneWrap: {width: 22, height: 24, alignItems: 'center'}, - cone: { - width: 0, - height: 0, - borderTopWidth: 24, - borderLeftWidth: 11, - borderRightWidth: 11, - borderTopColor: 'rgba(37,99,235,0.70)', - borderLeftColor: 'transparent', - borderRightColor: 'transparent', - }, }); export default ExpandedMapScreen; diff --git a/src/components/screens/MapScreen.js b/src/components/screens/MapScreen.js index bd8f710..5fe5878 100644 --- a/src/components/screens/MapScreen.js +++ b/src/components/screens/MapScreen.js @@ -1,6 +1,6 @@ import React, {useCallback, useEffect, useState} from 'react'; import {ActivityIndicator, Pressable, ScrollView, StyleSheet, Text, TextInput, View} from 'react-native'; -import MapView, {Circle, Marker} from 'react-native-maps'; +import MapView, {Circle, Marker, Polygon} from 'react-native-maps'; import * as Location from 'expo-location'; import {Magnetometer} from 'expo-sensors'; import Screen from '../layout/Screen'; @@ -11,7 +11,7 @@ import PlayerMapView from '../gameplay/PlayerMapView'; import useGameHook from '../../hooks/useGameHook'; import usePlayerGame from '../../hooks/usePlayerGame'; import {getSession, setSessionGroup, setSessionUser} from '../../hooks/SessionStore'; -import {toHeading} from '../../utils/geoMath'; +import {getFovCone, toHeading} from '../../utils/geoMath'; const DEFAULT_REGION = {latitude: 51.5074, longitude: -0.1278, latitudeDelta: 0.05, longitudeDelta: 0.05}; @@ -378,17 +378,12 @@ const MapScreen = ({navigation}) => { ))} {userLocation && heading !== null && ( - - - - - + )} @@ -563,18 +558,6 @@ const styles = StyleSheet.create({ cacheSection: {flex: 1, backgroundColor: '#f3f4f6', paddingHorizontal: 12, paddingTop: 10}, emptyText: {color: '#9ca3af', textAlign: 'center', marginTop: 20, fontSize: 14}, mapWrap: {flex: 1}, - // Heading direction cone (like Google Maps) - headingConeWrap: {width: 22, height: 24, alignItems: 'center'}, - headingCone: { - width: 0, - height: 0, - borderTopWidth: 24, - borderLeftWidth: 11, - borderRightWidth: 11, - borderTopColor: 'rgba(37,99,235,0.70)', - borderLeftColor: 'transparent', - borderRightColor: 'transparent', - }, }); export default MapScreen; diff --git a/src/utils/geoMath.js b/src/utils/geoMath.js index b0c11ad..748d5a6 100644 --- a/src/utils/geoMath.js +++ b/src/utils/geoMath.js @@ -1,9 +1,34 @@ import * as geolib from 'geolib'; -// Convert raw Magnetometer {x, y} reading to a 0-360° heading +// --- FOV Configuration --- +const FOV_DISTANCE = 20; // metres — radius of arc + length of straight edges +const FOV_ANGLE = 45; // degrees — total field of view (halved for each side) + +// 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); - return angle < 0 ? angle + 360 : angle; + 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; + + // 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}); + } + + return points; }; const isWithinRadius = (playerCoords, cacheCoords, radius) => { @@ -20,5 +45,5 @@ const isLookingAtCache = (playerHeading, playerCoords, cacheCoords, tolerance = return diff <= tolerance || diff >= 360 - tolerance; }; -export { toHeading, isWithinRadius, isLookingAtCache }; +export { toHeading, getFovCone, isWithinRadius, isLookingAtCache };