// React import { useState, useEffect, useRef, Fragment } from 'react'; import { ScrollView, View, Text, Image, Alert, StyleSheet, TouchableOpacity, TouchableHighlight } from 'react-native'; import MapView, { Marker, Circle, Polygon } from 'react-native-maps'; // Expo import { useRouter } from 'expo-router'; // Components import CustomImage from '../components/image'; import CustomTextInput from '../components/input'; import Stat from '../components/stat'; import Collapsible from 'react-native-collapsible'; import LinearGradient from 'react-native-linear-gradient'; // Other import { useSocket } from '../context/socketContext'; import { useTeamContext } from '../context/teamContext'; import { useTeamConnexion } from '../context/teamConnexionContext'; import { useTimeDifference } from '../hook/useTimeDifference'; import { GameState } from '../util/gameState'; import useGame from '../hook/useGame'; const backgroundColor = '#f5f5f5'; const initialRegion = {latitude: 48.864, longitude: 2.342, latitudeDelta: 0, longitudeDelta: 50} // France centrée sur Paris const zoneTypes = { circle: "circle", polygon: "polygon" } export default function Display() { const arrowUp = require('../assets/images/arrow.png'); const [collapsibleState, setCollapsibleState] = useState(true); const [bottomContainerHeight, setBottomContainerHeight] = useState(0); const router = useRouter(); const {SERVER_URL} = useSocket(); const {messages, zoneType, zoneExtremities, nextZoneDate, isShrinking, location, startLocationTracking, stopLocationTracking, gameState, startDate} = useTeamContext(); const {loggedIn, logout, loading} = useTeamConnexion(); const {sendCurrentPosition, capture, enemyLocation, enemyName, startingArea, captureCode, name, ready, captured, lastSentLocation, locationSendDeadline, teamId, outOfZone, outOfZoneDeadline, distance, finishDate, nCaptures, nSentLocation, hasHandicap, enemyHasHandicap} = useGame(); const [enemyCaptureCode, setEnemyCaptureCode] = useState(""); const [timeLeftSendLocation] = useTimeDifference(locationSendDeadline, 1000); const [timeLeftNextZone] = useTimeDifference(nextZoneDate, 1000); const [timeLeftOutOfZone] = useTimeDifference(outOfZoneDeadline, 1000); const [timeSinceStart] = useTimeDifference(startDate, 1000); const [avgSpeed, setAvgSpeed] = useState(0); // Speed in m/s const [enemyImageURI, setEnemyImageURI] = useState("../assets/images/missing_image.jpg"); const [captureStatus, setCaptureStatus] = useState(0); // 0 : no capture | 1 : waiting for response from server | 2 : capture failed | 3 : capture succesful const captureStatusColor = {0: "#777", 1: "#FFA500", 2: "#FF6B6B", 3: "#81C784"}; const mapRef = useRef(null); const [centerMap, setCenterMap] = useState(true); // Router useEffect(() => { if (!loading) { if (!loggedIn) { router.replace("/"); } } }, [loggedIn, loading]); // Activating geolocation tracking useEffect(() => { if (loggedIn) { startLocationTracking(); } else { stopLocationTracking(); } }, [loggedIn, gameState, captured]); // Refresh the image useEffect(() => { setEnemyImageURI(`${SERVER_URL}/photo/enemy?team=${teamId}&t=${new Date().getTime()}`); }, [enemyName, teamId]); // Capture state update useEffect(() => { if (captureStatus == 2 || captureStatus == 3) { const timeout = setTimeout(() => { setCaptureStatus(0); }, 3000); return () => clearTimeout(timeout); } }, [captureStatus]); // Center the map on user position useEffect(() => { if (centerMap && mapRef.current && location) { mapRef.current.animateToRegion({latitude: location[0], longitude: location[1], latitudeDelta: 0, longitudeDelta: 0.02}, 1000); } }, [centerMap, mapRef, location]); // Update the average speed useEffect(() => { const hours = (finishDate ? (finishDate - startDate) : timeSinceStart*1000) / 1000 / 3600; const km = distance / 1000; setAvgSpeed(Math.floor(km / hours * 10) / 10); }, [distance, finishDate, timeSinceStart]); function toggleCollapsible() { setCollapsibleState(!collapsibleState); }; function handleCapture() { if (captureStatus != 1) { setCaptureStatus(1); capture(enemyCaptureCode) .then((response) => { if (response.hasCaptured) { setCaptureStatus(3); } else { setCaptureStatus(2); } }) .catch(() => { Alert.alert("Échec", "La connexion au serveur a échoué."); setCaptureStatus(2); }); setEnemyCaptureCode(""); } } function formatTimeMinutes(time) { // time is in seconds if (!Number.isInteger(time)) return "Inconnue"; if (time < 0) time = 0; const minutes = Math.floor(time / 60); const seconds = Math.floor(time % 60); return String(minutes).padStart(2,"0") + ":" + String(seconds).padStart(2,"0"); } function formatTimeHours(time) { // time is in seconds if (!Number.isInteger(time)) return "Inconnue"; if (time < 0) time = 0; const hours = Math.floor(time / 3600); const minutes = Math.floor(time / 60); const seconds = Math.floor(time % 60); return String(hours).padStart(2,"0") + ":" + String(minutes).padStart(2,"0") + ":" + String(seconds).padStart(2,"0"); } const Logout = () => { return ( ); } const Settings = () => { return ( Alert.alert("Settings")}> ); } const TeamName = () => { return( {(name ?? "Indisponible")} ); } const GameLog = () => { return ( { gameState == GameState.SETUP && {messages?.waiting || "Préparation de la partie"}} { gameState == GameState.PLACEMENT && Phase de placement} { gameState == GameState.PLAYING && !outOfZone && La partie est en cours} { gameState == GameState.PLAYING && outOfZone && !hasHandicap && {`Veuillez retourner dans la zone\nHandicap dans ${formatTimeMinutes(-timeLeftOutOfZone)}`}} { gameState == GameState.PLAYING && hasHandicap && {`Veuillez retourner dans la zone\nVotre position est révélée en continue`}} { gameState == GameState.FINISHED && La partie est terminée} ); } const TimeBeforeNextZone = () => { return ( {isShrinking ? "Réduction de la zone" : "Durée de la zone"} {formatTimeMinutes(-timeLeftNextZone)} ); } const TimeBeforeNextPosition = () => { return ( Position envoyée dans { !hasHandicap ? formatTimeMinutes(-timeLeftSendLocation) : "00:00"} ); } const Timers = () => { return ( { TimeBeforeNextZone() } { TimeBeforeNextPosition() } ); } const Ready = () => { return ( {ready ? "Placé" : "Non placé"} ); } const CapturedMessage = () => { return ( {messages?.captured || "Vous avez été éliminé..."} ); } const EndGameMessage = () => { return ( {captured && {messages?.loser || "Vous avez perdu..."}} {!captured && {messages?.winner || "Vous avez gagné !"}} ); } const Zones = () => { const latToLatitude = (pos) => ({latitude: pos.lat, longitude: pos.lng}); return ( { zoneType == zoneTypes.circle && zoneExtremities.begin && } { zoneType == zoneTypes.circle && zoneExtremities.end && } { zoneType == zoneTypes.polygon && zoneExtremities.begin && latToLatitude(pos))} strokeColor="red" fillColor="rgba(255,0,0,0.1)" strokeWidth={2} /> } { zoneType == zoneTypes.polygon && zoneExtremities.end && latToLatitude(pos))} strokeColor="green" fillColor="rgba(0,255,0,0.1)" strokeWidth={2} /> } ); } const Map = () => { return ( setCenterMap(false)} toolbarEnabled={false}> { gameState == GameState.PLACEMENT && startingArea && } { gameState == GameState.PLAYING && zoneExtremities && } { location && Alert.alert("Position actuelle", "Ceci est votre position")}> } { gameState == GameState.PLAYING && lastSentLocation && !hasHandicap && Alert.alert("Position envoyée", "Ceci est votre dernière position connue par le serveur")}> } { gameState == GameState.PLAYING && enemyLocation && !hasHandicap && Alert.alert("Position ennemie", "Ceci est la dernière position de vos ennemis connue")}/> } ); } const UpdatePositionButton = () => { return ( !hasHandicap && ); } const CenterMapButton = () => { return ( setCenterMap(true)}> ); } const LayerButton = () => { return( Alert.alert("Layer")}> ); } const CollapsibleButton = () => { return ( ); } const TeamCaptureCode = () => { return ( Code de {(name ?? "Indisponible")} : {String(captureCode).padStart(4,"0")} ); } const ChasedTeamImage = () => { return ( {{"Cible (" + (enemyName ?? "Indisponible") + ")"}} {} ); } const CaptureCode = () => { return ( ); } const CaptureButton = () => { return ( ); } const Stats = () => { return ( {Math.floor(distance / 100) / 10}km {formatTimeHours((finishDate ? Math.floor((finishDate - startDate) / 1000) : timeSinceStart))} {avgSpeed}km/h {nCaptures} {nSentLocation} ); } return ( { Logout() } { false && Settings() } { TeamName() } { GameLog() } { gameState == GameState.PLACEMENT && Ready() } { gameState == GameState.PLAYING && !captured && {Timers()} {enemyHasHandicap && Position ennemie révélée en continue !} } { gameState == GameState.PLAYING && captured && CapturedMessage() } { gameState == GameState.FINISHED && EndGameMessage() } {setBottomContainerHeight(event.nativeEvent.layout.height)}}> { Map() } { !centerMap && CenterMapButton() } { false && gameState == GameState.PLAYING && !captured && { LayerButton() } } { gameState == GameState.PLAYING && !captured && { UpdatePositionButton() } } { (gameState == GameState.PLAYING || gameState == GameState.FINISHED) && { CollapsibleButton() } { gameState == GameState.PLAYING && TeamCaptureCode() } { gameState == GameState.PLAYING && !hasHandicap && { ChasedTeamImage() } { CaptureCode() } { CaptureButton() } } { Stats() } } ); } const styles = StyleSheet.create({ globalContainer: { backgroundColor: backgroundColor, flex: 1, }, topContainer: { width: '100%', alignItems: 'center', padding: 15, }, topheadContainer: { width: "100%", flexDirection: "row", justifyContent: 'space-between' }, teamNameContainer: { width: '100%', alignItems: 'center', justifyContent: 'center' }, logContainer: { width: '100%', alignItems: 'center', justifyContent: 'center', marginTop: 15 }, gameState: { borderWidth: 2, borderRadius: 10, width: "100%", backgroundColor: 'white', fontSize: 18, padding: 10, }, gameStateOutOfZone: { borderWidth: 2, borderRadius: 10, width: "100%", backgroundColor: 'white', fontSize: 18, padding: 10, borderColor: 'red' }, timersContainer: { width: '100%', alignItems: 'center', justifyContent: 'center', flexDirection: 'row', marginTop: 15 }, zoneTimerContainer: { width: "50%", alignItems: 'center', justifyContent: 'center', }, positionTimerContainer: { width: "50%", alignItems: 'center', justifyContent: 'center', }, readyIndicator: { width: "100%", maxWidth: 240, height: 61, alignItems: 'center', justifyContent: 'center', padding: 3, borderRadius: 10 }, bottomContainer: { flex: 1, }, mapContainer: { flex: 1, width: '100%', borderTopLeftRadius: 30, borderTopRightRadius: 30, overflow: 'hidden', }, outerDrawerContainer: { position: 'absolute', bottom: 0, left: 0, right: 0, }, innerDrawerContainer: { width: "100%", backgroundColor: backgroundColor, borderTopLeftRadius: 30, borderTopRightRadius: 30, overflow: 'hidden', }, collapsibleButton: { justifyContent: 'center', alignItems: 'center', width: "100%", height: 45 }, collapsibleWindow: { width: "100%", justifyContent: 'center', backgroundColor: backgroundColor, }, collapsibleContent: { paddingHorizontal: 15, }, centerMapContainer: { position: 'absolute', right: 20, top: 20, width: 40, height: 40, borderRadius: 20, backgroundColor: 'white', borderWidth: 2, borderColor: 'black', alignItems: 'center', justifyContent: 'center', }, toolBarLeft: { position: 'absolute', left: 30, bottom: 80 }, toolBarRight: { position: 'absolute', right: 30, bottom: 80 }, updatePositionContainer: { width: 60, height: 60, borderRadius: 30, backgroundColor: 'white', borderWidth: 4, borderColor: 'black', alignItems: 'center', justifyContent: 'center', }, layerContainer: { width: 60, height: 60, borderRadius: 30, backgroundColor: 'white', borderWidth: 4, borderColor: 'black', alignItems: 'center', justifyContent: 'center', }, imageContainer: { width: "100%", alignItems: "center", justifyContent: "center", marginTop: 15 }, actionsContainer: { flexDirection: "row", width: "100%", alignItems: 'center', justifyContent: 'space-between', marginTop: 15 }, actionsLeftContainer: { flexGrow: 1, alignItems: 'center', justifyContent: 'center', marginRight: 15 }, actionsRightContainer: { width: 100, alignItems: 'center', justifyContent: 'center' }, button: { borderRadius: 12, width: '100%', height: 75, alignItems: 'center', justifyContent: 'center', backgroundColor: '#444' }, });