Ajout de stats + corrections

This commit is contained in:
Sébastien Rivière
2025-06-22 01:34:59 +02:00
parent ab81a5351c
commit 7d541159cd
12 changed files with 246 additions and 170 deletions

View File

@@ -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"
},
]
}

View File

@@ -53,7 +53,7 @@ export function initAdminSocketHandler() {
loggedInSockets.push(socket.id); loggedInSockets.push(socket.id);
loggedIn = true; loggedIn = true;
// Send the current state // 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 // Other settings that need initialization
socket.emit("penalty_settings", penaltyController.settings) socket.emit("penalty_settings", penaltyController.settings)
socket.emit("game_settings", game.settings) socket.emit("game_settings", game.settings)
@@ -61,7 +61,8 @@ export function initAdminSocketHandler() {
socket.emit("zone", zone.currentZone) socket.emit("zone", zone.currentZone)
socket.emit("new_zone", { socket.emit("new_zone", {
begin: zone.currentStartZone, begin: zone.currentStartZone,
end: zone.nextZone end: zone.nextZone,
endDate: zone.nextZoneDate,
}) })
} else { } else {
// Attempt unsuccessful // Attempt unsuccessful
@@ -143,10 +144,7 @@ export function initAdminSocketHandler() {
socket.emit("error", "Not logged in"); socket.emit("error", "Not logged in");
return; return;
} }
if (game.setState(state)) { if (!game.setState(state)) {
secureAdminBroadcast("game_state", game.state);
playersBroadcast("game_state", game.state)
} else {
socket.emit("error", "Error setting state"); socket.emit("error", "Error setting state");
} }
}); });

View File

