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
- [ ] Mettre en évidence le menu paramètre (configuration)
- [ ] Afficher un feedback quand un paramètre est sauvegardé
- [ ] Pouvoir définir la zone de départ de chaque équipe
- [ ] Nommer les polygons par des lettres de l'alphabet
- [ ] (IMPORTANT) Pouvoir définir la zone de départ de chaque équipe
- [ ] (IMPORTANT) Nommer les polygons par des lettres de l'alphabet
- [ ] Faire un menu quand on arrive sur la traque
- [ ] Pouvoir load des paramètres enregistrés
- [ ] 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 le système de création zone (cercle et polygone)
- [ ] (IMPORTANT) Améliorer la sélection du système de zone
- [ ] Penser l'affichage en fin de 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 game from "./game.js"
import zoneManager from "./zone_manager.js"
import { playersBroadcast, sendUpdatedTeamInformations } from "./team_socket.js";
import { createHash } from "crypto";
import { config } from "dotenv";
import game from "./game.js"
import zoneManager from "./zone_manager.js"
config();
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) {
loggedInSockets.forEach(s => {
io.of("admin").to(s).emit(event, data);
});
}
// Array of logged in sockets
let loggedInSockets = [];
export function initAdminSocketHandler() {
@@ -33,52 +21,63 @@ export function initAdminSocketHandler() {
console.log("Connection of an admin");
let loggedIn = false;
socket.on("disconnect", () => {
console.log("Disconnection of an admin");
const login = (password) => {
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);
loggedIn = false;
return true;
}
socket.on("disconnect", () => {
console.log("Disconnection of an admin");
logout();
});
socket.on("logout", () => {
loggedInSockets = loggedInSockets.filter(s => s !== socket.id);
loggedIn = false;
})
socket.on("login", (password) => {
const hash = createHash('sha256').update(password).digest('hex');
if (hash === ADMIN_PASSWORD_HASH && !loggedIn) {
loggedInSockets.push(socket.id);
loggedIn = true;
socket.emit("teams", game.teams);
socket.emit("game_state", {
state: game.state,
date: game.stateDate
});
socket.emit("current_zone", {
begin: zoneManager.getCurrentZone(),
end: zoneManager.getNextZone(),
endDate: zoneManager.currentZoneEndDate,
});
socket.emit("settings", game.getSettings());
}
logout();
});
socket.on("update_settings", (settings) => {
if (!loggedIn) return;
game.changeSettings(settings);
secureAdminBroadcast("settings", game.getSettings());
})
socket.on("login", (password) => {
if (!login(password)) return;
socket.emit("teams", game.teams);
socket.emit("game_state", {
state: game.state,
date: game.stateDate
});
socket.emit("current_zone", {
begin: zoneManager.getCurrentZone(),
end: zoneManager.getNextZone(),
endDate: zoneManager.currentZoneEndDate,
});
socket.emit("settings", game.getSettings());
});
socket.on("add_team", (teamName) => {
if (!loggedIn) return;
game.addTeam(teamName);
secureAdminBroadcast("teams", game.teams);
});
socket.on("remove_team", (teamId) => {
if (!loggedIn) return;
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) => {
@@ -86,22 +85,9 @@ export function initAdminSocketHandler() {
game.setState(state);
});
// Use is sending a new list containing the new order of the teams
// 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) => {
socket.on("update_settings", (settings) => {
if (!loggedIn) return;
game.reorderTeams(newOrder);
secureAdminBroadcast("teams", game.teams);
game.teams.forEach(t => sendUpdatedTeamInformations(t.id));
game.changeSettings(settings);
});
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 { teamBroadcast, playersBroadcast, sendUpdatedTeamInformations, } from "./team_socket.js";
import { teamBroadcast, playersBroadcast, sendUpdatedTeamInformations } from "./team_socket.js";
import { sendPositionTimeouts, outOfZoneTimeouts } from "./timeout_handler.js";
import zoneManager from "./zone_manager.js";
import trajectory from "./trajectory.js";
/**
* Compute the distance between two points givent their longitude and latitude
* @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 randint(max) {
return Math.floor(Math.random() * max);
}
function getDistanceFromLatLon({ lat: lat1, lng: lon1 }, { lat: lat2, lng: lon2 }) {
const degToRad = (deg) => deg * (Math.PI / 180);
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;
}
/**
* 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) {
return getDistanceFromLatLon(position, center) < radius;
}
/**
* The possible states of the game
*/
export const GameState = {
SETUP: "setup",
PLACEMENT: "placement",
@@ -51,7 +35,7 @@ export const GameState = {
}
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: [],
// Current state of the game
state: GameState.SETUP,
@@ -65,6 +49,78 @@ export default {
loser: "",
},
/* ------------------------------- USEFUL FUNCTIONS ------------------------------- */
getNewTeamId() {
let id = randint(1_000_000);
while (this.teams.find(t => t.id === id)) id = randint(1_000_000);
return id;
},
checkEndGame() {
if (this.teams.filter(team => !team.captured) <= 2) this.setState(GameState.FINISHED);
},
updateChasingChain() {
const playingTeams = this.teams.filter(team => !team.captured);
for (let i = 0; i < playingTeams.length; i++) {
playingTeams[i].chasing = playingTeams[(i+1) % playingTeams.length].id;
playingTeams[i].chased = playingTeams[(playingTeams.length + i-1) % playingTeams.length].id;
sendUpdatedTeamInformations(playingTeams[i].id);
}
},
initLastSentLocations() {
const dateNow = Date.now();
// Update of lastSentLocation
for (const 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
team.captured = false;
team.chasing = null;
team.chased = null;
// Locations
team.lastSentLocation = null;
team.locationSendDeadline = null;
team.enemyLocation = null;
// Placement
team.ready = false;
// Zone
team.outOfZone = false;
team.outOfZoneDeadline = null;
// Stats
team.distance = 0;
team.nCaptures = 0;
team.nSentLocation = 0;
team.nObserved = 0;
team.finishDate = null;
sendUpdatedTeamInformations(team.id);
}
secureAdminBroadcast("teams", this.teams);
},
/* ------------------------------- STATE AND SETTINGS FUNCTIONS ------------------------------- */
getSettings() {
return {
messages: this.messages,
@@ -74,54 +130,25 @@ export default {
};
},
/**
* Update the game settings
* @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);
// Broadcast new infos
secureAdminBroadcast("settings", this.getSettings());
playersBroadcast("game_settings", this.messages);
},
/**
* Change the state of the game to newState and start the necessary processes
* @param {String} newState
*/
setState(newState) {
// Checks is the newState is a Gamestate
if (Object.values(GameState).indexOf(newState) == -1) return false;
// Match case
const dateNow = Date.now();
switch (newState) {
case GameState.SETUP:
trajectory.stop();
zoneManager.stop();
sendPositionTimeouts.clearAll();
outOfZoneTimeouts.clearAll();
for (let team of this.teams) {
// Chasing
team.captured = false;
team.chasing = null;
team.chased = null;
// Locations
team.lastSentLocation = null;
team.locationSendDeadline = null;
team.enemyLocation = null;
// Placement
team.ready = false;
// Zone
team.outOfZone = false;
team.outOfZoneDeadline = null;
// Stats
team.distance = 0;
team.nCaptures = 0;
team.nSentLocation = 0;
team.nObserved = 0;
team.finishDate = null;
}
this.stateDate = Date.now();
this.updateTeamChasing();
this.resetTeamsInfos();
break;
case GameState.PLACEMENT:
if (this.teams.length < 3) {
@@ -132,7 +159,6 @@ export default {
zoneManager.stop();
sendPositionTimeouts.clearAll();
outOfZoneTimeouts.clearAll();
this.stateDate = Date.now();
break;
case GameState.PLAYING:
if (this.teams.length < 3) {
@@ -142,60 +168,86 @@ export default {
trajectory.start();
zoneManager.start();
this.initLastSentLocations();
this.stateDate = Date.now();
break;
case GameState.FINISHED:
if (this.state != GameState.PLAYING) {
secureAdminBroadcast("game_state", {state: this.state, stateDate: this.stateDate});
return false;
}
for (const team of this.teams) {
if (!team.finishDate) team.finishDate = Date.now();
}
trajectory.stop();
zoneManager.stop();
sendPositionTimeouts.clearAll();
outOfZoneTimeouts.clearAll();
this.teams.forEach(team => {if (!team.finishDate) team.finishDate = dateNow});
secureAdminBroadcast("teams", this.teams);
break;
}
// Update the state
this.state = newState;
this.stateDate = dateNow;
// Broadcast new infos
secureAdminBroadcast("game_state", {state: newState, stateDate: this.stateDate});
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);
return true;
},
/**
* Get a new unused team id
* @returns a new unique team id
*/
getNewTeamId() {
let id = null;
while (id === null || this.teams.find(t => t.id === id)) {
id = Math.floor(Math.random() * 1_000_000);
removePlayer(teamId, socketId) {
// Test of parameters
if (!this.hasTeam(teamId)) return false;
// Variables
const team = this.getTeam(teamId);
// Remove the player and its data
if (this.isCapitain(teamId, socketId)) {
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;
},
/**
* Return a random capture code
* @returns a random 4 digit number
*/
createCaptureCode() {
return Math.floor(Math.random() * 10000);
isPlayerCapitain(teamId, socketId) {
return this.getTeam(teamId).sockets.indexOf(socketId) == 0;
},
/* ------------------------------- 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) {
this.teams.push({
// Identification
sockets: [],
name: teamName,
id: this.getNewTeamId(),
captureCode: this.createCaptureCode(),
id: this.getNewTeamId(this.teams),
captureCode: randint(10_000),
// Chasing
captured: false,
chasing: null,
@@ -223,204 +275,150 @@ export default {
phoneName: null,
battery: null,
});
this.updateTeamChasing();
},
/**
* 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;
this.updateChasingChain();
// Broadcast new infos
secureAdminBroadcast("teams", this.teams);
return true;
},
/**
* Rearrange the order of the teams and update the chasing chain
* @param {Array} newOrder An array of teams in the new order
*/
reorderTeams(newOrder) {
this.teams = newOrder;
this.updateTeamChasing();
removeTeam(teamId) {
// Test of parameters
if (!this.hasTeam(teamId)) return false;
// Logout the team
teamBroadcast(teamId, "logout");
this.teams = this.teams.filter(t => t.id !== teamId);
sendPositionTimeouts.clear(teamId);
outOfZoneTimeouts.clear(teamId);
this.updateChasingChain();
this.checkEndGame();
// Broadcast new infos
secureAdminBroadcast("teams", this.teams);
return true;
},
/**
* Get a team by its ID
* @param {Number} teamId The id of the team
* @returns the team object or undefined if not found
*/
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) {
captureTeam(teamId) {
// Test of parameters
if (!this.hasTeam(teamId)) return false;
// Variables
const team = this.getTeam(teamId);
if (!team || !location) {
return;
}
const dateNow = Date.now();
// 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]}));
// Update of events of the game
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)
if (this.state == GameState.PLACEMENT && team.startingArea && team.startingArea && location) {
team.lastCurrentLocationDate = dateNow;
// 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);
}
// Verify zone
// Update out of zone
const teamCurrentlyOutOfZone = !zoneManager.isInZone({ lat: location[0], lng: location[1] })
if (teamCurrentlyOutOfZone && !team.outOfZone) {
team.outOfZone = true;
team.outOfZoneDeadline = Date.now() + outOfZoneTimeouts.duration * 60 * 1000;
team.outOfZoneDeadline = dateNow + outOfZoneTimeouts.duration * 60 * 1000;
outOfZoneTimeouts.set(teamId);
} else if (!teamCurrentlyOutOfZone && team.outOfZone) {
team.outOfZone = false;
team.outOfZoneDeadline = null;
outOfZoneTimeouts.clear(teamId);
}
// Sending new infos to the team
// Broadcast new infos
secureAdminBroadcast("teams", this.teams);
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
trajectory.writeSeePosition(dateNow, teamId, team.chasing);
// Update of locationSendDeadline
trajectory.writePosition(dateNow, team.id, location[0], location[1]);
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;
sendPositionTimeouts.set(team.id);
// Update of lastSentLocation
team.lastSentLocation = team.currentLocation;
// Update of enemyLocation
const teamChasing = this.getTeam(team.chasing);
if (teamChasing) team.enemyLocation = teamChasing.lastSentLocation;
// Sending new infos to the team
// Update enemy
enemyTeam.nObserved++;
// Broadcast new infos
secureAdminBroadcast("teams", this.teams);
sendUpdatedTeamInformations(team.id);
sendUpdatedTeamInformations(enemyTeam.id);
// Update of events of the game
trajectory.writeSeePosition(dateNow, team.id, enemyTeam.id);
return true;
},
/**
* Remove a team by its ID
* @param {Number} teamId The id of the team to remove
*/
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) {
tryCapture(teamId, captureCode) {
// Test of parameters
if (!this.hasTeam(teamId)) return false;
// Variables
const team = this.getTeam(teamId);
const enemyTeam = this.getTeam(team.chasing);
if (!enemyTeam || enemyTeam.captureCode != captureCode) {
return;
}
const dateNow = Date.now();
// Verify the capture
if (enemyTeam.captureCode != captureCode) return false;
// Make the capture
team.nCaptures++;
// Update of events of the game
trajectory.writeCapture(Date.now(), teamId, enemyTeam.id);
// Update of capture and chasing cycle
this.capture(enemyTeam.id);
// Sending new infos to the teams
enemyTeam.captured = true;
enemyTeam.finishDate = dateNow;
enemyTeam.chasing = null;
enemyTeam.chased = null;
sendPositionTimeouts.clear(enemyTeam.id);
outOfZoneTimeouts.clear(enemyTeam.id);
this.updateChasingChain();
this.checkEndGame();
// Broadcast new infos
secureAdminBroadcast("teams", this.teams);
sendUpdatedTeamInformations(team.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.
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 game from "./game.js";
import zoneManager from "./zone_manager.js";
@@ -24,9 +23,7 @@ export function teamBroadcast(teamId, event, data) {
* @param {String} data payload
*/
export function playersBroadcast(event, data) {
for (const team of game.teams) {
teamBroadcast(team.id, event, data);
}
game.teams.forEach(team => teamBroadcast(team.id, event, data));
}
/**
@@ -42,7 +39,7 @@ export function sendUpdatedTeamInformations(teamId) {
captureCode: team.captureCode,
// Chasing
captured: team.captured,
enemyName: game.getTeam(team.chasing)? game.getTeam(team.chasing).name : null,
enemyName: game.getTeam(team.chasing)?.name ?? null,
// Locations
lastSentLocation: team.lastSentLocation,
enemyLocation: team.enemyLocation,
@@ -57,10 +54,9 @@ export function sendUpdatedTeamInformations(teamId) {
distance: team.distance,
nCaptures: team.nCaptures,
nSentLocation: team.nSentLocation,
startDate: game.startDate,
stateDate: game.stateDate,
finishDate: team.finishDate,
})
secureAdminBroadcast("teams", game.teams);
});
}
export function initTeamSocket() {
@@ -68,42 +64,36 @@ export function initTeamSocket() {
console.log("Connection of a player");
let teamId = null;
const logoutPlayer = () => {
const login = (loginTeamId) => {
logout();
if (!game.addPlayer(loginTeamId, socket.id)) return false;
teamId = loginTeamId;
return true;
}
const logout = () => {
if (!teamId) return;
const team = game.getTeam(teamId);
if (team.sockets.indexOf(socket.id) == 0) {
team.battery = null;
team.phoneModel = null;
team.phoneName = null;
}
// Delete the player from the team
team.sockets = team.sockets.filter((sid) => sid != socket.id);
secureAdminBroadcast("teams", game.teams);
socket.emit("logout");
game.removePlayer(teamId, socket.id);
teamId = null;
}
socket.on("disconnect", () => {
console.log("Disconnection of a player");
logoutPlayer();
logout();
});
socket.on("logout", () => {
logoutPlayer();
logout();
});
socket.on("login", (loginTeamId, callback) => {
logoutPlayer();
const team = game.getTeam(loginTeamId);
if (!team) {
if (!login(loginTeamId)) {
callback({ isLoggedIn: false, message: "Login denied" });
return;
}
teamId = loginTeamId;
team.sockets.push(socket.id);
sendUpdatedTeamInformations(loginTeamId);
socket.emit("game_state", game.state);
socket.emit("game_settings", game.settings);
socket.emit("game_settings", game.messages);
socket.emit("zone", {
type: zoneManager.settings.type,
begin: zoneManager.getCurrentZone(),
@@ -115,13 +105,9 @@ export function initTeamSocket() {
socket.on("update_position", (position) => {
if (!teamId) return;
const team = game.getTeam(teamId);
// 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) {
if (game.isPlayerCapitain(teamId, socket.id)) {
game.updateLocation(teamId, position);
team.lastCurrentLocationDate = Date.now();
}
secureAdminBroadcast("teams", game.teams);
});
socket.on("send_position", () => {
@@ -131,27 +117,25 @@ export function initTeamSocket() {
socket.on("capture", (captureCode, callback) => {
if (!teamId) return;
game.requestCapture(teamId, captureCode);
callback({ hasCaptured : true, message: "Capture successful" });
if (game.tryCapture(teamId, captureCode)) {
callback({ hasCaptured : true, message: "Capture successful" });
} else {
callback({ hasCaptured : false, message: "Capture denied" });
}
});
socket.on("device_info", (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;
if (game.isPlayerCapitain(teamId, socket.id)) {
game.updateTeam(teamId, {phoneModel: infos.model, phoneName: infos.name});
}
});
socket.on("battery_update", (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;
if (game.isPlayerCapitain(teamId, socket.id)) {
game.updateTeam(teamId, {battery: batteryLevel});
}
});
});
}
}

View File

@@ -89,7 +89,7 @@ export default function TeamSidePanel({ selectedTeamId, onClose }) {
/>
</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')} />
</div>
<div>

View File

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

View File

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

View File

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

View File

@@ -2,11 +2,7 @@ import { ReorderList } from '@/components/list';
import useAdmin from '@/hook/useAdmin';
function TeamManagerItem({ team }) {
const { updateTeam, removeTeam } = useAdmin();
function handleRemove() {
removeTeam(team.id);
}
const { captureTeam, removeTeam } = useAdmin();
return (
<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>
<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>
<img src={`/icons/heart/${team.captured ? "grey" : "pink"}.png`} className="w-8 h-8" onClick={() => updateTeam(team.id, { captured: !team.captured })} />
<img src="/icons/trash.png" className="w-8 h-8" onClick={handleRemove} />
<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={() => removeTeam(team.id)} />
</div>
</div>
</div>
@@ -26,7 +22,7 @@ export default function TeamManager() {
const { teams, reorderTeams } = useAdmin();
return (
<ReorderList droppableId="team-manager" array={teams} setArray={reorderTeams}>
<ReorderList droppableId="team-manager" array={teams} setArray={(teams) => reorderTeams(teams.map(team => team.id))}>
{(team) => (
<TeamManagerItem team={team}/>
)}

View File

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

View File

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

View File

@@ -17,10 +17,10 @@ export function List({array, children}) {
}
export function ReorderList({droppableId, array, setArray, children}) {
const [arrayLocal, setArrayLocal] = useState(array);
const [localArray, setLocalArray] = useState(array);
useEffect(() => {
setArrayLocal(array);
setLocalArray(array);
}, [array])
function reorder(list, startIndex, endIndex) {
@@ -34,7 +34,7 @@ export function ReorderList({droppableId, array, setArray, children}) {
if (!result.destination) return;
if (result.destination.index === result.source.index) return;
const newArray = reorder(array, result.source.index, result.destination.index);
setArrayLocal(newArray);
setLocalArray(newArray);
setArray(newArray);
}
@@ -44,7 +44,7 @@ export function ReorderList({droppableId, array, setArray, children}) {
{provided => (
<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">
{arrayLocal.map((elem, i) => (
{localArray.map((elem, i) => (
<li className='w-full' key={elem.id}>
<Draggable draggableId={elem.id.toString()} index={i}>
{provided => (

View File

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