This commit is contained in:
Sebastien Riviere
2025-09-05 23:37:19 +02:00
parent 999c9a8b77
commit 93f9a05b1a
13 changed files with 356 additions and 400 deletions

View File

@@ -41,12 +41,12 @@
- [x] Refaire les flèches de chasse sur la map - [x] Refaire les flèches de chasse sur la map
- [ ] Mettre en évidence le menu paramètre (configuration) - [ ] Mettre en évidence le menu paramètre (configuration)
- [ ] Afficher un feedback quand un paramètre est sauvegardé - [ ] Afficher un feedback quand un paramètre est sauvegardé
- [ ] Pouvoir définir la zone de départ de chaque équipe - [ ] (IMPORTANT) Pouvoir définir la zone de départ de chaque équipe
- [ ] Nommer les polygons par des lettres de l'alphabet - [ ] (IMPORTANT) Nommer les polygons par des lettres de l'alphabet
- [ ] Faire un menu quand on arrive sur la traque - [ ] Faire un menu quand on arrive sur la traque
- [ ] Pouvoir load des paramètres enregistrés - [ ] Pouvoir load des paramètres enregistrés
- [ ] Améliorer le système de création zone (cercle et polygone) - [ ] (IMPORTANT) Améliorer le système de création zone (cercle et polygone)
- [ ] Améliorer la sélection du système de zone - [ ] (IMPORTANT) Améliorer la sélection du système de zone
- [ ] Penser l'affichage en fin de traque - [ ] Penser l'affichage en fin de traque
### Améliorations du jeu de la traque ### Améliorations du jeu de la traque

View File

@@ -1,31 +1,19 @@
/*
This module manages the admin access to the server via websocket.
It receives messages, checks permissions, manages authentication and performs actions by calling functions from other modules.
This module also exposes functions to send messages via socket to all admins
*/
import { io } from "./index.js"; import { io } from "./index.js";
import game from "./game.js"
import zoneManager from "./zone_manager.js"
import { playersBroadcast, sendUpdatedTeamInformations } from "./team_socket.js";
import { createHash } from "crypto"; import { createHash } from "crypto";
import { config } from "dotenv"; import { config } from "dotenv";
import game from "./game.js"
import zoneManager from "./zone_manager.js"
config(); config();
const ADMIN_PASSWORD_HASH = process.env.ADMIN_PASSWORD_HASH; const ADMIN_PASSWORD_HASH = process.env.ADMIN_PASSWORD_HASH;
/**
* Send a message to all logged in admin sockets
* @param {String} event The event name
* @param {String} data The data to send
*/
export function secureAdminBroadcast(event, data) { export function secureAdminBroadcast(event, data) {
loggedInSockets.forEach(s => { loggedInSockets.forEach(s => {
io.of("admin").to(s).emit(event, data); io.of("admin").to(s).emit(event, data);
}); });
} }
// Array of logged in sockets
let loggedInSockets = []; let loggedInSockets = [];
export function initAdminSocketHandler() { export function initAdminSocketHandler() {
@@ -33,22 +21,32 @@ export function initAdminSocketHandler() {
console.log("Connection of an admin"); console.log("Connection of an admin");
let loggedIn = false; let loggedIn = false;
socket.on("disconnect", () => { const login = (password) => {
console.log("Disconnection of an admin"); if (loggedIn) return false;
if (createHash('sha256').update(password).digest('hex') !== ADMIN_PASSWORD_HASH) return false;
loggedInSockets.push(socket.id);
loggedIn = true;
return true;
}
const logout = () => {
if (!loggedIn) return false;
loggedInSockets = loggedInSockets.filter(s => s !== socket.id); loggedInSockets = loggedInSockets.filter(s => s !== socket.id);
loggedIn = false; loggedIn = false;
return true;
}
socket.on("disconnect", () => {
console.log("Disconnection of an admin");
logout();
}); });
socket.on("logout", () => { socket.on("logout", () => {
loggedInSockets = loggedInSockets.filter(s => s !== socket.id); logout();
loggedIn = false; });
})
socket.on("login", (password) => { socket.on("login", (password) => {
const hash = createHash('sha256').update(password).digest('hex'); if (!login(password)) return;
if (hash === ADMIN_PASSWORD_HASH && !loggedIn) {
loggedInSockets.push(socket.id);
loggedIn = true;
socket.emit("teams", game.teams); socket.emit("teams", game.teams);
socket.emit("game_state", { socket.emit("game_state", {
state: game.state, state: game.state,
@@ -60,25 +58,26 @@ export function initAdminSocketHandler() {
endDate: zoneManager.currentZoneEndDate, endDate: zoneManager.currentZoneEndDate,
}); });
socket.emit("settings", game.getSettings()); socket.emit("settings", game.getSettings());
}
}); });
socket.on("update_settings", (settings) => {
if (!loggedIn) return;
game.changeSettings(settings);
secureAdminBroadcast("settings", game.getSettings());
})
socket.on("add_team", (teamName) => { socket.on("add_team", (teamName) => {
if (!loggedIn) return; if (!loggedIn) return;
game.addTeam(teamName); game.addTeam(teamName);
secureAdminBroadcast("teams", game.teams);
}); });
socket.on("remove_team", (teamId) => { socket.on("remove_team", (teamId) => {
if (!loggedIn) return; if (!loggedIn) return;
game.removeTeam(teamId); game.removeTeam(teamId);
secureAdminBroadcast("teams", game.teams); });
socket.on("reorder_teams", (newOrder) => {
if (!loggedIn) return;
game.reorderTeams(newOrder);
});
socket.on("capture_team", (teamId, newTeam) => {
if (!loggedIn) return;
game.captureTeam(teamId, newTeam);
}); });
socket.on("change_state", (state) => { socket.on("change_state", (state) => {
@@ -86,22 +85,9 @@ export function initAdminSocketHandler() {
game.setState(state); game.setState(state);
}); });
// Use is sending a new list containing the new order of the teams socket.on("update_settings", (settings) => {
// Note that we never check if the new order contains the same teams as the old order, so it behaves more like a setTeams function
// But the frontend should always send the same teams in a different order
socket.on("reorder_teams", (newOrder) => {
if (!loggedIn) return; if (!loggedIn) return;
game.reorderTeams(newOrder); game.changeSettings(settings);
secureAdminBroadcast("teams", game.teams);
game.teams.forEach(t => sendUpdatedTeamInformations(t.id));
}); });
socket.on("update_team", (teamId, newTeam) => {
if (!loggedIn) return;
game.updateTeam(teamId, newTeam);
secureAdminBroadcast("teams", game.teams);
sendUpdatedTeamInformations(teamId);
sendUpdatedTeamInformations(game.getTeam(teamId).chased);
})
}); });
} }