@@ -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 { secureAdminBroadcast } from "./admin_socket.js";
import { playersBroadcast, sendUpdatedTeamInformations } from "./team_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 timeoutHandler from "./timeoutHandler.js";
import penaltyController from "./penalty_controller.js"; import penaltyController from "./penalty_controller.js";
import zoneManager from "./zone_manager.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 * The possible states of the game
@@ -24,6 +24,8 @@ export default {
teams: [], teams: [],
//Current state of the game //Current state of the game
state: GameState.SETUP, state: GameState.SETUP,
// Date since gameState switched to PLAYING
startDate: null,
//Settings of the game //Settings of the game
settings: { settings: {
loserEndGameMessage: "", loserEndGameMessage: "",
@@ -48,41 +50,63 @@ export default {
* @returns true if the state has been changed * @returns true if the state has been changed
*/ */
setState(newState) { setState(newState) {
if (Object.values(GameState).indexOf(newState) == -1) { // Checks is the newState is a Gamestate
return false; if (Object.values(GameState).indexOf(newState) == -1) return false;
} // Match case
//The game has started switch (newState) {
if (newState == GameState.PLAYING) { case GameState.SETUP:
penaltyController.start(); trajectory.stop();
if (!zoneManager.ready()) { zoneManager.reset();
return false; penaltyController.stop();
} timeoutHandler.endAllSendPositionTimeout();
this.initLastSentLocations(); for (let team of this.teams) {
zoneManager.reset() team.outOfZone = false;
//If the zone cannot be setup, reset everything team.penalties = 0;
if (!zoneManager.start()) { team.captured = false;
this.setState(GameState.SETUP); team.enemyLocation = null;
return; team.enemyName = null;
} team.currentLocation = null;
} team.lastSentLocation = null;
if (newState != GameState.PLAYING) { team.distance = null;
zoneManager.reset(); team.finishDate = null;
penaltyController.stop(); team.nCaptures = 0;
timeoutHandler.endAllSendPositionTimeout(); team.nSentLocation = 0;
} team.nObserved = 0;
//Game reset }
if (newState == GameState.SETUP) { this.startDate = null;
for (let team of this.teams) { this.updateTeamChasing();
team.penalties = 0; break;
team.captured = false; case GameState.PLACEMENT:
team.enemyLocation = null; trajectory.stop();
team.enemyName = null; zoneManager.reset();
team.currentLocation = null; penaltyController.stop();
team.lastSentLocation = null; timeoutHandler.endAllSendPositionTimeout();
} this.startDate = null;
this.updateTeamChasing(); 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; this.state = newState;
secureAdminBroadcast("game_state", {state: newState, startDate: this.startDate});
playersBroadcast("game_state", newState);
secureAdminBroadcast("teams", this.teams);
return true; return true;
}, },
@@ -128,6 +152,18 @@ export default {
ready: false, ready: false,
captured: false, captured: false,
penalties: 0, 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(); this.updateTeamChasing();
return true; return true;
@@ -156,7 +192,7 @@ export default {
updateTeamChasing() { updateTeamChasing() {
if (this.playingTeamCount() <= 2) { if (this.playingTeamCount() <= 2) {
if (this.state == GameState.PLAYING) { if (this.state == GameState.PLAYING) {
this.finishGame(); this.setState(GameState.FINISHED);
} }
return false; return false;
} }
@@ -230,8 +266,9 @@ export default {
if (!team || !location) { if (!team || !location) {
return false; 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 // 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 // Update of currentLocation
team.currentLocation = location; team.currentLocation = location;
// Update of ready (true if the team is in the starting area) // 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 * Initialize the last sent location of the teams to their starting location
*/ */
initLastSentLocations() { initLastSentLocations() {
// Update of lastSentLocation
for (const team of this.teams) { for (const team of this.teams) {
team.lastSentLocation = team.currentLocation; team.lastSentLocation = team.currentLocation;
team.locationSendDeadline = Date.now() + penaltyController.settings.allowedTimeBetweenPositionUpdate * 60 * 1000; team.locationSendDeadline = Date.now() + penaltyController.settings.allowedTimeBetweenPositionUpdate * 60 * 1000;
timeoutHandler.setSendPositionTimeout(team.id, team.locationSendDeadline); timeoutHandler.setSendPositionTimeout(team.id, team.locationSendDeadline);
this.getTeam(team.chasing).enemyLocation = team.lastSentLocation;
sendUpdatedTeamInformations(team.id); sendUpdatedTeamInformations(team.id);
} }
// Update of enemyLocation now we have the lastSentLocation of the enemy
for (const team of this.teams) { for (const team of this.teams) {
team.enemyLocation = this.getTeam(team.chasing).lastSentLocation; team.enemyLocation = this.getTeam(team.chasing).lastSentLocation;
sendUpdatedTeamInformations(team.id); sendUpdatedTeamInformations(team.id);
@@ -267,12 +305,15 @@ export default {
*/ */
sendLocation(teamId) { sendLocation(teamId) {
const team = this.getTeam(teamId); const team = this.getTeam(teamId);
const enemyTeam = this.getTeam(team.chasing);
if (!team || !team.currentLocation) { if (!team || !team.currentLocation) {
return false; return false;
} }
team.nSentLocation++;
enemyTeam.nObserved++;
const dateNow = Date.now(); const dateNow = Date.now();
// Update of events of the game // Update of events of the game
writeSeePosition(dateNow, teamId, team.chasing); trajectory.writeSeePosition(dateNow, teamId, team.chasing);
// Update of locationSendDeadline // Update of locationSendDeadline
team.locationSendDeadline = dateNow + penaltyController.settings.allowedTimeBetweenPositionUpdate * 60 * 1000; team.locationSendDeadline = dateNow + penaltyController.settings.allowedTimeBetweenPositionUpdate * 60 * 1000;
timeoutHandler.setSendPositionTimeout(team.id, team.locationSendDeadline); timeoutHandler.setSendPositionTimeout(team.id, team.locationSendDeadline);
@@ -283,6 +324,7 @@ export default {
if (teamChasing) team.enemyLocation = teamChasing.lastSentLocation; if (teamChasing) team.enemyLocation = teamChasing.lastSentLocation;
// Sending new infos to the team // Sending new infos to the team
sendUpdatedTeamInformations(team.id); sendUpdatedTeamInformations(team.id);
sendUpdatedTeamInformations(enemyTeam.id);
return true; return true;
}, },
@@ -315,8 +357,9 @@ export default {
if (!enemyTeam || enemyTeam.captureCode != captureCode) { if (!enemyTeam || enemyTeam.captureCode != captureCode) {
return false; return false;
} }
team.nCaptures++;
// Update of events of the game // 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 // Update of capture and chasing cycle
this.capture(enemyTeam.id); this.capture(enemyTeam.id);
// Sending new infos to the teams // Sending new infos to the teams
@@ -330,7 +373,9 @@ export default {
* @param {Number} teamId the Id of the captured team * @param {Number} teamId the Id of the captured team
*/ */
capture(teamId) { capture(teamId) {
this.getTeam(teamId).captured = true; const team = this.getTeam(teamId);
team.captured = true;
team.finishDate = Date.now();
timeoutHandler.endSendPositionTimeout(teamId); timeoutHandler.endSendPositionTimeout(teamId);
this.updateTeamChasing(); this.updateTeamChasing();
}, },
@@ -352,22 +397,10 @@ export default {
} }
zoneManager.udpateSettings(newSettings); zoneManager.udpateSettings(newSettings);
if (this.state == GameState.PLAYING || this.state == GameState.FINISHED) { if (this.state == GameState.PLAYING || this.state == GameState.FINISHED) {
zoneManager.reset()
if (!zoneManager.start()) { if (!zoneManager.start()) {
this.setState(GameState.SETUP);
return false; return false;
} }
} }
return true; 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);
},
} }

View File

@@ -5,7 +5,6 @@ import { config } from "dotenv";
import { initAdminSocketHandler } from "./admin_socket.js"; import { initAdminSocketHandler } from "./admin_socket.js";
import { initTeamSocket } from "./team_socket.js"; import { initTeamSocket } from "./team_socket.js";
import { initPhotoUpload } from "./photo.js"; import { initPhotoUpload } from "./photo.js";
import { initTrajectories } from "./trajectory.js";
config(); config();
const HOST = process.env.HOST; const HOST = process.env.HOST;
@@ -29,4 +28,3 @@ export const io = new Server(httpServer, {
initAdminSocketHandler(); initAdminSocketHandler();
initTeamSocket(); initTeamSocket();
initPhotoUpload(); initPhotoUpload();
initTrajectories();

View File

@@ -14,7 +14,7 @@ function degToRad(deg) {
* @returns the distance between the two positions in meters * @returns the distance between the two positions in meters
* @see https://gist.github.com/miguelmota/10076960 * @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 R = 6371; // Radius of the earth in km
var dLat = degToRad(lat2 - lat1); var dLat = degToRad(lat2 - lat1);
var dLon = degToRad(lon2 - lon1); var dLon = degToRad(lon2 - lon1);

View File

@@ -121,17 +121,21 @@ export default {
if (!isInCircle({ lat: team.currentLocation[0], lng: team.currentLocation[1] }, zone.currentZone.center, zone.currentZone.radius)) { 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 //The team was not previously out of the zone
if (!this.outOfBoundsSince[team.id]) { if (!this.outOfBoundsSince[team.id]) {
this.outOfBoundsSince[team.id] = new Date(); this.outOfBoundsSince[team.id] = Date.now();
teamBroadcast(team.id, "warning", `You left the zone, you have ${this.settings.allowedTimeOutOfZone} minutes to get back in the marked area.`) team.outOfZone = true;
} else if (new Date() - this.outOfBoundsSince[team.id] > this.settings.allowedTimeOutOfZone * 60 * 1000) { team.outOfZoneDeadline = this.outOfBoundsSince[team.id] + this.settings.allowedTimeOutOfZone * 60 * 1000;
this.addPenalty(team.id) secureAdminBroadcast("teams", game.teams)
this.outOfBoundsSince[team.id] = new Date(); } else if (Date.now() - this.outOfBoundsSince[team.id] > this.settings.allowedTimeOutOfZone * 60 * 1000) {
} else if (Math.abs(new Date() - this.outOfBoundsSince[team.id] - (this.settings.allowedTimeOutOfZone - 1) * 60 * 1000) < 100) { this.addPenalty(team.id);
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.outOfZoneDeadline = this.outOfBoundsSince[team.id] + this.settings.allowedTimeOutOfZone * 60 * 1000;
secureAdminBroadcast("teams", game.teams)
} }
} else { } else {
if (this.outOfBoundsSince[team.id]) { if (this.outOfBoundsSince[team.id]) {
team.outOfZone = false;
delete this.outOfBoundsSince[team.id]; 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 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.captured) { return }
if (team.locationSendDeadline == null) { 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; return;
} }
if (new Date() > team.locationSendDeadline) { if (Date.now() > team.locationSendDeadline) {
this.addPenalty(team.id); this.addPenalty(team.id);
game.sendLocation(team.id); game.sendLocation(team.id);
sendUpdatedTeamInformations(team.id); sendUpdatedTeamInformations(team.id);
secureAdminBroadcast("teams", game.teams) 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.`) teamBroadcast(team.id, "warning", `You have one minute left to udpate your location.`)
} }
}) })

View File

@@ -45,7 +45,6 @@ export function sendUpdatedTeamInformations(teamId) {
name: team.name, name: team.name,
enemyLocation: team.enemyLocation, enemyLocation: team.enemyLocation,
enemyName: team.enemyName, enemyName: team.enemyName,
currentLocation: team.currentLocation,
lastSentLocation: team.lastSentLocation, lastSentLocation: team.lastSentLocation,
locationSendDeadline: team.locationSendDeadline, locationSendDeadline: team.locationSendDeadline,
captureCode: team.captureCode, captureCode: team.captureCode,
@@ -53,6 +52,13 @@ export function sendUpdatedTeamInformations(teamId) {
ready: team.ready, ready: team.ready,
captured: team.captured, captured: team.captured,
penalties: team.penalties, 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); secureAdminBroadcast("teams", game.teams);
@@ -94,7 +100,8 @@ export function initTeamSocket() {
socket.emit("zone", zone.currentZone); socket.emit("zone", zone.currentZone);
socket.emit("new_zone", { socket.emit("new_zone", {
begin: zone.currentStartZone, begin: zone.currentStartZone,
end: zone.nextZone end: zone.nextZone,
endDate: zone.nextZoneDate,
}) })
callback({ isLoggedIn : true, message: "Logged in"}); callback({ isLoggedIn : true, message: "Logged in"});
}); });
@@ -133,6 +140,29 @@ export function initTeamSocket() {
return; return;
} }
callback({ hasCaptured : true, message: "Capture successful" }); 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;
}
});
}); });
} }

View File

@@ -43,23 +43,48 @@ function addLineToFile(teamID, line) {
} }
} }
// Export functions function initTrajectories() {
export async function initTrajectories() {
const files = fs.readdirSync(UPLOAD_DIR); const files = fs.readdirSync(UPLOAD_DIR);
for (const file of files) fs.unlinkSync(path.join(UPLOAD_DIR, file)); for (const file of files) fs.unlinkSync(path.join(UPLOAD_DIR, file));
} }
export function writePosition(date, teamID, lon, lat) { // Export functions
addLineToFile(teamID, dataToLine(date, "position", lon, lat));
}
export function writeCapture(date, teamID, capturedTeamID) { export default {
addLineToFile(teamID, dataToLine(date, "capture", capturedTeamID)); isRecording: false,
addLineToFile(capturedTeamID, dataToLine(date, "captured", teamID));
}
export function writeSeePosition(date, teamID, seenTeamID) { start() {
addLineToFile(teamID, dataToLine(date, "see")); initTrajectories();
addLineToFile(seenTeamID, dataToLine(date, "seen")); 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));
}
},
} }

View File

@@ -116,6 +116,8 @@ export default {
* Start the zone reduction sequence * Start the zone reduction sequence
*/ */
start() { start() {
if (!this.ready()) return false;
this.reset();
this.started = true; this.started = true;
this.startDate = new Date(); this.startDate = new Date();
//initialize the zone to its max value //initialize the zone to its max value

View File

@@ -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"
}
}
]
}

View File

@@ -5,6 +5,7 @@ import useAdmin from '@/hook/useAdmin';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { env } from 'next-runtime-env'; import { env } from 'next-runtime-env';
import { GameState } from '@/util/gameState';
const CircularAreaPicker = dynamic(() => import('./mapPicker').then((mod) => mod.CircularAreaPicker), { const CircularAreaPicker = dynamic(() => import('./mapPicker').then((mod) => mod.CircularAreaPicker), {
ssr: false ssr: false
@@ -12,8 +13,9 @@ const CircularAreaPicker = dynamic(() => import('./mapPicker').then((mod) => mod
export default function TeamEdit({ selectedTeamId, setSelectedTeamId }) { export default function TeamEdit({ selectedTeamId, setSelectedTeamId }) {
const teamImage = useRef(null); const teamImage = useRef(null);
const [avgSpeed, setAvgSpeed] = useState(0); // Speed in m/s
const [newTeamName, setNewTeamName] = React.useState(''); 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 [team, setTeam] = useState({});
const NEXT_PUBLIC_SOCKET_HOST = env("NEXT_PUBLIC_SOCKET_HOST"); const NEXT_PUBLIC_SOCKET_HOST = env("NEXT_PUBLIC_SOCKET_HOST");
var protocol = "https://"; 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(); teamImage.current.src = SERVER_URL + "/photo/my?team=" + selectedTeamId + "&t=" + new Date().getTime();
}, [selectedTeamId, teams]) }, [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) { function handleRename(e) {
e.preventDefault(); e.preventDefault();
updateTeam(team.id, { name: newTeamName }); updateTeam(team.id, { name: newTeamName });
@@ -49,51 +57,70 @@ export default function TeamEdit({ selectedTeamId, setSelectedTeamId }) {
updateTeam(team.id, { penalties: newPenalties }); 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 && return (team &&
<div className='flex flex-col w-full h-full'> <div className='flex flex-row w-full h-full gap-2'>
<div className='flex flex-row gap-2'> <div className='flex w-1/2 flex-col h-1/2 gap-2'>
<div className='flex w-1/2 flex-col gap-2 h-min self-start'> <h2 className='text-2xl text-center'>Actions</h2>
<h2 className='text-2xl text-center'>Actions</h2> <form className='flex flex-row' onSubmit={handleRename}>
<form className='flex flex-row' onSubmit={handleRename}> <div className='w-4/5'>
<div className='w-4/5'> <TextInput name="teamName" label='Team name' value={newTeamName} onChange={(e) => setNewTeamName(e.target.value)} />
<TextInput name="teamName" label='Team name' value={newTeamName} onChange={(e) => setNewTeamName(e.target.value)} />
</div>
<div className='w-2/5'>
<BlueButton type="submit">Rename</BlueButton>
</div>
</form>
<div className='flex flex-row'>
<BlueButton onClick={() => updateTeam(team.id, { captured: !team.captured })}>{team.captured ? "Revive" : "Capture"}</BlueButton>
<RedButton onClick={handleRemove}>Remove</RedButton>
</div> </div>
<div className='w-2/5'>
<BlueButton type="submit">Rename</BlueButton>
</div>
</form>
<div className='flex flex-row'>
<BlueButton onClick={() => {updateTeam(team.id, { captured: !team.captured }); team.finishDate = Date.now()}}>{team.captured ? "Revive" : "Capture"}</BlueButton>
<RedButton onClick={handleRemove}>Remove</RedButton>
</div> </div>
<div className='flex w-1/2 flex-col space-y-2 h-min self-start'> <p className='text-2xl text-center w-full'>Starting zone</p>
<h2 className='text-2xl text-center'>Team details</h2> <CircularAreaPicker area={team.startingArea} setArea={(startingArea) => updateTeam(team.id, { startingArea })} markerPosition={team?.currentLocation} />
</div>
<div className='flex w-1/2 flex-col h-min gap-2 items-center'>
<h2 className='text-2xl text-center'>Team details</h2>
<div className='w-3/5'>
<img className='self-stretch' ref={teamImage} onError={(e) => {e.target.src = "/images/missing_image.jpg"}} />
</div>
<div className='flex flex-col gap-3'>
<div> <div>
<p>Secret : {String(team.id).padStart(6, '0').replace(/(\d{3})(\d{3})/, '$1 $2')}</p> <p>Secret : {String(team.id).padStart(6, '0').replace(/(\d{3})(\d{3})/, '$1 $2')}</p>
<p>Capture code : {String(team.captureCode).padStart(4, '0')}</p> <p>Capture code : {String(team.captureCode).padStart(4, '0')}</p>
</div>
<div>
<p>Chasing : {getTeamName(team.chasing)}</p> <p>Chasing : {getTeamName(team.chasing)}</p>
<p>Chased by : {getTeamName(team.chased)}</p> <p>Chased by : {getTeamName(team.chased)}</p>
<div className='flex flex-row'>
<p>Penalties :</p>
<button className='w-7 h-7 mx-4 bg-blue-600 hover:bg-blue-500 text-md ease-out duration-200 text-white shadow-sm rounded' onClick={() => handleAddPenalty(-1)}>-</button>
<p>{team.penalties}</p>
<button className='w-7 h-7 mx-4 bg-blue-600 hover:bg-blue-500 text-md ease-out duration-200 text-white shadow-sm rounded' onClick={() => handleAddPenalty(1)}>+</button>
</div>
<br/>
</div> </div>
</div> {gameState == GameState.PLAYING &&
</div> <div>
<div className='flex flex-row'> <p>Distance: { (team.distance / 1000).toFixed(1) }km</p>
<p className='text-2xl text-center w-full'>Starting zone</p> <p>Time : {formatTimeHours(Math.floor((team.finishDate ? team.finishDate - startDate : Date.now() - startDate) / 1000))}</p>
<p className='text-2xl text-center w-full'>Profile picture</p> <p>Average speed : {(avgSpeed*3.6).toFixed(1)}km/h</p>
</div> <p>Captures : {team.nCaptures}</p>
<div className='flex grow flex-row'> <p>Sent location : {team.nSentLocation}</p>
<div className='w-1/2'> <p>Oberved : {team.nObserved}</p>
<CircularAreaPicker area={team.startingArea} setArea={(startingArea) => updateTeam(team.id, { startingArea })} markerPosition={team?.currentLocation} /> </div>
</div> }
<div className='w-1/2'> <div>
<img className='self-stretch' ref={teamImage} onError={(e) => {e.target.src = "/images/missing_image.jpg"}} /> <p>Phone model : {team.phoneModel ?? "Unknown"}</p>
<p>Phone name : {team.phoneName ?? "Unknown"}</p>
<p>Battery: {team.battery ? team.battery + "%" : "Unknown"}</p>
</div>
<div className='flex flex-row'>
<p>Penalties :</p>
<button className='w-7 h-7 mx-4 bg-blue-600 hover:bg-blue-500 text-md ease-out duration-200 text-white shadow-sm rounded' onClick={() => handleAddPenalty(-1)}>-</button>
<p>{team.penalties}</p>
<button className='w-7 h-7 mx-4 bg-blue-600 hover:bg-blue-500 text-md ease-out duration-200 text-white shadow-sm rounded' onClick={() => handleAddPenalty(1)}>+</button>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -19,8 +19,9 @@ function AdminProvider({ children }) {
const { adminSocket } = useSocket(); const { adminSocket } = useSocket();
const { loggedIn } = useAdminConnexion(); const { loggedIn } = useAdminConnexion();
const [gameState, setGameState] = useState(GameState.SETUP); const [gameState, setGameState] = useState(GameState.SETUP);
const [startDate, setStartDate] = useState(null);
useSocketListener(adminSocket, "game_state", setGameState); useSocketListener(adminSocket, "game_state", (data) => {setGameState(data.state); setStartDate(data.startDate)});
//Send a request to get the teams when the user logs in //Send a request to get the teams when the user logs in
useEffect(() => { useEffect(() => {
adminSocket.emit("get_teams"); adminSocket.emit("get_teams");
@@ -46,7 +47,7 @@ function AdminProvider({ children }) {
useSocketListener(adminSocket, "zone_start", shrinking); useSocketListener(adminSocket, "zone_start", shrinking);
useSocketListener(adminSocket, "new_zone", waiting); useSocketListener(adminSocket, "new_zone", waiting);
const value = useMemo(() => ({ zone, zoneExtremities, teams, zoneSettings, penaltySettings, gameSettings, gameState, nextZoneDate, isShrinking }), [zoneSettings, teams, gameState, zone, zoneExtremities, penaltySettings, gameSettings, nextZoneDate, isShrinking]); const value = useMemo(() => ({ zone, zoneExtremities, teams, zoneSettings, penaltySettings, gameSettings, gameState, nextZoneDate, isShrinking, startDate }), [zoneSettings, teams, gameState, zone, zoneExtremities, penaltySettings, gameSettings, nextZoneDate, isShrinking, startDate]);
return ( return (
<adminContext.Provider value={value}> <adminContext.Provider value={value}>
{children} {children}