diff --git a/doc/TODO.md b/doc/TODO.md index abfc3c2..afb5ca7 100644 --- a/doc/TODO.md +++ b/doc/TODO.md @@ -23,6 +23,8 @@ - [ ] Permettre le changement du style de la carte (schéma, satellite, relief etc) - [ ] Ajouter imprécision de la position au besoin (comme sur google maps) - [ ] Synchroniser les horloges sur l'interface +- [ ] Avoir un récap des évènement de la partie +- [ ] Publier sur le playstore ### Admin (Pageweb) @@ -30,12 +32,27 @@ - [x] Clarifier qui chasse qui sur l'interface. - [x] Ajouter timer du rétrécissement des zones. - [x] Pouvoir changer les paramètres du jeu pendant une partie. -- [ ] Implémenter les wireframes +- [x] Implémenter les wireframes +- [ ] Ajouter une région par défaut si pas de position +- [ ] Pouvoir faire pause dans la partie +- [ ] Voir les traces et évènements des teams +- [ ] Voir l'incertitude de position des teams +- [ ] Focus une team cliquée +- [ ] 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 +- [ ] 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 +- [ ] Penser l'affichage en fin de traque ### Améliorations du jeu de la traque - [x] Supprimer la pénalité de non envoi de position : envoyer la position automatiquement à la fin du timer. -- [ ] Supprimer la pénalité d'hors zone : révéler la position de la team hors zone au bout d'un certain temps. +- [x] Supprimer la pénalité d'hors zone : révéler la position de la team hors zone au bout d'un certain temps. - [x] Changer le système de zone de jeu pour qu'il soit fait d'un pavage de zones qui se ferment successivement. ### Autres idées diff --git a/traque-app/app/display.js b/traque-app/app/display.js index 187745e..d3822d3 100644 --- a/traque-app/app/display.js +++ b/traque-app/app/display.js @@ -14,22 +14,27 @@ import LinearGradient from 'react-native-linear-gradient'; import { useSocket } from '../context/socketContext'; import { useTeamContext } from '../context/teamContext'; import { useTeamConnexion } from '../context/teamConnexionContext'; -import { useDeadline, useTimeDifference } from '../hook/useTimeDifference'; +import { useTimeDifference } from '../hook/useTimeDifference'; import { GameState } from '../util/gameState'; import useGame from '../hook/useGame'; const backgroundColor = '#f5f5f5'; const initialRegion = {latitude: 48.864, longitude: 2.342, latitudeDelta: 0, longitudeDelta: 50} // France centrée sur Paris +const zoneTypes = { + circle: "circle", + polygon: "polygon" +} + export default function Display() { const arrowUp = require('../assets/images/arrow.png'); const [collapsibleState, setCollapsibleState] = useState(true); const [bottomContainerHeight, setBottomContainerHeight] = useState(0); const router = useRouter(); const {SERVER_URL} = useSocket(); - const {gameSettings, zoneExtremities, nextZoneDate, isShrinking, location, startLocationTracking, stopLocationTracking, gameState, zone} = useTeamContext(); + const {gameSettings, zoneType, zoneExtremities, nextZoneDate, isShrinking, location, startLocationTracking, stopLocationTracking, gameState} = useTeamContext(); const {loggedIn, logout, loading} = useTeamConnexion(); - const {sendCurrentPosition, capture, enemyLocation, enemyName, startingArea, captureCode, name, ready, captured, lastSentLocation, locationSendDeadline, penalties, teamId, outOfZone, outOfZoneDeadline, distance, startDate, finishDate, nCaptures, nSentLocation} = useGame(); + const {sendCurrentPosition, capture, enemyLocation, enemyName, startingArea, captureCode, name, ready, captured, lastSentLocation, locationSendDeadline, teamId, outOfZone, outOfZoneDeadline, distance, startDate, finishDate, nCaptures, nSentLocation} = useGame(); const [enemyCaptureCode, setEnemyCaptureCode] = useState(""); const [timeLeftSendLocation] = useTimeDifference(locationSendDeadline, 1000); const [timeLeftNextZone] = useTimeDifference(nextZoneDate, 1000); @@ -234,12 +239,32 @@ export default function Display() { ); } + const Zones = () => { + switch (zoneType) { + case zoneTypes.circle: + return ( + + { zoneExtremities.begin && } + { zoneExtremities.end && } + + ); + case zoneTypes.polygon: + return ( + + { zoneExtremities.begin && } + { zoneExtremities.end && } + + ); + default: + return null; + } + } + const Map = () => { return ( setCenterMap(false)}> { gameState == GameState.PLACEMENT && startingArea && circle("0, 0, 255", startingArea)} - { gameState == GameState.PLAYING && zoneExtremities && } - { gameState == GameState.PLAYING && zoneExtremities && } + { gameState == GameState.PLAYING && zoneExtremities && } { location && @@ -345,9 +370,6 @@ export default function Display() { { Logout() } - { penalties > 0 && gameState == GameState.PLAYING && - Pénalités : {penalties} - } { false && Settings() } diff --git a/traque-app/context/teamConnexionContext.jsx b/traque-app/context/teamConnexionContext.jsx index 5364a41..2a76591 100644 --- a/traque-app/context/teamConnexionContext.jsx +++ b/traque-app/context/teamConnexionContext.jsx @@ -3,6 +3,7 @@ import { useSocket } from "./socketContext"; import { useSocketAuth } from "../hook/useSocketAuth"; const teamConnexionContext = createContext(); + const TeamConnexionProvider = ({ children }) => { const { teamSocket } = useSocket(); const { login, password: teamId, loggedIn, loading, logout } = useSocketAuth(teamSocket, "team_password"); diff --git a/traque-app/context/teamContext.jsx b/traque-app/context/teamContext.jsx index 00e907e..d2709f9 100644 --- a/traque-app/context/teamContext.jsx +++ b/traque-app/context/teamContext.jsx @@ -4,13 +4,21 @@ import { createContext, useContext, useMemo, useRef, useState } from "react"; import { useSocket } from "./socketContext"; import { GameState } from "../util/gameState"; import useSendDeviceInfo from "../hook/useSendDeviceInfo"; +import { useTeamConnexion } from "./teamConnexionContext"; +const teamContext = createContext(); + +const zoneTypes = { + circle: "circle", + polygon: "polygon" +} -const teamContext = createContext() function TeamProvider({children}) { + const { logout } = useTeamConnexion(); const [teamInfos, setTeamInfos] = useState({}); const [gameState, setGameState] = useState(GameState.SETUP); const [gameSettings, setGameSettings] = useState(null); + const [zoneType, setZoneType] = useState(null); const [zoneExtremities, setZoneExtremities] = useState(null); const [nextZoneDate, setNextZoneDate] = useState(null); const [location, getLocationAuthorization, startLocationTracking, stopLocationTracking] = useLocation(5000, 10); @@ -21,21 +29,38 @@ function TeamProvider({children}) { teamInfosRef.current = teamInfos; - function setCurrentZone(data) { - const newBegin = {points : data.begin.points.map( p => ({latitude: p.lat,longitude: p.lng}) ), duration: data.begin.duration}; - const newEnd = {points : data.end.points.map( p => ({latitude: p.lat,longitude: p.lng}) ), duration: data.end.duration}; - setZoneExtremities({begin: newBegin, end: newEnd}); + function setZone(data) { + setZoneType(data.type); + switch (data.type) { + case zoneTypes.circle: + setZoneExtremities({ + begin: {...data.begin, ...{center : {latitude: data.begin.center.lat, longitude: data.begin.center.lng} }}, + end: {...data.end, ...{center : {latitude: data.end.center.lat, longitude: data.end.center.lng} }} + }); + break; + case zoneTypes.polygon: + setZoneExtremities({ + begin: {...data.begin, ...{points : data.begin.points.map( p => ({latitude: p.lat,longitude: p.lng}) )}}, + end: {...data.end, ...{points : data.end.points.map( p => ({latitude: p.lat,longitude: p.lng}) )}} + }); + break; + default: + setZoneExtremities({begin: data.begin, end: data.end}); + break; + } setNextZoneDate(data.endDate); } useSocketListener(teamSocket, "update_team", (newTeamInfos) => {setTeamInfos({...teamInfosRef.current, ...newTeamInfos})}); useSocketListener(teamSocket, "game_state", setGameState); - useSocketListener(teamSocket, "current_zone", setCurrentZone); + useSocketListener(teamSocket, "zone", setZone); useSocketListener(teamSocket, "game_settings", setGameSettings); + useSocketListener(teamSocket, "logout", logout); const value = useMemo(() => ( - {teamInfos, gameState, zoneExtremities, nextZoneDate, gameSettings, location, getLocationAuthorization, startLocationTracking, stopLocationTracking} - ), [teamInfos, gameState, zoneExtremities, nextZoneDate, gameSettings, location]); + {teamInfos, gameState, zoneType, zoneExtremities, nextZoneDate, gameSettings, location, getLocationAuthorization, startLocationTracking, stopLocationTracking} + ), [teamInfos, gameState, zoneType, zoneExtremities, nextZoneDate, gameSettings, location]); + return ( {children} @@ -47,4 +72,4 @@ function useTeamContext() { return useContext(teamContext); } -export { TeamProvider, useTeamContext }; \ No newline at end of file +export { TeamProvider, useTeamContext }; diff --git a/traque-app/hook/useSendDeviceInfo.jsx b/traque-app/hook/useSendDeviceInfo.jsx index f8b6c49..72e9ef0 100644 --- a/traque-app/hook/useSendDeviceInfo.jsx +++ b/traque-app/hook/useSendDeviceInfo.jsx @@ -15,12 +15,12 @@ export default function useSendDeviceInfo() { const brand = DeviceInfo.getBrand(); const model = DeviceInfo.getModel(); const name = await DeviceInfo.getDeviceName(); - teamSocket.emit('deviceInfo', {model: brand + " " + model, name: name}); + teamSocket.emit('device_info', {model: brand + " " + model, name: name}); }; const sendBattery = async () => { const level = await DeviceInfo.getBatteryLevel(); - teamSocket.emit('batteryUpdate', Math.round(level * 100)); + teamSocket.emit('battery_update', Math.round(level * 100)); }; sendInfo(); diff --git a/traque-back/admin_socket.js b/traque-back/admin_socket.js index eb7dc44..11b5991 100644 --- a/traque-back/admin_socket.js +++ b/traque-back/admin_socket.js @@ -6,12 +6,12 @@ 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 penaltyController from "./penalty_controller.js"; import { playersBroadcast, sendUpdatedTeamInformations } from "./team_socket.js"; import { createHash } from "crypto"; import { config } from "dotenv"; config(); + const ADMIN_PASSWORD_HASH = process.env.ADMIN_PASSWORD_HASH; /** @@ -27,163 +27,81 @@ export function secureAdminBroadcast(event, data) { // Array of logged in sockets let loggedInSockets = []; + export function initAdminSocketHandler() { - // Admin namespace io.of("admin").on("connection", (socket) => { - // Flag to check if the user is logged in, defined for each socket console.log("Connection of an admin"); let loggedIn = false; socket.on("disconnect", () => { console.log("Disconnection of an admin"); - // Remove the socket from the logged in sockets array loggedInSockets = loggedInSockets.filter(s => s !== socket.id); + loggedIn = false; }); socket.on("logout", () => { loggedInSockets = loggedInSockets.filter(s => s !== socket.id); + loggedIn = false; }) - // User is attempting to log in socket.on("login", (password) => { const hash = createHash('sha256').update(password).digest('hex'); if (hash === ADMIN_PASSWORD_HASH && !loggedIn) { - // Attempt successful - socket.emit("login_response", true); loggedInSockets.push(socket.id); loggedIn = true; - // Send the current state - socket.emit("game_state", {state: game.state, startDate: game.startDate}) - // Other settings that need initialization - socket.emit("penalty_settings", penaltyController.settings) - socket.emit("game_settings", game.settings) - socket.emit("zone_settings", zoneManager.settings) + 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, - }) - } else { - // Attempt unsuccessful - socket.emit("login_response", false); + }); + socket.emit("settings", game.getSettings()); } }); - socket.on("set_game_settings", (settings) => { - if (!loggedIn) { - socket.emit("error", "Not logged in"); - return; - } - if (!game.changeSettings(settings)) { - socket.emit("error", "Invalid settings"); - socket.emit("game_settings", penaltyController.settings) - } else { - secureAdminBroadcast("game_settings", game.settings); - playersBroadcast("game_settings", game.settings); - } + socket.on("update_settings", (settings) => { + if (!loggedIn) return; + game.changeSettings(settings); + secureAdminBroadcast("settings", game.getSettings()); }) - socket.on("set_zone_settings", (settings) => { - if (!loggedIn) { - socket.emit("error", "Not logged in"); - return; - } - if (!zoneManager.changeSettings(settings)) { - socket.emit("error", "Error changing zone"); - socket.emit("zone_settings", settings) - } else { - secureAdminBroadcast("zone_settings", settings) - } - - }) - - socket.on("set_penalty_settings", (settings) => { - if (!loggedIn) { - socket.emit("error", "Not logged in"); - return; - } - if (!penaltyController.updateSettings(settings)) { - socket.emit("error", "Invalid settings"); - socket.emit("penalty_settings", penaltyController.settings) - } else { - secureAdminBroadcast("penalty_settings", penaltyController.settings) - } - - }) - - // User is attempting to add a new team socket.on("add_team", (teamName) => { - if (!loggedIn) { - socket.emit("error", "Not logged in"); - return; - } - if (game.addTeam(teamName)) { - secureAdminBroadcast("teams", game.teams); - } else { - socket.emit("error", "Error adding team"); - } + if (!loggedIn) return; + game.addTeam(teamName); + secureAdminBroadcast("teams", game.teams); }); - // User is attempting to remove a team socket.on("remove_team", (teamId) => { - if (!loggedIn) { - socket.emit("error", "Not logged in"); - return; - } - if (game.removeTeam(teamId)) { - secureAdminBroadcast("teams", game.teams); - } else { - socket.emit("error", "Error removing team"); - } + if (!loggedIn) return; + game.removeTeam(teamId); + secureAdminBroadcast("teams", game.teams); }); - // User is attempting to change the game state socket.on("change_state", (state) => { - if (!loggedIn) { - socket.emit("error", "Not logged in"); - return; - } - if (!game.setState(state)) { - socket.emit("error", "Error setting state"); - } + if (!loggedIn) return; + 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) => { - if (!loggedIn) { - socket.emit("error", "Not logged in"); - return; - } - if (game.reorderTeams(newOrder)) { - secureAdminBroadcast("teams", game.teams); - game.teams.forEach(t => sendUpdatedTeamInformations(t.id)) - } else { - socket.emit("error", "Error reordering teams"); - } + if (!loggedIn) return; + game.reorderTeams(newOrder); + secureAdminBroadcast("teams", game.teams); + game.teams.forEach(t => sendUpdatedTeamInformations(t.id)); }); socket.on("update_team", (teamId, newTeam) => { - if (!loggedIn) { - socket.emit("error", "Not logged in"); - return; - } - if (game.updateTeam(teamId, newTeam)) { - secureAdminBroadcast("teams", game.teams); - sendUpdatedTeamInformations(teamId) - sendUpdatedTeamInformations(game.getTeam(teamId).chased) - } + if (!loggedIn) return; + game.updateTeam(teamId, newTeam); + secureAdminBroadcast("teams", game.teams); + sendUpdatedTeamInformations(teamId); + sendUpdatedTeamInformations(game.getTeam(teamId).chased); }) - - // Request an update of the team list - // We only reply to the sender to prevent spam - socket.on("get_teams", () => { - if (!loggedIn) { - socket.emit("error", "Not logged in"); - return; - } - socket.emit("teams", game.teams); - }); }); } diff --git a/traque-back/game.js b/traque-back/game.js index e61e7ea..fe6aeb5 100644 --- a/traque-back/game.js +++ b/traque-back/game.js @@ -2,9 +2,8 @@ This module manages the main game state, the teams, the settings and the game logic */ import { secureAdminBroadcast } from "./admin_socket.js"; -import { playersBroadcast, sendUpdatedTeamInformations } from "./team_socket.js"; -import timeoutHandler from "./timeoutHandler.js"; -import penaltyController from "./penalty_controller.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"; @@ -52,34 +51,43 @@ 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 methods teams: [], - //Current state of the game + // Current state of the game state: GameState.SETUP, - // Date since gameState switched to PLAYING - startDate: null, - //Settings of the game - settings: { - loserEndGameMessage: "", - winnerEndGameMessage: "", - capturedMessage: "", - waitingMessage: "" + // Date since the state changed + stateDate: Date.now(), + // Messages + messages: { + waiting: "", + captured: "", + winner: "", + loser: "", + }, + + getSettings() { + return { + messages: this.messages, + zone: zoneManager.settings, + sendPositionDelay: sendPositionTimeouts.delay, + outOfZoneDelay: outOfZoneTimeouts.delay + }; }, /** * Update the game settings * @param {Object} newSettings settings to be updated, can be partial - * @returns true if the settings are applied */ changeSettings(newSettings) { - this.settings = { ...this.settings, ...newSettings }; - return true; + 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); }, /** * Change the state of the game to newState and start the necessary processes * @param {String} newState - * @returns true if the state has been changed */ setState(newState) { // Checks is the newState is a Gamestate @@ -89,8 +97,8 @@ export default { case GameState.SETUP: trajectory.stop(); zoneManager.stop(); - penaltyController.stop(); - timeoutHandler.endAllSendPositionTimeout(); + sendPositionTimeouts.clearAll(); + outOfZoneTimeouts.clearAll(); for (let team of this.teams) { // Chasing team.captured = false; @@ -103,7 +111,6 @@ export default { // Placement team.ready = false; // Zone - team.penalties = 0; team.outOfZone = false; team.outOfZoneDeadline = null; // Stats @@ -113,34 +120,33 @@ export default { team.nObserved = 0; team.finishDate = null; } - this.startDate = null; + this.stateDate = Date.now(); this.updateTeamChasing(); break; case GameState.PLACEMENT: if (this.teams.length < 3) { - secureAdminBroadcast("game_state", {state: this.state, startDate: this.startDate}); + secureAdminBroadcast("game_state", {state: this.state, stateDate: this.stateDate}); return false; } trajectory.stop(); zoneManager.stop(); - penaltyController.stop(); - timeoutHandler.endAllSendPositionTimeout(); - this.startDate = null; + sendPositionTimeouts.clearAll(); + outOfZoneTimeouts.clearAll(); + this.stateDate = Date.now(); break; case GameState.PLAYING: if (this.teams.length < 3) { - secureAdminBroadcast("game_state", {state: this.state, startDate: this.startDate}); + secureAdminBroadcast("game_state", {state: this.state, stateDate: this.stateDate}); return false; } trajectory.start(); zoneManager.start(); - penaltyController.start(); this.initLastSentLocations(); - this.startDate = Date.now(); + this.stateDate = Date.now(); break; case GameState.FINISHED: if (this.state != GameState.PLAYING) { - secureAdminBroadcast("game_state", {state: this.state, startDate: this.startDate}); + secureAdminBroadcast("game_state", {state: this.state, stateDate: this.stateDate}); return false; } for (const team of this.teams) { @@ -148,16 +154,15 @@ export default { } trajectory.stop(); zoneManager.stop(); - penaltyController.stop(); - timeoutHandler.endAllSendPositionTimeout(); + sendPositionTimeouts.clearAll(); + outOfZoneTimeouts.clearAll(); break; } // Update the state this.state = newState; - secureAdminBroadcast("game_state", {state: newState, startDate: this.startDate}); + secureAdminBroadcast("game_state", {state: newState, stateDate: this.stateDate}); playersBroadcast("game_state", newState); secureAdminBroadcast("teams", this.teams); - return true; }, /** @@ -183,7 +188,6 @@ export default { /** * Add a new team to the game * @param {String} teamName the name of the team - * @returns true if the team has been added */ addTeam(teamName) { this.teams.push({ @@ -206,7 +210,6 @@ export default { startingArea: null, ready: false, // Zone - penalties: 0, outOfZone: false, outOfZoneDeadline: null, // Stats @@ -221,35 +224,18 @@ export default { battery: null, }); this.updateTeamChasing(); - return true; - }, - - /** - * Count the number of teams that are not captured - * @returns the number of teams that are not captured - */ - playingTeamCount() { - let res = 0; - this.teams.forEach((t) => { - if (!t.captured) { - res++; - } - }) - return res; }, /** * 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 - * @returns true if successful */ updateTeamChasing() { - if (this.playingTeamCount() <= 2) { - if (this.state == GameState.PLAYING) { - this.setState(GameState.FINISHED); - } - return false; + 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 @@ -267,17 +253,15 @@ export default { this.getTeam(firstTeam).chased = previousTeam; this.getTeam(previousTeam).chasing = firstTeam; 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 - * @returns */ reorderTeams(newOrder) { this.teams = newOrder; - return this.updateTeamChasing(); + this.updateTeamChasing(); }, /** @@ -293,7 +277,6 @@ export default { * 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 - * @returns true if the team has been updated */ updateTeam(teamId, newTeam) { this.teams = this.teams.map((t) => { @@ -304,20 +287,17 @@ export default { } }) this.updateTeamChasing(); - penaltyController.checkPenalties(); - return true; }, /** * * @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 - * @returns true if the location has been updated */ updateLocation(teamId, location) { const team = this.getTeam(teamId); if (!team || !location) { - return false; + return; } 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 @@ -328,9 +308,19 @@ export default { if (this.state == GameState.PLACEMENT && team.startingArea && team.startingArea && location) { team.ready = isInCircle({ lat: location[0], lng: location[1] }, team.startingArea.center, team.startingArea.radius); } + // Verify 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; + outOfZoneTimeouts.set(teamId); + } else if (!teamCurrentlyOutOfZone && team.outOfZone) { + team.outOfZone = false; + team.outOfZoneDeadline = null; + outOfZoneTimeouts.clear(teamId); + } // Sending new infos to the team sendUpdatedTeamInformations(team.id); - return true; }, /** @@ -340,8 +330,8 @@ export default { // Update of lastSentLocation for (const team of this.teams) { team.lastSentLocation = team.currentLocation; - team.locationSendDeadline = Date.now() + penaltyController.settings.allowedTimeBetweenPositionUpdate * 60 * 1000; - timeoutHandler.setSendPositionTimeout(team.id, team.locationSendDeadline); + 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 @@ -354,22 +344,21 @@ export default { /** * 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 - * @returns true if the location has been sent */ sendLocation(teamId) { const team = this.getTeam(teamId); - const enemyTeam = this.getTeam(team.chasing); if (!team || !team.currentLocation) { - return false; + 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 - team.locationSendDeadline = dateNow + penaltyController.settings.allowedTimeBetweenPositionUpdate * 60 * 1000; - timeoutHandler.setSendPositionTimeout(team.id, team.locationSendDeadline); + team.locationSendDeadline = dateNow + sendPositionTimeouts.duration * 60 * 1000; + sendPositionTimeouts.set(team.id); // Update of lastSentLocation team.lastSentLocation = team.currentLocation; // Update of enemyLocation @@ -378,22 +367,21 @@ export default { // Sending new infos to the team sendUpdatedTeamInformations(team.id); sendUpdatedTeamInformations(enemyTeam.id); - return true; }, /** * Remove a team by its ID * @param {Number} teamId The id of the team to remove - * @returns true if the team has been deleted */ removeTeam(teamId) { if (!this.getTeam(teamId)) { - return false; + return; } + teamBroadcast("logout"); this.teams = this.teams.filter(t => t.id !== teamId); this.updateTeamChasing(); - timeoutHandler.endSendPositionTimeout(teamId); - return true; + sendPositionTimeouts.clear(teamId); + outOfZoneTimeouts.clear(teamId); }, /** @@ -402,13 +390,12 @@ export default { * 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 - * @returns {Boolean} if the capture has been successfull or not */ requestCapture(teamId, captureCode) { const team = this.getTeam(teamId); const enemyTeam = this.getTeam(team.chasing); if (!enemyTeam || enemyTeam.captureCode != captureCode) { - return false; + return; } team.nCaptures++; // Update of events of the game @@ -418,7 +405,6 @@ export default { // Sending new infos to the teams sendUpdatedTeamInformations(team.id); sendUpdatedTeamInformations(enemyTeam.id); - return true; }, /** @@ -429,7 +415,12 @@ export default { const team = this.getTeam(teamId); team.captured = true; team.finishDate = Date.now(); - timeoutHandler.endSendPositionTimeout(teamId); + sendPositionTimeouts.clear(teamId); + outOfZoneTimeouts.clear(teamId); this.updateTeamChasing(); }, -} \ No newline at end of file + + handicapTeam(teamId) { + // TODO + } +} diff --git a/traque-back/penalty_controller.js b/traque-back/penalty_controller.js deleted file mode 100644 index eb3152d..0000000 --- a/traque-back/penalty_controller.js +++ /dev/null @@ -1,165 +0,0 @@ -/* -This module manages the verification of the game rules and the penalties. -*/ -import { sendUpdatedTeamInformations, teamBroadcast } from "./team_socket.js"; -import { secureAdminBroadcast } from "./admin_socket.js"; -import game, { GameState } from "./game.js"; -import zoneManager from "./zone_manager.js"; - -export default { - // Object mapping team id to the date they left the zone as a UNIX millisecond timestamp - outOfBoundsSince: {}, - // Id of the interval checking the rules - checkIntervalId: null, - settings: { - //Time in minutes before a team is penalized for not updating their position - allowedTimeOutOfZone: 10, - //Time in minutes before a team is penalized for not updating their position - allowedTimeBetweenPositionUpdate: 10, - //Number of penalties needed to be eliminated - maxPenalties: 3 - }, - - /** - * Start the penalty controller, watch the team positions and apply penalties if necessary - */ - start() { - this.outOfBoundsSince = {}; - if (this.checkIntervalId) { - clearInterval(this.checkIntervalId) - } - //Watch periodically if all teams need are following the rules - this.checkIntervalId = setInterval(() => { - if (game.state == GameState.PLAYING) { - //this.watchPositionUpdate(); - this.watchZone(); - } - }, 100); - }, - - /** - * Stop the penalty controller - */ - stop() { - this.outOfBoundsSince = {}; - if (this.checkIntervalId) { - clearInterval(this.checkIntervalId) - this.checkIntervalId = null; - } - }, - - /** - * Update the penalty controller settings - * @param {Object} newSettings the object containing the settings to be udpated, can be partial - * @returns true if the settings were updated, false otherwise - */ - updateSettings(newSettings) { - //Sanitize input - if (newSettings.maxPenalties && (isNaN(parseInt(newSettings.maxPenalties)) || newSettings.maxPenalties < 0)) { return false } - if (newSettings.allowedTimeBetweenPositionUpdate && (isNaN(parseFloat(newSettings.allowedTimeBetweenPositionUpdate)) || newSettings.allowedTimeBetweenPositionUpdate < 0)) { return false } - if (newSettings.allowedTimeOutOfZone && (isNaN(parseFloat(newSettings.allowedTimeOutOfZone)) || newSettings.allowedTimeOutOfZone < 0)) { return false } - - this.settings = { ...this.settings, ...newSettings }; - return true; - }, - - /** - * Increment the penalty score of a team, send a message to the team and eliminated if necessary - * @param {Number} teamId The team that will recieve a penalty - */ - addPenalty(teamId) { - let team = game.getTeam(teamId); - if (!team) { - return; - } - if (team.captured) { - return; - } - team.penalties++; - if (team.penalties >= this.settings.maxPenalties) { - game.capture(team.id); - sendUpdatedTeamInformations(teamId); - sendUpdatedTeamInformations(team.chased); - teamBroadcast(teamId, "warning", "You have been eliminated (reason: too many penalties)") - teamBroadcast(team.chased, "success", "The team you were chasing has been eliminated") - } else { - teamBroadcast(teamId, "warning", `You recieved a penalty (${team.penalties}/${this.settings.maxPenalties})`) - sendUpdatedTeamInformations(teamId); - } - secureAdminBroadcast("teams", game.teams) - }, - - /** - * Check if each team has too many penalties and eliminate them if necessary - * Also send a socket message to the team and the chased team - */ - checkPenalties() { - for (let team of game.teams) { - if (team.penalties >= this.settings.maxPenalties) { - game.capture(team.id); - sendUpdatedTeamInformations(team.id); - sendUpdatedTeamInformations(team.chased); - teamBroadcast(team.id, "warning", "You have been eliminated (reason: too many penalties)") - teamBroadcast(team.chased, "success", "The team you were chasing has been eliminated") - } - } - }, - - /** - * Watch the position of each team and apply penalties if necessary - * If a team is out of the zone, a warning will be sent to them - * A warning is also sent one minute before the penalty is applied - */ - watchZone() { - game.teams.forEach((team) => { - if (team.captured) { return } - //All the informations are not ready yet - if (team.currentLocation == null || !zoneManager.isRunning) { - return; - } - if (!zoneManager.isInZone({ lat: team.currentLocation[0], lng: team.currentLocation[1] })) { - //The team was not previously out of the zone - if (!this.outOfBoundsSince[team.id]) { - this.outOfBoundsSince[team.id] = Date.now(); - team.outOfZone = true; - team.outOfZoneDeadline = this.outOfBoundsSince[team.id] + this.settings.allowedTimeOutOfZone * 60 * 1000; - secureAdminBroadcast("teams", game.teams) - } else if (Date.now() - this.outOfBoundsSince[team.id] > this.settings.allowedTimeOutOfZone * 60 * 1000) { - this.addPenalty(team.id); - this.outOfBoundsSince[team.id] = Date.now(); - team.outOfZoneDeadline = this.outOfBoundsSince[team.id] + this.settings.allowedTimeOutOfZone * 60 * 1000; - secureAdminBroadcast("teams", game.teams) - } - } else { - if (this.outOfBoundsSince[team.id]) { - team.outOfZone = false; - delete this.outOfBoundsSince[team.id]; - secureAdminBroadcast("teams", game.teams) - } - } - }) - }, - - /** - * Watch the date of the last position update of each team and apply penalties if necessary - * Also send a message one minute before the penalty is applied - */ - watchPositionUpdate() { - game.teams.forEach((team) => { - //If the team has not sent their location for more than the allowed period, automatically send it and add a penalty - if (team.captured) { return } - if (team.locationSendDeadline == null) { - team.locationSendDeadline = Number(Date.now()) + this.settings.allowedTimeBetweenPositionUpdate * 60 * 1000; - return; - } - if (Date.now() > team.locationSendDeadline) { - this.addPenalty(team.id); - game.sendLocation(team.id); - sendUpdatedTeamInformations(team.id); - secureAdminBroadcast("teams", game.teams) - } else if (Math.abs(Date.now() - team.locationSendDeadline - 60 * 1000) < 100) { - teamBroadcast(team.id, "warning", `You have one minute left to udpate your location.`) - } - }) - }, -} \ No newline at end of file diff --git a/traque-back/team_socket.js b/traque-back/team_socket.js index 6427060..9f29ca0 100644 --- a/traque-back/team_socket.js +++ b/traque-back/team_socket.js @@ -15,9 +15,7 @@ import zoneManager from "./zone_manager.js"; * @param {*} data The payload */ export function teamBroadcast(teamId, event, data) { - for (const socketId of game.getTeam(teamId).sockets) { - io.of("player").to(socketId).emit(event, data); - } + game.getTeam(teamId).sockets.forEach(socketId => io.of("player").to(socketId).emit(event, data)); } /** @@ -52,7 +50,6 @@ export function sendUpdatedTeamInformations(teamId) { startingArea: team.startingArea, ready: team.ready, // Constraints - penalties: team.penalties, outOfZone: team.outOfZone, outOfZoneDeadline: team.outOfZoneDeadline, locationSendDeadline: team.locationSendDeadline, @@ -66,66 +63,60 @@ export function sendUpdatedTeamInformations(teamId) { secureAdminBroadcast("teams", game.teams); } -/** - * Remove a player from the list of logged in players - * @param {Number} id The id of the player to log out - */ -function logoutPlayer(id) { - for (const team of game.teams) { - if (team.sockets.indexOf(id) == 0) { - team.battery = null; - team.phoneModel = null; - team.phoneName = null; - } - team.sockets = team.sockets.filter((sid) => sid != id); - } - secureAdminBroadcast("teams", game.teams); -} - export function initTeamSocket() { io.of("player").on("connection", (socket) => { console.log("Connection of a player"); let teamId = null; + const logoutPlayer = () => { + 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"); + teamId = null; + } + socket.on("disconnect", () => { console.log("Disconnection of a player"); - logoutPlayer(socket.id); + logoutPlayer(); + }); + + socket.on("logout", () => { + logoutPlayer(); }); socket.on("login", (loginTeamId, callback) => { + logoutPlayer(); const team = game.getTeam(loginTeamId); if (!team) { callback({ isLoggedIn: false, message: "Login denied" }); return; } - logoutPlayer(socket.id); - team.sockets.push(socket.id); teamId = loginTeamId; + team.sockets.push(socket.id); sendUpdatedTeamInformations(loginTeamId); - socket.emit("login_response", true); socket.emit("game_state", game.state); socket.emit("game_settings", game.settings); socket.emit("zone", { + type: zoneManager.settings.type, begin: zoneManager.getCurrentZone(), end: zoneManager.getNextZone(), endDate: zoneManager.currentZoneEndDate, - }) + }); callback({ isLoggedIn : true, message: "Logged in"}); }); - socket.on("logout", () => { - logoutPlayer(socket.id); - teamId = null; - }) - socket.on("update_position", (position) => { - // Only the first player to connect to the team socket can update the current position - // This is done to prevent multiple clients from sending slightly different prosition back and forth - // Making the point jitter on the map - if (!teamId) { - return; - } + 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) { game.updateLocation(teamId, position); team.lastCurrentLocationDate = Date.now(); @@ -134,27 +125,18 @@ export function initTeamSocket() { }); socket.on("send_position", () => { - if (!teamId) { - return; - } + if (!teamId) return; game.sendLocation(teamId); }); socket.on("capture", (captureCode, callback) => { - if (!teamId) { - return; - } - if (!game.requestCapture(teamId, captureCode)) { - callback({ hasCaptured : false, message: "Capture failed" }); - return; - } + if (!teamId) return; + game.requestCapture(teamId, captureCode); callback({ hasCaptured : true, message: "Capture successful" }); }); - socket.on("deviceInfo", (infos) => { - if (!teamId) { - return; - } + 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) { @@ -163,10 +145,8 @@ export function initTeamSocket() { } }); - socket.on("batteryUpdate", (batteryLevel) => { - if (!teamId) { - return; - } + 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) { diff --git a/traque-back/timeoutHandler.js b/traque-back/timeoutHandler.js deleted file mode 100644 index ec58d6f..0000000 --- a/traque-back/timeoutHandler.js +++ /dev/null @@ -1,30 +0,0 @@ -import game from "./game.js"; - -export default { - teams: [], - - setSendPositionTimeout(teamID, deadline) { - const foundTeam = this.teams.find(t => t.teamID === teamID); - if (!foundTeam) { - this.teams.push({teamID: teamID, timeoutID: setTimeout(() => game.sendLocation(teamID), deadline - Date.now())}); - } else { - clearTimeout(foundTeam.timeoutID); - foundTeam.timeoutID = setTimeout(() => game.sendLocation(teamID), deadline - Date.now()); - } - }, - - endSendPositionTimeout(teamID) { - const foundTeam = this.teams.find(t => t.teamID === teamID); - if (foundTeam) { - clearTimeout(foundTeam.timeoutID); - this.teams = this.teams.filter(t => t.teamID !== teamID); - } - }, - - endAllSendPositionTimeout() { - for (const team of this.teams) { - clearTimeout(team.timeoutID); - } - this.teams = []; - } -} diff --git a/traque-back/timeout_handler.js b/traque-back/timeout_handler.js new file mode 100644 index 0000000..9c68059 --- /dev/null +++ b/traque-back/timeout_handler.js @@ -0,0 +1,80 @@ +import game from "./game.js"; + +class TimeoutManager { + constructor() { + this.timeouts = new Map(); + } + + set(key, callback, delay) { + const newCallback = () => { + this.timeouts.delete(key); + callback(); + } + + if (this.timeouts.has(key)) clearTimeout(this.timeouts.get(key)); + this.timeouts.set(key, setTimeout(newCallback, delay)); + } + + clear(key) { + if (this.timeouts.has(key)) { + clearTimeout(this.timeouts.get(key)); + this.timeouts.delete(key); + } + } + + clearAll() { + this.timeouts.forEach(timeout => clearTimeout(timeout)); + this.timeouts = new Map(); + } +} + +export const sendPositionTimeouts = { + timeoutManager: new TimeoutManager(), + delay: 10, // Minutes + + set(teamID) { + const callback = () => { + game.sendLocation(teamID); + this.set(teamID); + } + + this.timeoutManager.set(teamID, callback, this.delay * 60 * 1000); + }, + + clear(teamID) { + this.timeoutManager.clear(teamID); + }, + + clearAll() { + this.timeoutManager.clearAll(); + }, + + setDelay(delay) { + this.delay = delay; + } +} + +export const outOfZoneTimeouts = { + timeoutManager: new TimeoutManager(), + delay: 10, // Minutes + + set(teamID) { + const callback = () => { + game.handicapTeam(teamID); + } + + this.timeoutManager.set(teamID, callback, this.delay * 60 * 1000); + }, + + clear(teamID) { + this.timeoutManager.clear(teamID); + }, + + clearAll() { + this.timeoutManager.clearAll(); + }, + + setDelay(delay) { + this.delay = delay; + } +} diff --git a/traque-back/zone_manager.js b/traque-back/zone_manager.js index 55444d2..9dafe3d 100644 --- a/traque-back/zone_manager.js +++ b/traque-back/zone_manager.js @@ -233,11 +233,12 @@ export default { zoneBroadcast() { const zone = { + type: this.settings.type, begin: this.getCurrentZone(), end: this.getNextZone(), endDate:this.currentZone.endDate, }; - playersBroadcast("current_zone", zone); + playersBroadcast("zone", zone); secureAdminBroadcast("current_zone", zone); }, } diff --git a/traque-front/app/admin/components/liveMap.jsx b/traque-front/app/admin/components/liveMap.jsx index 321708d..4a6bff0 100644 --- a/traque-front/app/admin/components/liveMap.jsx +++ b/traque-front/app/admin/components/liveMap.jsx @@ -19,7 +19,7 @@ const zoneTypes = { } export default function LiveMap({mapStyle, showZones, showNames, showArrows}) { - const { zoneSettings, zoneExtremities, teams, nextZoneDate, getTeam, gameState } = useAdmin(); + const { zoneType, zoneExtremities, teams, nextZoneDate, getTeam, gameState } = useAdmin(); const [timeLeftNextZone, setTimeLeftNextZone] = useState(null); // Remaining time before sending position @@ -48,16 +48,16 @@ export default function LiveMap({mapStyle, showZones, showNames, showArrows}) { if (pos1 && pos2) { return ( - ) + ); } else { return null; } } function Zones() { - if (!(showZones && gameState == GameState.PLAYING && zoneSettings)) return null; + if (!(showZones && gameState == GameState.PLAYING && zoneType)) return null; - switch (zoneSettings.type) { + switch (zoneType) { case zoneTypes.circle: return (
@@ -83,10 +83,12 @@ export default function LiveMap({mapStyle, showZones, showNames, showArrows}) { {teams.map((team) => team.currentLocation && !team.captured && - - {showNames && {team.name}} - {showArrows && } - +
+ + {showNames && {team.name}} + + {showArrows && } +
)}
diff --git a/traque-front/app/admin/components/teamSidePanel.jsx b/traque-front/app/admin/components/teamSidePanel.jsx index cf56d88..97d94bc 100644 --- a/traque-front/app/admin/components/teamSidePanel.jsx +++ b/traque-front/app/admin/components/teamSidePanel.jsx @@ -50,7 +50,7 @@ function getStatus(team, gamestate) { } export default function TeamSidePanel({ selectedTeamId, onClose }) { - const { getTeam, getTeamName, startDate, gameState } = useAdmin(); + const { getTeam, startDate, gameState } = useAdmin(); const [imgSrc, setImgSrc] = useState(""); const team = getTeam(selectedTeamId); const NO_VALUE = "XX"; @@ -115,8 +115,8 @@ export default function TeamSidePanel({ selectedTeamId, onClose }) {
- - + +
diff --git a/traque-front/app/admin/parameters/components/circleZoneSelector.jsx b/traque-front/app/admin/parameters/components/circleZoneSelector.jsx index 5cb0f19..e42c7f3 100644 --- a/traque-front/app/admin/parameters/components/circleZoneSelector.jsx +++ b/traque-front/app/admin/parameters/components/circleZoneSelector.jsx @@ -6,6 +6,7 @@ import { CustomMapContainer, MapEventListener } from "@/components/map"; import { TextInput } from "@/components/input"; import useAdmin from "@/hook/useAdmin"; import useMapCircleDraw from "@/hook/useMapCircleDraw"; +import useLocalVariable from "@/hook/useLocalVariable"; const EditMode = { MIN: 0, @@ -43,38 +44,36 @@ function Drawings({ minZone, setMinZone, maxZone, setMaxZone, editMode }) { return (
- {minCenter && minRadius && } - {maxCenter && maxRadius && } + {minCenter && minRadius && } + {maxCenter && maxRadius && }
); } -export default function CircleZoneSelector({zoneSettings, updateZoneSettings, applyZoneSettings}) { - const {penaltySettings, changePenaltySettings} = useAdmin(); - const [allowedTimeOutOfZone, setAllowedTimeOutOfZone] = useState(""); +export default function CircleZoneSelector({zoneSettings, modifyZoneSettings, applyZoneSettings}) { + const {outOfZoneDelay, updateSettings} = useAdmin(); + const [localOutOfZoneDelay, setLocalOutOfZoneDelay, applyLocalOutOfZoneDelay] = useLocalVariable(outOfZoneDelay, (e) => updateSettings({outOfZoneDelay: e})); const [editMode, setEditMode] = useState(EditMode.MIN); useEffect(() => { setEditMode(editMode == EditMode.MIN ? EditMode.MAX : EditMode.MIN); - }, [zoneSettings.min, zoneSettings.max]) - - useEffect(() => { - if (penaltySettings) { - setAllowedTimeOutOfZone(penaltySettings.allowedTimeOutOfZone.toString()); - } - }, [penaltySettings]); + }, [zoneSettings.min, zoneSettings.max]); function handleSettingsSubmit() { - console.log(zoneSettings) applyZoneSettings(); - changePenaltySettings({allowedTimeOutOfZone: Number(allowedTimeOutOfZone)}); + applyLocalOutOfZoneDelay(); + } + + function customStringToInt(e) { + const res = parseInt(e, 10); + return isNaN(res) ? null : res; } return ( -
+
- updateZoneSettings("min", e)} maxZone={zoneSettings.max} setMaxZone={(e) => updateZoneSettings("max", e)} editMode={editMode} /> + modifyZoneSettings("min", e)} maxZone={zoneSettings.max} setMaxZone={(e) => modifyZoneSettings("max", e)} editMode={editMode} />
@@ -83,21 +82,21 @@ export default function CircleZoneSelector({zoneSettings, updateZoneSettings, ap {editMode == EditMode.MAX && setEditMode(EditMode.MIN)}>Click to edit last zone}
-

Number

+

Reduction number

- updateZoneSettings("reductionCount", e.target.value)} /> + modifyZoneSettings("reductionCount", customStringToInt(e.target.value))} />
-

Duration

+

Zone duration

- updateZoneSettings("duration", e.target.value)} /> + modifyZoneSettings("duration", customStringToInt(e.target.value))} />

