diff --git a/traque-back/.vscode/launch.json b/traque-back/.vscode/launch.json deleted file mode 100644 index 32a81da..0000000 --- a/traque-back/.vscode/launch.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "command": "sudo npm start", - "name": "Run npm start", - "request": "launch", - "type": "node-terminal" - }, - ] -} \ No newline at end of file diff --git a/traque-back/admin_socket.js b/traque-back/admin_socket.js index 4976cf2..0d6282f 100644 --- a/traque-back/admin_socket.js +++ b/traque-back/admin_socket.js @@ -53,7 +53,7 @@ export function initAdminSocketHandler() { loggedInSockets.push(socket.id); loggedIn = true; // Send the current state - socket.emit("game_state", game.state) + socket.emit("game_state", {state: game.state, startDate: game.startDate}) // Other settings that need initialization socket.emit("penalty_settings", penaltyController.settings) socket.emit("game_settings", game.settings) @@ -61,7 +61,8 @@ export function initAdminSocketHandler() { socket.emit("zone", zone.currentZone) socket.emit("new_zone", { begin: zone.currentStartZone, - end: zone.nextZone + end: zone.nextZone, + endDate: zone.nextZoneDate, }) } else { // Attempt unsuccessful @@ -143,10 +144,7 @@ export function initAdminSocketHandler() { socket.emit("error", "Not logged in"); return; } - if (game.setState(state)) { - secureAdminBroadcast("game_state", game.state); - playersBroadcast("game_state", game.state) - } else { + if (!game.setState(state)) { socket.emit("error", "Error setting state"); } }); diff --git a/traque-back/game.js b/traque-back/game.js index bc6a6af..1bc788f 100644 --- a/traque-back/game.js +++ b/traque-back/game.js @@ -3,11 +3,11 @@ This module manages the main game state, the teams, the settings and the game lo */ import { secureAdminBroadcast } from "./admin_socket.js"; import { playersBroadcast, sendUpdatedTeamInformations } from "./team_socket.js"; -import { isInCircle } from "./map_utils.js"; +import { isInCircle, getDistanceFromLatLon } from "./map_utils.js"; import timeoutHandler from "./timeoutHandler.js"; import penaltyController from "./penalty_controller.js"; import zoneManager from "./zone_manager.js"; -import { writePosition, writeCapture, writeSeePosition } from "./trajectory.js"; +import trajectory from "./trajectory.js"; /** * The possible states of the game @@ -24,6 +24,8 @@ export default { teams: [], //Current state of the game state: GameState.SETUP, + // Date since gameState switched to PLAYING + startDate: null, //Settings of the game settings: { loserEndGameMessage: "", @@ -48,41 +50,63 @@ export default { * @returns true if the state has been changed */ setState(newState) { - if (Object.values(GameState).indexOf(newState) == -1) { - return false; - } - //The game has started - if (newState == GameState.PLAYING) { - penaltyController.start(); - if (!zoneManager.ready()) { - return false; - } - this.initLastSentLocations(); - zoneManager.reset() - //If the zone cannot be setup, reset everything - if (!zoneManager.start()) { - this.setState(GameState.SETUP); - return; - } - } - if (newState != GameState.PLAYING) { - zoneManager.reset(); - penaltyController.stop(); - timeoutHandler.endAllSendPositionTimeout(); - } - //Game reset - if (newState == GameState.SETUP) { - for (let team of this.teams) { - team.penalties = 0; - team.captured = false; - team.enemyLocation = null; - team.enemyName = null; - team.currentLocation = null; - team.lastSentLocation = null; - } - this.updateTeamChasing(); + // Checks is the newState is a Gamestate + if (Object.values(GameState).indexOf(newState) == -1) return false; + // Match case + switch (newState) { + case GameState.SETUP: + trajectory.stop(); + zoneManager.reset(); + penaltyController.stop(); + timeoutHandler.endAllSendPositionTimeout(); + for (let team of this.teams) { + team.outOfZone = false; + team.penalties = 0; + team.captured = false; + team.enemyLocation = null; + team.enemyName = null; + team.currentLocation = null; + team.lastSentLocation = null; + team.distance = null; + team.finishDate = null; + team.nCaptures = 0; + team.nSentLocation = 0; + team.nObserved = 0; + } + this.startDate = null; + this.updateTeamChasing(); + break; + case GameState.PLACEMENT: + trajectory.stop(); + zoneManager.reset(); + penaltyController.stop(); + timeoutHandler.endAllSendPositionTimeout(); + this.startDate = null; + break; + case GameState.PLAYING: + if (!zoneManager.start()) { + return false; + } + trajectory.start(); + penaltyController.start(); + this.initLastSentLocations(); + this.startDate = Date.now(); + break; + case GameState.FINISHED: + for (const team of this.teams) { + if (!team.finishDate) team.finishDate = Date.now(); + } + trajectory.stop(); + penaltyController.stop(); + zoneManager.reset(); + timeoutHandler.endAllSendPositionTimeout(); + break; } + // Update the state this.state = newState; + secureAdminBroadcast("game_state", {state: newState, startDate: this.startDate}); + playersBroadcast("game_state", newState); + secureAdminBroadcast("teams", this.teams); return true; }, @@ -128,6 +152,18 @@ export default { ready: false, captured: false, penalties: 0, + outOfZone: false, + outOfZoneDeadline: null, + distance: 0, + finishDate: null, + nCaptures: 0, + nSentLocation: 0, + nObserved: 0, + phoneModel: null, + phoneName: null, + battery: null, + ping: null, + nConnected: 0, }); this.updateTeamChasing(); return true; @@ -156,7 +192,7 @@ export default { updateTeamChasing() { if (this.playingTeamCount() <= 2) { if (this.state == GameState.PLAYING) { - this.finishGame(); + this.setState(GameState.FINISHED); } return false; } @@ -230,8 +266,9 @@ export default { if (!team || !location) { return false; } + if (team.currentLocation) team.distance += Math.floor(getDistanceFromLatLon({lat: location[0], lng: location[1]}, {lat: team.currentLocation[0], lng: team.currentLocation[1]})); // Update of events of the game - writePosition(Date.now(), teamId, location[0], location[1]); + trajectory.writePosition(Date.now(), teamId, location[0], location[1]); // Update of currentLocation team.currentLocation = location; // Update of ready (true if the team is in the starting area) @@ -247,13 +284,14 @@ export default { * Initialize the last sent location of the teams to their starting location */ initLastSentLocations() { + // Update of lastSentLocation for (const team of this.teams) { team.lastSentLocation = team.currentLocation; team.locationSendDeadline = Date.now() + penaltyController.settings.allowedTimeBetweenPositionUpdate * 60 * 1000; timeoutHandler.setSendPositionTimeout(team.id, team.locationSendDeadline); - this.getTeam(team.chasing).enemyLocation = team.lastSentLocation; sendUpdatedTeamInformations(team.id); } + // Update of enemyLocation now we have the lastSentLocation of the enemy for (const team of this.teams) { team.enemyLocation = this.getTeam(team.chasing).lastSentLocation; sendUpdatedTeamInformations(team.id); @@ -267,12 +305,15 @@ export default { */ sendLocation(teamId) { const team = this.getTeam(teamId); + const enemyTeam = this.getTeam(team.chasing); if (!team || !team.currentLocation) { return false; } + team.nSentLocation++; + enemyTeam.nObserved++; const dateNow = Date.now(); // Update of events of the game - writeSeePosition(dateNow, teamId, team.chasing); + trajectory.writeSeePosition(dateNow, teamId, team.chasing); // Update of locationSendDeadline team.locationSendDeadline = dateNow + penaltyController.settings.allowedTimeBetweenPositionUpdate * 60 * 1000; timeoutHandler.setSendPositionTimeout(team.id, team.locationSendDeadline); @@ -283,6 +324,7 @@ export default { if (teamChasing) team.enemyLocation = teamChasing.lastSentLocation; // Sending new infos to the team sendUpdatedTeamInformations(team.id); + sendUpdatedTeamInformations(enemyTeam.id); return true; }, @@ -315,8 +357,9 @@ export default { if (!enemyTeam || enemyTeam.captureCode != captureCode) { return false; } + team.nCaptures++; // Update of events of the game - writeCapture(Date.now(), teamId, enemyTeam.id); + trajectory.writeCapture(Date.now(), teamId, enemyTeam.id); // Update of capture and chasing cycle this.capture(enemyTeam.id); // Sending new infos to the teams @@ -330,7 +373,9 @@ export default { * @param {Number} teamId the Id of the captured team */ capture(teamId) { - this.getTeam(teamId).captured = true; + const team = this.getTeam(teamId); + team.captured = true; + team.finishDate = Date.now(); timeoutHandler.endSendPositionTimeout(teamId); this.updateTeamChasing(); }, @@ -352,22 +397,10 @@ export default { } zoneManager.udpateSettings(newSettings); if (this.state == GameState.PLAYING || this.state == GameState.FINISHED) { - zoneManager.reset() if (!zoneManager.start()) { - this.setState(GameState.SETUP); return false; } } return true; - }, - - /** - * Set the game state as finished, as well as resetting the zone manager - */ - finishGame() { - this.setState(GameState.FINISHED); - zoneManager.reset(); - timeoutHandler.endAllSendPositionTimeout(); - playersBroadcast("game_state", this.state); - }, + } } \ No newline at end of file diff --git a/traque-back/index.js b/traque-back/index.js index 5f126e0..000a26e 100644 --- a/traque-back/index.js +++ b/traque-back/index.js @@ -5,7 +5,6 @@ import { config } from "dotenv"; import { initAdminSocketHandler } from "./admin_socket.js"; import { initTeamSocket } from "./team_socket.js"; import { initPhotoUpload } from "./photo.js"; -import { initTrajectories } from "./trajectory.js"; config(); const HOST = process.env.HOST; @@ -29,4 +28,3 @@ export const io = new Server(httpServer, { initAdminSocketHandler(); initTeamSocket(); initPhotoUpload(); -initTrajectories(); diff --git a/traque-back/map_utils.js b/traque-back/map_utils.js index 330c83d..ca5385d 100644 --- a/traque-back/map_utils.js +++ b/traque-back/map_utils.js @@ -14,7 +14,7 @@ function degToRad(deg) { * @returns the distance between the two positions in meters * @see https://gist.github.com/miguelmota/10076960 */ -function getDistanceFromLatLon({ lat: lat1, lng: lon1 }, { lat: lat2, lng: lon2 }) { +export function getDistanceFromLatLon({ lat: lat1, lng: lon1 }, { lat: lat2, lng: lon2 }) { var R = 6371; // Radius of the earth in km var dLat = degToRad(lat2 - lat1); var dLon = degToRad(lon2 - lon1); diff --git a/traque-back/penalty_controller.js b/traque-back/penalty_controller.js index 8208f9d..35d7233 100644 --- a/traque-back/penalty_controller.js +++ b/traque-back/penalty_controller.js @@ -121,17 +121,21 @@ export default { if (!isInCircle({ lat: team.currentLocation[0], lng: team.currentLocation[1] }, zone.currentZone.center, zone.currentZone.radius)) { //The team was not previously out of the zone if (!this.outOfBoundsSince[team.id]) { - this.outOfBoundsSince[team.id] = new Date(); - teamBroadcast(team.id, "warning", `You left the zone, you have ${this.settings.allowedTimeOutOfZone} minutes to get back in the marked area.`) - } else if (new Date() - this.outOfBoundsSince[team.id] > this.settings.allowedTimeOutOfZone * 60 * 1000) { - this.addPenalty(team.id) - this.outOfBoundsSince[team.id] = new Date(); - } else if (Math.abs(new Date() - this.outOfBoundsSince[team.id] - (this.settings.allowedTimeOutOfZone - 1) * 60 * 1000) < 100) { - teamBroadcast(team.id, "warning", `You left the zone, you have 1 minutes to get back in the marked area.`) + this.outOfBoundsSince[team.id] = Date.now(); + team.outOfZone = true; + team.outOfZoneDeadline = this.outOfBoundsSince[team.id] + this.settings.allowedTimeOutOfZone * 60 * 1000; + secureAdminBroadcast("teams", game.teams) + } else if (Date.now() - this.outOfBoundsSince[team.id] > this.settings.allowedTimeOutOfZone * 60 * 1000) { + this.addPenalty(team.id); + this.outOfBoundsSince[team.id] = Date.now(); + team.outOfZoneDeadline = this.outOfBoundsSince[team.id] + this.settings.allowedTimeOutOfZone * 60 * 1000; + secureAdminBroadcast("teams", game.teams) } } else { if (this.outOfBoundsSince[team.id]) { + team.outOfZone = false; delete this.outOfBoundsSince[team.id]; + secureAdminBroadcast("teams", game.teams) } } }) @@ -146,15 +150,15 @@ export default { //If the team has not sent their location for more than the allowed period, automatically send it and add a penalty if (team.captured) { return } if (team.locationSendDeadline == null) { - team.locationSendDeadline = Number(new Date()) + this.settings.allowedTimeBetweenPositionUpdate * 60 * 1000; + team.locationSendDeadline = Number(Date.now()) + this.settings.allowedTimeBetweenPositionUpdate * 60 * 1000; return; } - if (new Date() > team.locationSendDeadline) { + if (Date.now() > team.locationSendDeadline) { this.addPenalty(team.id); game.sendLocation(team.id); sendUpdatedTeamInformations(team.id); secureAdminBroadcast("teams", game.teams) - } else if (Math.abs(new Date() - team.locationSendDeadline - 60 * 1000) < 100) { + } else if (Math.abs(Date.now() - team.locationSendDeadline - 60 * 1000) < 100) { teamBroadcast(team.id, "warning", `You have one minute left to udpate your location.`) } }) diff --git a/traque-back/team_socket.js b/traque-back/team_socket.js index 967969c..f74afb2 100644 --- a/traque-back/team_socket.js +++ b/traque-back/team_socket.js @@ -45,7 +45,6 @@ export function sendUpdatedTeamInformations(teamId) { name: team.name, enemyLocation: team.enemyLocation, enemyName: team.enemyName, - currentLocation: team.currentLocation, lastSentLocation: team.lastSentLocation, locationSendDeadline: team.locationSendDeadline, captureCode: team.captureCode, @@ -53,6 +52,13 @@ export function sendUpdatedTeamInformations(teamId) { ready: team.ready, captured: team.captured, penalties: team.penalties, + outOfZone: team.outOfZone, + outOfZoneDeadline: team.outOfZoneDeadline, + distance: team.distance, + startDate: game.startDate, + finishDate: team.finishDate, + nCaptures: team.nCaptures, + nSentLocation: team.nSentLocation, }) }) secureAdminBroadcast("teams", game.teams); @@ -94,7 +100,8 @@ export function initTeamSocket() { socket.emit("zone", zone.currentZone); socket.emit("new_zone", { begin: zone.currentStartZone, - end: zone.nextZone + end: zone.nextZone, + endDate: zone.nextZoneDate, }) callback({ isLoggedIn : true, message: "Logged in"}); }); @@ -133,6 +140,29 @@ export function initTeamSocket() { return; } callback({ hasCaptured : true, message: "Capture successful" }); - }) + }); + + socket.on("deviceInfo", (infos) => { + if (!teamId) { + return; + } + const team = game.getTeam(teamId); + // Only the first socket shares its infos since he is the one whose location is tracked + if (team.sockets.indexOf(socket.id) == 0) { + team.phoneModel = infos.model; + team.phoneName = infos.name; + } + }); + + socket.on("batteryUpdate", (batteryLevel) => { + if (!teamId) { + return; + } + const team = game.getTeam(teamId); + // Only the first socket shares its infos since he is the one whose location is tracked + if (team.sockets.indexOf(socket.id) == 0) { + team.battery = batteryLevel; + } + }); }); } \ No newline at end of file diff --git a/traque-back/trajectory.js b/traque-back/trajectory.js index 6aa0ab9..1b4b0a7 100644 --- a/traque-back/trajectory.js +++ b/traque-back/trajectory.js @@ -43,23 +43,48 @@ function addLineToFile(teamID, line) { } } -// Export functions - -export async function initTrajectories() { +function initTrajectories() { const files = fs.readdirSync(UPLOAD_DIR); for (const file of files) fs.unlinkSync(path.join(UPLOAD_DIR, file)); } -export function writePosition(date, teamID, lon, lat) { - addLineToFile(teamID, dataToLine(date, "position", lon, lat)); -} +// Export functions -export function writeCapture(date, teamID, capturedTeamID) { - addLineToFile(teamID, dataToLine(date, "capture", capturedTeamID)); - addLineToFile(capturedTeamID, dataToLine(date, "captured", teamID)); -} +export default { + isRecording: false, -export function writeSeePosition(date, teamID, seenTeamID) { - addLineToFile(teamID, dataToLine(date, "see")); - addLineToFile(seenTeamID, dataToLine(date, "seen")); + start() { + initTrajectories(); + this.isRecording = true; + }, + + stop() { + this.isRecording = false; + }, + + writePosition(date, teamID, lon, lat) { + if (this.isRecording) { + addLineToFile(teamID, dataToLine(date, "position", lon, lat)); + } + }, + + writeCapture(date, teamID, capturedTeamID) { + if (this.isRecording) { + addLineToFile(teamID, dataToLine(date, "capture", capturedTeamID)); + addLineToFile(capturedTeamID, dataToLine(date, "captured", teamID)); + } + }, + + writeSeePosition(date, teamID, seenTeamID) { + if (this.isRecording) { + addLineToFile(teamID, dataToLine(date, "see")); + addLineToFile(seenTeamID, dataToLine(date, "seen")); + } + }, + + writeOutOfZone(date, teamID, isOutOfZone) { + if (this.isRecording) { + addLineToFile(teamID, dataToLine(date, "zone", isOutOfZone)); + } + }, } diff --git a/traque-back/zone_manager.js b/traque-back/zone_manager.js index 73123fe..e17fe58 100644 --- a/traque-back/zone_manager.js +++ b/traque-back/zone_manager.js @@ -116,6 +116,8 @@ export default { * Start the zone reduction sequence */ start() { + if (!this.ready()) return false; + this.reset(); this.started = true; this.startDate = new Date(); //initialize the zone to its max value diff --git a/traque-front/.vscode/launch.json b/traque-front/.vscode/launch.json deleted file mode 100644 index e18d3ef..0000000 --- a/traque-front/.vscode/launch.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "name": "Next.js: debug server-side", - "type": "node-terminal", - "request": "launch", - "command": "npm run dev" - }, - { - "name": "Next.js: debug client-side", - "type": "chrome", - "request": "launch", - "url": "http://localhost:3000" - }, - { - "name": "Next.js: debug full stack", - "type": "node-terminal", - "request": "launch", - "command": "npm run dev", - "serverReadyAction": { - "pattern": "- Local:.+(https?://.+)", - "uriFormat": "%s", - "action": "debugWithChrome" - } - } - ] -} \ No newline at end of file diff --git a/traque-front/components/admin/teamEdit.jsx b/traque-front/components/admin/teamEdit.jsx index 43ed926..3d78914 100644 --- a/traque-front/components/admin/teamEdit.jsx +++ b/traque-front/components/admin/teamEdit.jsx @@ -5,6 +5,7 @@ import useAdmin from '@/hook/useAdmin'; import dynamic from 'next/dynamic'; import { env } from 'next-runtime-env'; +import { GameState } from '@/util/gameState'; const CircularAreaPicker = dynamic(() => import('./mapPicker').then((mod) => mod.CircularAreaPicker), { ssr: false @@ -12,8 +13,9 @@ const CircularAreaPicker = dynamic(() => import('./mapPicker').then((mod) => mod export default function TeamEdit({ selectedTeamId, setSelectedTeamId }) { const teamImage = useRef(null); + const [avgSpeed, setAvgSpeed] = useState(0); // Speed in m/s const [newTeamName, setNewTeamName] = React.useState(''); - const { updateTeam, getTeamName, removeTeam, getTeam, teams } = useAdmin(); + const { updateTeam, getTeamName, removeTeam, getTeam, teams, gameState, startDate } = useAdmin(); const [team, setTeam] = useState({}); const NEXT_PUBLIC_SOCKET_HOST = env("NEXT_PUBLIC_SOCKET_HOST"); var protocol = "https://"; @@ -32,6 +34,12 @@ export default function TeamEdit({ selectedTeamId, setSelectedTeamId }) { teamImage.current.src = SERVER_URL + "/photo/my?team=" + selectedTeamId + "&t=" + new Date().getTime(); }, [selectedTeamId, teams]) + // Update the average speed + useEffect(() => { + const time = Math.floor((team.finishDate ? team.finishDate - startDate : Date.now() - startDate) / 1000); + setAvgSpeed(team.distance/time); + }, [team.distance, team.finishDate]); + function handleRename(e) { e.preventDefault(); updateTeam(team.id, { name: newTeamName }); @@ -49,51 +57,70 @@ export default function TeamEdit({ selectedTeamId, setSelectedTeamId }) { updateTeam(team.id, { penalties: newPenalties }); } + 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"); + } + return (team && -
Starting zone
+Secret : {String(team.id).padStart(6, '0').replace(/(\d{3})(\d{3})/, '$1 $2')}
Capture code : {String(team.captureCode).padStart(4, '0')}
+Chasing : {getTeamName(team.chasing)}
Chased by : {getTeamName(team.chased)}
-Penalties :
- -{team.penalties}
- -Starting zone
-Profile picture
-Distance: { (team.distance / 1000).toFixed(1) }km
+Time : {formatTimeHours(Math.floor((team.finishDate ? team.finishDate - startDate : Date.now() - startDate) / 1000))}
+Average speed : {(avgSpeed*3.6).toFixed(1)}km/h
+Captures : {team.nCaptures}
+Sent location : {team.nSentLocation}
+Oberved : {team.nObserved}
+Phone model : {team.phoneModel ?? "Unknown"}
+Phone name : {team.phoneName ?? "Unknown"}
+Battery: {team.battery ? team.battery + "%" : "Unknown"}
+Penalties :
+ +{team.penalties}
+ +