View File

@@ -1,19 +1,13 @@
/*
This module manages the main game state, the teams, the settings and the game logic
*/
import { secureAdminBroadcast } from "./admin_socket.js"; import { secureAdminBroadcast } from "./admin_socket.js";
import { teamBroadcast, playersBroadcast, sendUpdatedTeamInformations, } from "./team_socket.js"; import { teamBroadcast, playersBroadcast, sendUpdatedTeamInformations } from "./team_socket.js";
import { sendPositionTimeouts, outOfZoneTimeouts } from "./timeout_handler.js"; import { sendPositionTimeouts, outOfZoneTimeouts } from "./timeout_handler.js";
import zoneManager from "./zone_manager.js"; import zoneManager from "./zone_manager.js";
import trajectory from "./trajectory.js"; import trajectory from "./trajectory.js";
/** function randint(max) {
* Compute the distance between two points givent their longitude and latitude return Math.floor(Math.random() * max);
* @param {Object} pos1 The first position }
* @param {Object} pos2 The second position
* @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 }) { function getDistanceFromLatLon({ lat: lat1, lng: lon1 }, { lat: lat2, lng: lon2 }) {
const degToRad = (deg) => deg * (Math.PI / 180); const degToRad = (deg) => deg * (Math.PI / 180);
var R = 6371; // Radius of the earth in km var R = 6371; // Radius of the earth in km
@@ -29,20 +23,10 @@ function getDistanceFromLatLon({ lat: lat1, lng: lon1 }, { lat: lat2, lng: lon2
return d * 1000; return d * 1000;
} }
/**
* Check if a GPS point is in a circle
* @param {Object} position The position to check, an object with lat and lng fields
* @param {Object} center The center of the circle, an object with lat and lng fields
* @param {Number} radius The radius of the circle in meters
* @returns
*/
function isInCircle(position, center, radius) { function isInCircle(position, center, radius) {
return getDistanceFromLatLon(position, center) < radius; return getDistanceFromLatLon(position, center) < radius;
} }
/**
* The possible states of the game
*/
export const GameState = { export const GameState = {
SETUP: "setup", SETUP: "setup",
PLACEMENT: "placement", PLACEMENT: "placement",
@@ -51,7 +35,7 @@ export const GameState = {
} }
export default { export default {
// List of teams, as objects. To see the fields see the addTeam methods // List of teams, as objects. To see the fields see the addTeam method
teams: [], teams: [],
// Current state of the game // Current state of the game
state: GameState.SETUP, state: GameState.SETUP,
@@ -65,41 +49,50 @@ export default {
loser: "", loser: "",
}, },
getSettings() {
return {
messages: this.messages, /* ------------------------------- USEFUL FUNCTIONS ------------------------------- */
zone: zoneManager.settings,
sendPositionDelay: sendPositionTimeouts.delay, getNewTeamId() {
outOfZoneDelay: outOfZoneTimeouts.delay let id = randint(1_000_000);
}; while (this.teams.find(t => t.id === id)) id = randint(1_000_000);
return id;
}, },
/** checkEndGame() {
* Update the game settings if (this.teams.filter(team => !team.captured) <= 2) this.setState(GameState.FINISHED);
* @param {Object} newSettings settings to be updated, can be partial
*/
changeSettings(newSettings) {
if ("messages" in newSettings) this.messages = {...this.messages, ...newSettings.messages};
if ("zone" in newSettings) zoneManager.changeSettings(newSettings.zone);
if ("sendPositionDelay" in newSettings) sendPositionTimeouts.setDelay(newSettings.sendPositionDelay);
if ("outOfZoneDelay" in newSettings) outOfZoneTimeouts.setDelay(newSettings.outOfZoneDelay);
}, },
/** updateChasingChain() {
* Change the state of the game to newState and start the necessary processes const playingTeams = this.teams.filter(team => !team.captured);
* @param {String} newState
*/ for (let i = 0; i < playingTeams.length; i++) {
setState(newState) { playingTeams[i].chasing = playingTeams[(i+1) % playingTeams.length].id;
// Checks is the newState is a Gamestate playingTeams[i].chased = playingTeams[(playingTeams.length + i-1) % playingTeams.length].id;
if (Object.values(GameState).indexOf(newState) == -1) return false; sendUpdatedTeamInformations(playingTeams[i].id);
// Match case }
switch (newState) { },
case GameState.SETUP:
trajectory.stop(); initLastSentLocations() {
zoneManager.stop(); const dateNow = Date.now();
sendPositionTimeouts.clearAll(); // Update of lastSentLocation
outOfZoneTimeouts.clearAll(); for (const team of this.teams) {
for (let team of this.teams) { team.lastSentLocation = team.currentLocation;
team.locationSendDeadline = dateNow + sendPositionTimeouts.duration * 60 * 1000;
sendPositionTimeouts.set(team.id);
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);
}
// Broadcast new infos
secureAdminBroadcast("teams", this.teams);
},
resetTeamsInfos() {
for (const team of this.teams) {
// Chasing // Chasing
team.captured = false; team.captured = false;
team.chasing = null; team.chasing = null;
@@ -119,9 +112,43 @@ export default {
team.nSentLocation = 0; team.nSentLocation = 0;
team.nObserved = 0; team.nObserved = 0;
team.finishDate = null; team.finishDate = null;
sendUpdatedTeamInformations(team.id);
} }
this.stateDate = Date.now(); secureAdminBroadcast("teams", this.teams);
this.updateTeamChasing(); },
/* ------------------------------- STATE AND SETTINGS FUNCTIONS ------------------------------- */
getSettings() {
return {
messages: this.messages,
zone: zoneManager.settings,
sendPositionDelay: sendPositionTimeouts.delay,
outOfZoneDelay: outOfZoneTimeouts.delay
};
},
changeSettings(newSettings) {
if ("messages" in newSettings) this.messages = {...this.messages, ...newSettings.messages};
if ("zone" in newSettings) zoneManager.changeSettings(newSettings.zone);
if ("sendPositionDelay" in newSettings) sendPositionTimeouts.setDelay(newSettings.sendPositionDelay);
if ("outOfZoneDelay" in newSettings) outOfZoneTimeouts.setDelay(newSettings.outOfZoneDelay);
// Broadcast new infos
secureAdminBroadcast("settings", this.getSettings());
playersBroadcast("game_settings", this.messages);
},
setState(newState) {
const dateNow = Date.now();
switch (newState) {
case GameState.SETUP:
trajectory.stop();
zoneManager.stop();
sendPositionTimeouts.clearAll();
outOfZoneTimeouts.clearAll();
this.resetTeamsInfos();
break; break;
case GameState.PLACEMENT: case GameState.PLACEMENT:
if (this.teams.length < 3) { if (this.teams.length < 3) {
@@ -132,7 +159,6 @@ export default {
zoneManager.stop(); zoneManager.stop();
sendPositionTimeouts.clearAll(); sendPositionTimeouts.clearAll();
outOfZoneTimeouts.clearAll(); outOfZoneTimeouts.clearAll();
this.stateDate = Date.now();
break; break;
case GameState.PLAYING: case GameState.PLAYING:
if (this.teams.length < 3) { if (this.teams.length < 3) {
@@ -142,60 +168,86 @@ export default {
trajectory.start(); trajectory.start();
zoneManager.start(); zoneManager.start();
this.initLastSentLocations(); this.initLastSentLocations();
this.stateDate = Date.now();
break; break;
case GameState.FINISHED: case GameState.FINISHED:
if (this.state != GameState.PLAYING) { if (this.state != GameState.PLAYING) {
secureAdminBroadcast("game_state", {state: this.state, stateDate: this.stateDate}); secureAdminBroadcast("game_state", {state: this.state, stateDate: this.stateDate});
return false; return false;
} }
for (const team of this.teams) {
if (!team.finishDate) team.finishDate = Date.now();
}
trajectory.stop(); trajectory.stop();
zoneManager.stop(); zoneManager.stop();
sendPositionTimeouts.clearAll(); sendPositionTimeouts.clearAll();
outOfZoneTimeouts.clearAll(); outOfZoneTimeouts.clearAll();
this.teams.forEach(team => {if (!team.finishDate) team.finishDate = dateNow});
secureAdminBroadcast("teams", this.teams);
break; break;
} }
// Update the state // Update the state
this.state = newState; this.state = newState;
this.stateDate = dateNow;
// Broadcast new infos
secureAdminBroadcast("game_state", {state: newState, stateDate: this.stateDate}); secureAdminBroadcast("game_state", {state: newState, stateDate: this.stateDate});
playersBroadcast("game_state", newState); playersBroadcast("game_state", newState);
return true;
},
/* ------------------------------- MANAGE PLAYERS FUNCTIONS ------------------------------- */
addPlayer(teamId, socketId) {
// Test of parameters
if (!this.hasTeam(teamId)) return false;
// Variables
const team = this.getTeam(teamId);
// Add the player
team.sockets.push(socketId);
// Broadcast new infos
secureAdminBroadcast("teams", this.teams); secureAdminBroadcast("teams", this.teams);
return true;
}, },
/** removePlayer(teamId, socketId) {
* Get a new unused team id // Test of parameters
* @returns a new unique team id if (!this.hasTeam(teamId)) return false;
*/ // Variables
getNewTeamId() { const team = this.getTeam(teamId);
let id = null; // Remove the player and its data
while (id === null || this.teams.find(t => t.id === id)) { if (this.isCapitain(teamId, socketId)) {
id = Math.floor(Math.random() * 1_000_000); team.battery = null;
team.phoneModel = null;
team.phoneName = null;
} }
return id; team.sockets = team.sockets.filter((sid) => sid != socketId);
// Broadcast new infos
secureAdminBroadcast("teams", this.teams);
sendUpdatedTeamInformations(team.id);
return true;
}, },
/** isPlayerCapitain(teamId, socketId) {
* Return a random capture code return this.getTeam(teamId).sockets.indexOf(socketId) == 0;
* @returns a random 4 digit number },
*/
createCaptureCode() {
return Math.floor(Math.random() * 10000);
/* ------------------------------- MANAGE TEAMS FUNCTIONS ------------------------------- */
getTeam(teamId) {
return this.teams.find(t => t.id === teamId);
},
hasTeam(teamId) {
return this.teams.some(t => t.id === teamId);
}, },
/**
* Add a new team to the game
* @param {String} teamName the name of the team
*/
addTeam(teamName) { addTeam(teamName) {
this.teams.push({ this.teams.push({
// Identification // Identification
sockets: [], sockets: [],
name: teamName, name: teamName,
id: this.getNewTeamId(), id: this.getNewTeamId(this.teams),
captureCode: this.createCaptureCode(), captureCode: randint(10_000),
// Chasing // Chasing
captured: false, captured: false,
chasing: null, chasing: null,
@@ -223,204 +275,150 @@ export default {
phoneName: null, phoneName: null,
battery: null, battery: null,
}); });
this.updateTeamChasing(); this.updateChasingChain();
}, // Broadcast new infos
/**
* Update the chasing chain of the teams based of the ordre of the teams array
* If there are only 2 teams left, the game will end
* This function will update the chasing and chased values of each teams
*/
updateTeamChasing() {
const playingTeams = this.teams.reduce((count, team) => count + (!team.captured ? 1 : 0), 0);
if (playingTeams <= 2) {
if (this.state == GameState.PLAYING) this.setState(GameState.FINISHED);
return;
}
let firstTeam = null;
let previousTeam = null
for (let i = 0; i < this.teams.length; i++) {
if (!this.teams[i].captured) {
if (previousTeam != null) {
this.teams[i].chased = previousTeam;
this.getTeam(previousTeam).chasing = this.teams[i].id;
} else {
firstTeam = this.teams[i].id;
}
previousTeam = this.teams[i].id;
}
}
this.getTeam(firstTeam).chased = previousTeam;
this.getTeam(previousTeam).chasing = firstTeam;
secureAdminBroadcast("teams", this.teams); secureAdminBroadcast("teams", this.teams);
return true;
}, },
/** removeTeam(teamId) {
* Rearrange the order of the teams and update the chasing chain // Test of parameters
* @param {Array} newOrder An array of teams in the new order if (!this.hasTeam(teamId)) return false;
*/ // Logout the team
reorderTeams(newOrder) { teamBroadcast(teamId, "logout");
this.teams = newOrder; this.teams = this.teams.filter(t => t.id !== teamId);
this.updateTeamChasing(); sendPositionTimeouts.clear(teamId);
outOfZoneTimeouts.clear(teamId);
this.updateChasingChain();
this.checkEndGame();
// Broadcast new infos
secureAdminBroadcast("teams", this.teams);
return true;
}, },
/** captureTeam(teamId) {
* Get a team by its ID // Test of parameters
* @param {Number} teamId The id of the team if (!this.hasTeam(teamId)) return false;
* @returns the team object or undefined if not found // Variables
*/
getTeam(teamId) {
return this.teams.find(t => t.id === teamId);
},
/**
* Update a team's values
* @param {Number} teamId The id of the team to update
* @param {Object} newTeam An object containing the new values of the team, can be partial
*/
updateTeam(teamId, newTeam) {
this.teams = this.teams.map((t) => {
if (t.id == teamId) {
return { ...t, ...newTeam };
} else {
return t;
}
})
this.updateTeamChasing();
},
/**
*
* @param {Number} teamId The ID of the team which location will be updated
* @param {Array} location An array containing in order the latitude and longitude of the new location
*/
updateLocation(teamId, location) {
const team = this.getTeam(teamId); const team = this.getTeam(teamId);
if (!team || !location) { const dateNow = Date.now();
return; // Make the capture
} team.captured = true;
team.finishDate = dateNow;
team.chasing = null;
team.chased = null;
sendPositionTimeouts.clear(team.id);
outOfZoneTimeouts.clear(team.id);
this.updateChasingChain();
this.checkEndGame();
// Broadcast new infos
secureAdminBroadcast("teams", this.teams);
sendUpdatedTeamInformations(team.id);
return true;
},
reorderTeams(newOrder) {
// Update teams
const teamMap = new Map(this.teams.map(team => [team.id, team]));
this.teams = newOrder.map(id => teamMap.get(id));
this.updateChasingChain();
// Broadcast new infos
secureAdminBroadcast("teams", this.teams);
return true;
},
handicapTeam(teamId) {
// TODO
},
/* ------------------------------- PLAYERS ACTIONS FUNCTIONS ------------------------------- */
updateLocation(teamId, location) {
// Test of parameters
if (!this.hasTeam(teamId)) return false;
if (!location) return false;
// Variables
const team = this.getTeam(teamId);
const dateNow = Date.now();
// Update distance
if (team.currentLocation) team.distance += Math.floor(getDistanceFromLatLon({lat: location[0], lng: location[1]}, {lat: team.currentLocation[0], lng: team.currentLocation[1]})); 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
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) team.lastCurrentLocationDate = dateNow;
if (this.state == GameState.PLACEMENT && team.startingArea && team.startingArea && location) { // Update of ready
if (this.state == GameState.PLACEMENT && team.startingArea) {
team.ready = isInCircle({ lat: location[0], lng: location[1] }, team.startingArea.center, team.startingArea.radius); team.ready = isInCircle({ lat: location[0], lng: location[1] }, team.startingArea.center, team.startingArea.radius);
} }
// Verify zone // Update out of zone
const teamCurrentlyOutOfZone = !zoneManager.isInZone({ lat: location[0], lng: location[1] }) const teamCurrentlyOutOfZone = !zoneManager.isInZone({ lat: location[0], lng: location[1] })
if (teamCurrentlyOutOfZone && !team.outOfZone) { if (teamCurrentlyOutOfZone && !team.outOfZone) {
team.outOfZone = true; team.outOfZone = true;
team.outOfZoneDeadline = Date.now() + outOfZoneTimeouts.duration * 60 * 1000; team.outOfZoneDeadline = dateNow + outOfZoneTimeouts.duration * 60 * 1000;
outOfZoneTimeouts.set(teamId); outOfZoneTimeouts.set(teamId);
} else if (!teamCurrentlyOutOfZone && team.outOfZone) { } else if (!teamCurrentlyOutOfZone && team.outOfZone) {
team.outOfZone = false; team.outOfZone = false;
team.outOfZoneDeadline = null; team.outOfZoneDeadline = null;
outOfZoneTimeouts.clear(teamId); outOfZoneTimeouts.clear(teamId);
} }
// Sending new infos to the team // Broadcast new infos
secureAdminBroadcast("teams", this.teams);
sendUpdatedTeamInformations(team.id); sendUpdatedTeamInformations(team.id);
},
/**
* 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() + sendPositionTimeouts.duration * 60 * 1000;
sendPositionTimeouts.set(team.id);
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);
}
},
/**
* Get the most recent enemy team's location as well as setting the latest accessible location to the current one
* @param {Number} teamId The ID of the team that will send its location
*/
sendLocation(teamId) {
const team = this.getTeam(teamId);
if (!team || !team.currentLocation) {
return;
}
const enemyTeam = this.getTeam(team.chasing);
team.nSentLocation++;
enemyTeam.nObserved++;
const dateNow = Date.now();
// Update of events of the game // Update of events of the game
trajectory.writeSeePosition(dateNow, teamId, team.chasing); trajectory.writePosition(dateNow, team.id, location[0], location[1]);
// Update of locationSendDeadline return true;
},
sendLocation(teamId) {
// Test of parameters
if (!this.hasTeam(teamId)) return false;
// Variables
const team = this.getTeam(teamId);
const enemyTeam = this.getTeam(team.chasing);
const dateNow = Date.now();
// Update team
team.nSentLocation++;
team.lastSentLocation = team.currentLocation;
team.enemyLocation = enemyTeam.lastSentLocation;
team.locationSendDeadline = dateNow + sendPositionTimeouts.duration * 60 * 1000; team.locationSendDeadline = dateNow + sendPositionTimeouts.duration * 60 * 1000;
sendPositionTimeouts.set(team.id); sendPositionTimeouts.set(team.id);
// Update of lastSentLocation // Update enemy
team.lastSentLocation = team.currentLocation; enemyTeam.nObserved++;
// Update of enemyLocation // Broadcast new infos
const teamChasing = this.getTeam(team.chasing); secureAdminBroadcast("teams", this.teams);
if (teamChasing) team.enemyLocation = teamChasing.lastSentLocation;
// Sending new infos to the team
sendUpdatedTeamInformations(team.id); sendUpdatedTeamInformations(team.id);
sendUpdatedTeamInformations(enemyTeam.id); sendUpdatedTeamInformations(enemyTeam.id);
// Update of events of the game
trajectory.writeSeePosition(dateNow, team.id, enemyTeam.id);
return true;
}, },
/** tryCapture(teamId, captureCode) {
* Remove a team by its ID // Test of parameters
* @param {Number} teamId The id of the team to remove if (!this.hasTeam(teamId)) return false;
*/ // Variables
removeTeam(teamId) {
if (!this.getTeam(teamId)) {
return;
}
teamBroadcast("logout");
this.teams = this.teams.filter(t => t.id !== teamId);
this.updateTeamChasing();
sendPositionTimeouts.clear(teamId);
outOfZoneTimeouts.clear(teamId);
},
/**
* Request a capture initiated by the team with id teamId (the one trying to capture)
* If the captureCode match, the team chased by teamId will be set to captured
* And the chase chain will be updated
* @param {Number} teamId The id of the capturing team
* @param {Number} captureCode The code sent by the capturing that only the captured team know, used to verify the authenticity of the capture
*/
requestCapture(teamId, captureCode) {
const team = this.getTeam(teamId); const team = this.getTeam(teamId);
const enemyTeam = this.getTeam(team.chasing); const enemyTeam = this.getTeam(team.chasing);
if (!enemyTeam || enemyTeam.captureCode != captureCode) { const dateNow = Date.now();
return; // Verify the capture
} if (enemyTeam.captureCode != captureCode) return false;
// Make the capture
team.nCaptures++; team.nCaptures++;
// Update of events of the game enemyTeam.captured = true;
trajectory.writeCapture(Date.now(), teamId, enemyTeam.id); enemyTeam.finishDate = dateNow;
// Update of capture and chasing cycle enemyTeam.chasing = null;
this.capture(enemyTeam.id); enemyTeam.chased = null;
// Sending new infos to the teams sendPositionTimeouts.clear(enemyTeam.id);
outOfZoneTimeouts.clear(enemyTeam.id);
this.updateChasingChain();
this.checkEndGame();
// Broadcast new infos
secureAdminBroadcast("teams", this.teams);
sendUpdatedTeamInformations(team.id); sendUpdatedTeamInformations(team.id);
sendUpdatedTeamInformations(enemyTeam.id); sendUpdatedTeamInformations(enemyTeam.id);
// Update of events of the game
trajectory.writeCapture(dateNow, team.id, enemyTeam.id);
return true;
}, },
/**
* Set a team to captured and update the chase chain
* @param {Number} teamId the Id of the captured team
*/
capture(teamId) {
const team = this.getTeam(teamId);
team.captured = true;
team.finishDate = Date.now();
sendPositionTimeouts.clear(teamId);
outOfZoneTimeouts.clear(teamId);
this.updateTeamChasing();
},
handicapTeam(teamId) {
// TODO
}
} }

View File

@@ -3,7 +3,6 @@ This file manages team access to the server via websocket.
It receives messages, checks permissions, manages authentication and performs actions by calling functions from other modules. It receives messages, checks permissions, manages authentication and performs actions by calling functions from other modules.
This module also exposes functions to send messages via socket to all teams This module also exposes functions to send messages via socket to all teams
*/ */
import { secureAdminBroadcast } from "./admin_socket.js";
import { io } from "./index.js"; import { io } from "./index.js";
import game from "./game.js"; import game from "./game.js";
import zoneManager from "./zone_manager.js"; import zoneManager from "./zone_manager.js";
@@ -24,9 +23,7 @@ export function teamBroadcast(teamId, event, data) {
* @param {String} data payload * @param {String} data payload
*/ */
export function playersBroadcast(event, data) { export function playersBroadcast(event, data) {
for (const team of game.teams) { game.teams.forEach(team => teamBroadcast(team.id, event, data));
teamBroadcast(team.id, event, data);
}
} }
/** /**
@@ -42,7 +39,7 @@ export function sendUpdatedTeamInformations(teamId) {
captureCode: team.captureCode, captureCode: team.captureCode,
// Chasing // Chasing
captured: team.captured, captured: team.captured,
enemyName: game.getTeam(team.chasing)? game.getTeam(team.chasing).name : null, enemyName: game.getTeam(team.chasing)?.name ?? null,
// Locations // Locations
lastSentLocation: team.lastSentLocation, lastSentLocation: team.lastSentLocation,
enemyLocation: team.enemyLocation, enemyLocation: team.enemyLocation,
@@ -57,10 +54,9 @@ export function sendUpdatedTeamInformations(teamId) {
distance: team.distance, distance: team.distance,
nCaptures: team.nCaptures, nCaptures: team.nCaptures,
nSentLocation: team.nSentLocation, nSentLocation: team.nSentLocation,
startDate: game.startDate, stateDate: game.stateDate,
finishDate: team.finishDate, finishDate: team.finishDate,
}) });
secureAdminBroadcast("teams", game.teams);
} }
export function initTeamSocket() { export function initTeamSocket() {
@@ -68,42 +64,36 @@ export function initTeamSocket() {
console.log("Connection of a player"); console.log("Connection of a player");
let teamId = null; let teamId = null;
const logoutPlayer = () => { const login = (loginTeamId) => {
if (!teamId) return; logout();
const team = game.getTeam(teamId); if (!game.addPlayer(loginTeamId, socket.id)) return false;
if (team.sockets.indexOf(socket.id) == 0) { teamId = loginTeamId;
team.battery = null; return true;
team.phoneModel = null;
team.phoneName = null;
} }
// Delete the player from the team
team.sockets = team.sockets.filter((sid) => sid != socket.id); const logout = () => {
secureAdminBroadcast("teams", game.teams); if (!teamId) return;
socket.emit("logout"); game.removePlayer(teamId, socket.id);
teamId = null; teamId = null;
} }
socket.on("disconnect", () => { socket.on("disconnect", () => {
console.log("Disconnection of a player"); console.log("Disconnection of a player");
logoutPlayer(); logout();
}); });
socket.on("logout", () => { socket.on("logout", () => {
logoutPlayer(); logout();
}); });
socket.on("login", (loginTeamId, callback) => { socket.on("login", (loginTeamId, callback) => {
logoutPlayer(); if (!login(loginTeamId)) {
const team = game.getTeam(loginTeamId);
if (!team) {
callback({ isLoggedIn: false, message: "Login denied" }); callback({ isLoggedIn: false, message: "Login denied" });
return; return;
} }
teamId = loginTeamId;
team.sockets.push(socket.id);
sendUpdatedTeamInformations(loginTeamId); sendUpdatedTeamInformations(loginTeamId);
socket.emit("game_state", game.state); socket.emit("game_state", game.state);
socket.emit("game_settings", game.settings); socket.emit("game_settings", game.messages);
socket.emit("zone", { socket.emit("zone", {
type: zoneManager.settings.type, type: zoneManager.settings.type,
begin: zoneManager.getCurrentZone(), begin: zoneManager.getCurrentZone(),
@@ -115,13 +105,9 @@ export function initTeamSocket() {
socket.on("update_position", (position) => { socket.on("update_position", (position) => {
if (!teamId) return; if (!teamId) return;
const team = game.getTeam(teamId); if (game.isPlayerCapitain(teamId, socket.id)) {
// Only the first socket can update the current position since he is the one whose location is tracked
if (team.sockets.indexOf(socket.id) == 0) {
game.updateLocation(teamId, position); game.updateLocation(teamId, position);
team.lastCurrentLocationDate = Date.now();
} }
secureAdminBroadcast("teams", game.teams);
}); });
socket.on("send_position", () => { socket.on("send_position", () => {
@@ -131,26 +117,24 @@ export function initTeamSocket() {
socket.on("capture", (captureCode, callback) => { socket.on("capture", (captureCode, callback) => {
if (!teamId) return; if (!teamId) return;
game.requestCapture(teamId, captureCode); if (game.tryCapture(teamId, captureCode)) {
callback({ hasCaptured : true, message: "Capture successful" }); callback({ hasCaptured : true, message: "Capture successful" });
} else {
callback({ hasCaptured : false, message: "Capture denied" });
}
}); });
socket.on("device_info", (infos) => { socket.on("device_info", (infos) => {
if (!teamId) return; if (!teamId) return;
const team = game.getTeam(teamId); if (game.isPlayerCapitain(teamId, socket.id)) {
// Only the first socket shares its infos since he is the one whose location is tracked game.updateTeam(teamId, {phoneModel: infos.model, phoneName: infos.name});
if (team.sockets.indexOf(socket.id) == 0) {
team.phoneModel = infos.model;
team.phoneName = infos.name;
} }
}); });
socket.on("battery_update", (batteryLevel) => { socket.on("battery_update", (batteryLevel) => {
if (!teamId) return; if (!teamId) return;
const team = game.getTeam(teamId); if (game.isPlayerCapitain(teamId, socket.id)) {
// Only the first socket shares its infos since he is the one whose location is tracked game.updateTeam(teamId, {battery: batteryLevel});
if (team.sockets.indexOf(socket.id) == 0) {
team.battery = batteryLevel;
} }
}); });
}); });

View File

@@ -89,7 +89,7 @@ export default function TeamSidePanel({ selectedTeamId, onClose }) {
/> />
</div> </div>
<div> <div>
<DotLine label="ID d'équipe" value={String(team.id).padStart(6, '0').replace(/(\d{3})(\d{3})/, "$1 $2")} /> <DotLine label="ID d'équipe" value={String(selectedTeamId).padStart(6, '0').replace(/(\d{3})(\d{3})/, "$1 $2")} />
<DotLine label="ID de capture" value={String(team.captureCode).padStart(4, '0')} /> <DotLine label="ID de capture" value={String(team.captureCode).padStart(4, '0')} />
</div> </div>
<div> <div>

View File

@@ -16,8 +16,8 @@ const LiveMap = dynamic(() => import('./components/liveMap'), { ssr: false });
export default function AdminPage() { export default function AdminPage() {
const { useProtect } = useAdminConnexion(); const { useProtect } = useAdminConnexion();
const [selectedTeamId, setSelectedTeamId] = useState(null);
const { changeState, getTeam } = useAdmin(); const { changeState, getTeam } = useAdmin();
const [selectedTeamId, setSelectedTeamId] = useState(null);
const [mapStyle, setMapStyle] = useState(mapStyles.default); const [mapStyle, setMapStyle] = useState(mapStyles.default);
const [showZones, setShowZones] = useState(true); const [showZones, setShowZones] = useState(true);
const [showNames, setShowNames] = useState(true); const [showNames, setShowNames] = useState(true);

View File

@@ -1,7 +1,7 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Circle } from "react-leaflet"; import { Circle } from "react-leaflet";
import "leaflet/dist/leaflet.css"; import "leaflet/dist/leaflet.css";
import { BlueButton, GreenButton, RedButton } from "@/components/button"; import { CustomButton } from "@/components/button";
import { CustomMapContainer, MapEventListener } from "@/components/map"; import { CustomMapContainer, MapEventListener } from "@/components/map";
import { TextInput } from "@/components/input"; import { TextInput } from "@/components/input";
import useAdmin from "@/hook/useAdmin"; import useAdmin from "@/hook/useAdmin";
@@ -43,8 +43,7 @@ export default function CircleZoneSelector({zoneSettings, modifyZoneSettings, ap
} }
function customStringToInt(e) { function customStringToInt(e) {
const res = parseInt(e, 10); return parseInt(e, 10) || null;
return isNaN(res) ? null : res;
} }
return ( return (
@@ -56,8 +55,8 @@ export default function CircleZoneSelector({zoneSettings, modifyZoneSettings, ap
</div> </div>
<div className="h-full w-1/6 flex flex-col gap-3"> <div className="h-full w-1/6 flex flex-col gap-3">
<div className="w-full h-15"> <div className="w-full h-15">
{editMode == EditMode.MIN && <BlueButton onClick={() => setEditMode(EditMode.MAX)}>Click to edit first zone</BlueButton>} {editMode == EditMode.MIN && <CustomButton color="blue" onClick={() => setEditMode(EditMode.MAX)}>Click to edit first zone</CustomButton>}
{editMode == EditMode.MAX && <RedButton onClick={() => setEditMode(EditMode.MIN)}>Click to edit last zone</RedButton>} {editMode == EditMode.MAX && <CustomButton color="red" onClick={() => setEditMode(EditMode.MIN)}>Click to edit last zone</CustomButton>}
</div> </div>
<div className="w-full flex flex-row gap-2 items-center justify-between"> <div className="w-full flex flex-row gap-2 items-center justify-between">
<p>Reduction number</p> <p>Reduction number</p>
@@ -78,7 +77,7 @@ export default function CircleZoneSelector({zoneSettings, modifyZoneSettings, ap
</div> </div>
</div> </div>
<div className="w-full h-15"> <div className="w-full h-15">
<GreenButton onClick={handleSettingsSubmit}>Apply</GreenButton> <CustomButton color="green" onClick={handleSettingsSubmit}>Apply</CustomButton>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,7 +1,7 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Polyline, Polygon, Marker } from "react-leaflet"; import { Polyline } from "react-leaflet";
import "leaflet/dist/leaflet.css"; import "leaflet/dist/leaflet.css";
import { GreenButton } from "@/components/button"; import { CustomButton } from "@/components/button";
import { ReorderList } from "@/components/list"; import { ReorderList } from "@/components/list";
import { CustomMapContainer, MapEventListener } from "@/components/map"; import { CustomMapContainer, MapEventListener } from "@/components/map";
import { TextInput } from "@/components/input"; import { TextInput } from "@/components/input";
@@ -88,7 +88,7 @@ export default function PolygonZoneSelector({zoneSettings, modifyZoneSettings, a
</div> </div>
</div> </div>
<div className="w-full h-15"> <div className="w-full h-15">
<GreenButton onClick={handleSettingsSubmit}>Apply</GreenButton> <CustomButton color="green" onClick={handleSettingsSubmit}>Apply</CustomButton>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -2,11 +2,7 @@ import { ReorderList } from '@/components/list';
import useAdmin from '@/hook/useAdmin'; import useAdmin from '@/hook/useAdmin';
function TeamManagerItem({ team }) { function TeamManagerItem({ team }) {
const { updateTeam, removeTeam } = useAdmin(); const { captureTeam, removeTeam } = useAdmin();
function handleRemove() {
removeTeam(team.id);
}
return ( return (
<div className='w-full p-2 bg-white flex flex-row items-center text-xl gap-3 font-bold'> <div className='w-full p-2 bg-white flex flex-row items-center text-xl gap-3 font-bold'>
@@ -14,8 +10,8 @@ function TeamManagerItem({ team }) {
<p>{team.name}</p> <p>{team.name}</p>
<div className='flex flex-row items-center justify-between gap-3'> <div className='flex flex-row items-center justify-between gap-3'>
<p>{String(team.id).padStart(6, '0').replace(/(\d{3})(\d{3})/, "$1 $2")}</p> <p>{String(team.id).padStart(6, '0').replace(/(\d{3})(\d{3})/, "$1 $2")}</p>
<img src={`/icons/heart/${team.captured ? "grey" : "pink"}.png`} className="w-8 h-8" onClick={() => updateTeam(team.id, { captured: !team.captured })} /> <img src={`/icons/heart/${team.captured ? "grey" : "pink"}.png`} className="w-8 h-8" onClick={() => captureTeam(team.id)} />
<img src="/icons/trash.png" className="w-8 h-8" onClick={handleRemove} /> <img src="/icons/trash.png" className="w-8 h-8" onClick={() => removeTeam(team.id)} />
</div> </div>
</div> </div>
</div> </div>
@@ -26,7 +22,7 @@ export default function TeamManager() {
const { teams, reorderTeams } = useAdmin(); const { teams, reorderTeams } = useAdmin();
return ( return (
<ReorderList droppableId="team-manager" array={teams} setArray={reorderTeams}> <ReorderList droppableId="team-manager" array={teams} setArray={(teams) => reorderTeams(teams.map(team => team.id))}>
{(team) => ( {(team) => (
<TeamManagerItem team={team}/> <TeamManagerItem team={team}/>
)} )}

View File

@@ -4,7 +4,7 @@ import dynamic from "next/dynamic";
import Link from "next/link"; import Link from "next/link";
import { TextInput } from "@/components/input"; import { TextInput } from "@/components/input";
import { Section } from "@/components/section"; import { Section } from "@/components/section";
import { BlueButton } from "@/components/button"; import { CustomButton } from "@/components/button";
import { useAdminConnexion } from "@/context/adminConnexionContext"; import { useAdminConnexion } from "@/context/adminConnexionContext";
import useAdmin from '@/hook/useAdmin'; import useAdmin from '@/hook/useAdmin';
import Messages from "./components/messages"; import Messages from "./components/messages";
@@ -74,7 +74,7 @@ export default function ConfigurationPage() {
</div> </div>
<div className="h-full flex-1 flex flex-col p-3 gap-3 bg-white shadow-2xl"> <div className="h-full flex-1 flex flex-col p-3 gap-3 bg-white shadow-2xl">
<div className="w-full h-15"> <div className="w-full h-15">
{localZoneSettings && <BlueButton onClick={handleChangeZoneType}>Change zone type</BlueButton>} {localZoneSettings && <CustomButton color="blue" onClick={handleChangeZoneType}>Change zone type</CustomButton>}
</div> </div>
<div className="w-full flex-1"> <div className="w-full flex-1">
{localZoneSettings && localZoneSettings.type == ZoneTypes.CIRCLE && {localZoneSettings && localZoneSettings.type == ZoneTypes.CIRCLE &&

View File

@@ -1,22 +1,15 @@
export function BlueButton({ children, ...props }) { export function CustomButton({ color, children, ...props }) {
return ( const colorClasses = {
<button {...props} className="bg-blue-600 hover:bg-blue-500 text-lg ease-out duration-200 text-white w-full h-full p-4 shadow-sm rounded"> blue: 'bg-blue-600 hover:bg-blue-500',
{children} red: 'bg-red-600 hover:bg-red-500',
</button> green: 'bg-green-600 hover:bg-green-500',
); yellow: 'bg-yellow-600 hover:bg-yellow-500',
} purple: 'bg-purple-600 hover:bg-purple-500',
gray: 'bg-gray-600 hover:bg-gray-500',
};
export function RedButton({ children, ...props }) {
return ( return (
<button {...props} className="bg-red-600 hover:bg-red-500 text-lg ease-out duration-200 text-white w-full h-full p-4 shadow-sm rounded"> <button {...props} className={`${colorClasses[color]} text-lg ease-out duration-200 text-white w-full h-full p-4 shadow-sm rounded`}>
{children}
</button>
);
}
export function GreenButton({ children, ...props }) {
return (
<button {...props} className="bg-green-600 hover:bg-green-500 text-lg ease-out duration-200 text-white w-full h-full p-4 shadow-sm rounded">
{children} {children}
</button> </button>
); );

View File

@@ -17,10 +17,10 @@ export function List({array, children}) {
} }
export function ReorderList({droppableId, array, setArray, children}) { export function ReorderList({droppableId, array, setArray, children}) {
const [arrayLocal, setArrayLocal] = useState(array); const [localArray, setLocalArray] = useState(array);
useEffect(() => { useEffect(() => {
setArrayLocal(array); setLocalArray(array);
}, [array]) }, [array])
function reorder(list, startIndex, endIndex) { function reorder(list, startIndex, endIndex) {
@@ -34,7 +34,7 @@ export function ReorderList({droppableId, array, setArray, children}) {
if (!result.destination) return; if (!result.destination) return;
if (result.destination.index === result.source.index) return; if (result.destination.index === result.source.index) return;
const newArray = reorder(array, result.source.index, result.destination.index); const newArray = reorder(array, result.source.index, result.destination.index);
setArrayLocal(newArray); setLocalArray(newArray);
setArray(newArray); setArray(newArray);
} }
@@ -44,7 +44,7 @@ export function ReorderList({droppableId, array, setArray, children}) {
{provided => ( {provided => (
<div className='w-full h-full bg-gray-300 overflow-y-scroll' ref={provided.innerRef} {...provided.droppableProps}> <div className='w-full h-full bg-gray-300 overflow-y-scroll' ref={provided.innerRef} {...provided.droppableProps}>
<ul className="w-full p-1 pb-0"> <ul className="w-full p-1 pb-0">
{arrayLocal.map((elem, i) => ( {localArray.map((elem, i) => (
<li className='w-full' key={elem.id}> <li className='w-full' key={elem.id}>
<Draggable draggableId={elem.id.toString()} index={i}> <Draggable draggableId={elem.id.toString()} index={i}>
{provided => ( {provided => (

View File

@@ -23,8 +23,8 @@ export default function useAdmin() {
adminSocket.emit("remove_team", teamId); adminSocket.emit("remove_team", teamId);
} }
function updateTeam(teamId, team) { function captureTeam(teamId) {
adminSocket.emit("update_team", teamId, team); adminSocket.emit("capture_team", teamId);
} }
function changeState(state) { function changeState(state) {
@@ -35,5 +35,5 @@ export default function useAdmin() {
adminSocket.emit("update_settings", settings); adminSocket.emit("update_settings", settings);
} }
return { ...adminContext, getTeam, reorderTeams, addTeam, removeTeam, updateTeam, changeState, updateSettings }; return { ...adminContext, getTeam, reorderTeams, addTeam, removeTeam, captureTeam, changeState, updateSettings };
} }