Timeout

- setAllowedTimeOutOfZone(e.target.value)} /> + setLocalOutOfZoneDelay(customStringToInt(e.target.value))} />
diff --git a/traque-front/app/admin/parameters/components/messages.jsx b/traque-front/app/admin/parameters/components/messages.jsx index b827565..f0c350e 100644 --- a/traque-front/app/admin/parameters/components/messages.jsx +++ b/traque-front/app/admin/parameters/components/messages.jsx @@ -1,6 +1,6 @@ -import { useEffect, useState } from "react"; import { Section } from "@/components/section"; import useAdmin from "@/hook/useAdmin"; +import useLocalVariable from "@/hook/useLocalVariable"; function MessageInput({title, ...props}) { return ( @@ -12,33 +12,19 @@ function MessageInput({title, ...props}) { } export default function Messages() { - const {gameSettings, changeGameSettings} = useAdmin(); - const [capturedMessage, setCapturedMessage] = useState(""); - const [winnerEndMessage, setWinnerEndMessage] = useState(""); - const [loserEndMessage, setLoserEndMessage] = useState(""); - const [waitingMessage, setWaitingMessage] = useState(""); + const {messages, updateSettings} = useAdmin(); + const [localGameSettings, setLocalGameSettings, applyLocalGameSettings] = useLocalVariable(messages, (e) => updateSettings({messages: e})); - useEffect(() => { - if (gameSettings) { - setCapturedMessage(gameSettings.capturedMessage); - setWinnerEndMessage(gameSettings.winnerEndGameMessage); - setLoserEndMessage(gameSettings.loserEndGameMessage); - setWaitingMessage(gameSettings.waitingMessage); - } - }, [gameSettings]); - - function applySettings() { - changeGameSettings({capturedMessage: capturedMessage, winnerEndGameMessage: winnerEndMessage, loserEndGameMessage: loserEndMessage, waitingMessage: waitingMessage}); - } + function modifyLocalZoneSettings(key, value) { + setLocalGameSettings(prev => ({...prev, [key]: value})); + }; return ( -
-
- setWaitingMessage(e.target.value)} onBlur={applySettings}/> - setCapturedMessage(e.target.value)} onBlur={applySettings}/> - setWinnerEndMessage(e.target.value)} onBlur={applySettings}/> - setLoserEndMessage(e.target.value)} onBlur={applySettings}/> -
+
+ modifyLocalZoneSettings("waiting", e.target.value)} onBlur={applyLocalGameSettings}/> + modifyLocalZoneSettings("captured", e.target.value)} onBlur={applyLocalGameSettings}/> + modifyLocalZoneSettings("winner", e.target.value)} onBlur={applyLocalGameSettings}/> + modifyLocalZoneSettings("loser", e.target.value)} onBlur={applyLocalGameSettings}/>
); } diff --git a/traque-front/app/admin/parameters/components/polygonZoneSelector.jsx b/traque-front/app/admin/parameters/components/polygonZoneSelector.jsx index 337de98..fc782d6 100644 --- a/traque-front/app/admin/parameters/components/polygonZoneSelector.jsx +++ b/traque-front/app/admin/parameters/components/polygonZoneSelector.jsx @@ -7,6 +7,7 @@ import { CustomMapContainer, MapEventListener } from "@/components/map"; import { TextInput } from "@/components/input"; import useAdmin from "@/hook/useAdmin"; import useMapPolygonDraw from "@/hook/useMapPolygonDraw"; +import useLocalVariable from "@/hook/useLocalVariable"; function Drawings({ polygons, addPolygon, removePolygon }) { const { currentPolygon, highlightNodes, handleLeftClick, handleRightClick, handleMouseMove } = useMapPolygonDraw(polygons, addPolygon, removePolygon); @@ -94,51 +95,44 @@ function Drawings({ polygons, addPolygon, removePolygon }) { ); } -export default function PolygonZoneSelector({zoneSettings, updateZoneSettings, applyZoneSettings}) { +export default function PolygonZoneSelector({zoneSettings, modifyZoneSettings, applyZoneSettings}) { const defaultDuration = 10; const [polygons, setPolygons] = useState([]); - const {penaltySettings, changePenaltySettings} = useAdmin(); - const [allowedTimeOutOfZone, setAllowedTimeOutOfZone] = useState(""); + const {outOfZoneDelay, updateSettings} = useAdmin(); + const [localOutOfZoneDelay, setLocalOutOfZoneDelay, applyLocalOutOfZoneDelay] = useLocalVariable(outOfZoneDelay, (e) => updateSettings({outOfZoneDelay: e})); useEffect(() => { if (zoneSettings) { - const newPolygons = zoneSettings.polygons.map((zone) => ({id: idFromPolygon(zone.polygon), polygon: zone.polygon, duration: zone.duration})); - setPolygons(newPolygons.map((zone) => zone.polygon)); + setPolygons(zoneSettings.polygons.map((zone) => zone.polygon)); } }, [zoneSettings]); - useEffect(() => { - if (penaltySettings) { - setAllowedTimeOutOfZone(penaltySettings.allowedTimeOutOfZone.toString()); - } - }, [penaltySettings]); - function idFromPolygon(polygon) { return (polygon[0].lat + polygon[1].lat + polygon[2].lat).toString() + (polygon[0].lng + polygon[1].lng + polygon[2].lng).toString(); } function addPolygon(polygon) { const newPolygons = [...zoneSettings.polygons, {id: idFromPolygon(polygon), polygon: polygon, duration: defaultDuration}]; - updateZoneSettings("polygons", newPolygons); + modifyZoneSettings("polygons", newPolygons); } function removePolygon(i) { const newPolygons = zoneSettings.polygons.filter((_, index) => index !== i); - updateZoneSettings("polygons", newPolygons); + modifyZoneSettings("polygons", newPolygons); } function updateDuration(i, duration) { const newPolygons = zoneSettings.polygons.map((zone, index) => index === i ? {id: zone.id, polygon: zone.polygon, duration: duration} : zone); - updateZoneSettings("polygons", newPolygons); + modifyZoneSettings("polygons", newPolygons); } function handleSettingsSubmit() { applyZoneSettings(); - changePenaltySettings({allowedTimeOutOfZone: Number(allowedTimeOutOfZone)}); + applyLocalOutOfZoneDelay(); } return ( -
+
@@ -148,12 +142,12 @@ export default function PolygonZoneSelector({zoneSettings, updateZoneSettings, a

Reduction order

- updateZoneSettings("polygons", polygons)}> + modifyZoneSettings("polygons", polygons)}> { (zone, i) =>

Zone {i+1}

- updateDuration(i, e.target.value)}/> + updateDuration(i, parseInt(e.target.value, 10))}/>
} @@ -161,7 +155,7 @@ export default function PolygonZoneSelector({zoneSettings, updateZoneSettings, a

Timeout

- setAllowedTimeOutOfZone(e.target.value)} /> + setLocalOutOfZoneDelay(parseInt(e.target.value, 10))} />
diff --git a/traque-front/app/admin/parameters/page.js b/traque-front/app/admin/parameters/page.js index e5f30e1..92af4c1 100644 --- a/traque-front/app/admin/parameters/page.js +++ b/traque-front/app/admin/parameters/page.js @@ -1,5 +1,5 @@ "use client"; -import { useState, useEffect } from "react"; +import { useState } from "react"; import dynamic from "next/dynamic"; import Link from "next/link"; import { TextInput } from "@/components/input"; @@ -9,6 +9,7 @@ import { useAdminConnexion } from "@/context/adminConnexionContext"; import useAdmin from '@/hook/useAdmin'; import Messages from "./components/messages"; import TeamManager from './components/teamManager'; +import useLocalVariable from "@/hook/useLocalVariable"; // Imported at runtime and not at compile time const PolygonZoneSelector = dynamic(() => import('./components/polygonZoneSelector'), { ssr: false }); @@ -23,41 +24,23 @@ const defaultCircleSettings = {type: zoneTypes.circle, min: null, max: null, red const defaultPolygonSettings = {type: zoneTypes.polygon, polygons: []} export default function ConfigurationPage() { - const {zoneSettings, changeZoneSettings, penaltySettings, changePenaltySettings, addTeam} = useAdmin(); const { useProtect } = useAdminConnexion(); - const [allowedTimeBetweenUpdates, setAllowedTimeBetweenUpdates] = useState(""); + const {zoneSettings, sendPositionDelay, updateSettings, addTeam} = useAdmin(); const [teamName, setTeamName] = useState(''); - const [localZoneSettings, setLocalZoneSettings] = useState(zoneSettings); + const [localZoneSettings, setLocalZoneSettings, applyLocalZoneSettings] = useLocalVariable(zoneSettings, (e) => updateSettings({zone: e})); + const [localSendPositionDelay, setLocalSendPositionDelay, applyLocalSendPositionDelay] = useLocalVariable(sendPositionDelay, (e) => updateSettings({sendPositionDelay: e})); useProtect(); - - useEffect(() => { - if (penaltySettings) { - setAllowedTimeBetweenUpdates(penaltySettings.allowedTimeBetweenPositionUpdate.toString()); - } - }, [penaltySettings]); - - useEffect(() => { - if (zoneSettings) { - setLocalZoneSettings(zoneSettings); - } - }, [zoneSettings]); - function updateLocalZoneSettings(key, value) { + function modifyLocalZoneSettings(key, value) { setLocalZoneSettings(prev => ({...prev, [key]: value})); }; - function applySettings() { - if (Number(allowedTimeBetweenUpdates) != penaltySettings.allowedTimeBetweenPositionUpdate) { - changePenaltySettings({allowedTimeBetweenPositionUpdate: Number(allowedTimeBetweenUpdates)}); - } - } - function handleChangeZoneType() { setLocalZoneSettings(localZoneSettings.type == zoneTypes.circle ? defaultPolygonSettings : defaultCircleSettings) } - function handleSubmit(e) { + function handleTeamSubmit(e) { e.preventDefault(); if (teamName !== "") { addTeam(teamName); @@ -76,7 +59,7 @@ export default function ConfigurationPage() {
-
+
setTeamName(e.target.value)} type="text" className="w-full h-full p-4 ring-1 ring-inset ring-gray-300" />
@@ -90,21 +73,21 @@ export default function ConfigurationPage() {

Interval between position updates

- setAllowedTimeBetweenUpdates(e.target.value)} onBlur={applySettings} /> + setLocalSendPositionDelay(parseInt(e.target.value, 10))} onBlur={applyLocalSendPositionDelay} />
-
-
+
+
{localZoneSettings && Change zone type}
{localZoneSettings && localZoneSettings.type == zoneTypes.circle && - changeZoneSettings(localZoneSettings)}/> + } {localZoneSettings && localZoneSettings.type == zoneTypes.polygon && - changeZoneSettings(localZoneSettings)}/> + }
diff --git a/traque-front/components/input.jsx b/traque-front/components/input.jsx index aae9661..94aed9d 100644 --- a/traque-front/components/input.jsx +++ b/traque-front/components/input.jsx @@ -1,13 +1,12 @@ -import React from 'react' +const className = "block w-full h-full p-4 rounded text-center ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600"; export function TextInput({...props}) { return ( - + ) } - export function TextArea({...props}) { return ( -