mirror of
https://git.rezel.net/LudoTech/traque.git
synced 2026-02-09 02:10:18 +01:00
Remove out of zone penality + upgrades
This commit is contained in:
21
doc/TODO.md
21
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
|
||||
|
||||
@@ -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 (
|
||||
<View>
|
||||
{ zoneExtremities.begin && <Circle center={zoneExtremities.begin.center} radius={zoneExtremities.begin.radius} strokeColor="red" fillColor="rgba(255,0,0,0.1)" strokeWidth={2} />}
|
||||
{ zoneExtremities.end && <Circle center={zoneExtremities.end.center} radius={zoneExtremities.end.radius} strokeColor="green" fillColor="rgba(0,255,0,0.1)" strokeWidth={2} />}
|
||||
</View>
|
||||
);
|
||||
case zoneTypes.polygon:
|
||||
return (
|
||||
<View>
|
||||
{ zoneExtremities.begin && <Polygon coordinates={zoneExtremities.begin.points} strokeColor="red" fillColor="rgba(255,0,0,0.1)" strokeWidth={2} /> }
|
||||
{ zoneExtremities.end && <Polygon coordinates={zoneExtremities.end.points} strokeColor="green" fillColor="rgba(0,255,0,0.1)" strokeWidth={2} /> }
|
||||
</View>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const Map = () => {
|
||||
return (
|
||||
<MapView ref={mapRef} style={{flex: 1}} initialRegion={initialRegion} mapType="standard" onTouchMove={() => setCenterMap(false)}>
|
||||
{ gameState == GameState.PLACEMENT && startingArea && circle("0, 0, 255", startingArea)}
|
||||
{ gameState == GameState.PLAYING && zoneExtremities && <Polygon coordinates={zoneExtremities.begin.points} strokeColor="red" fillColor="rgba(255,0,0,0.1)" strokeWidth={2} /> }
|
||||
{ gameState == GameState.PLAYING && zoneExtremities && <Polygon coordinates={zoneExtremities.end.points} strokeColor="green" fillColor="rgba(0,255,0,0.1)" strokeWidth={2} /> }
|
||||
{ gameState == GameState.PLAYING && zoneExtremities && <Zones/>}
|
||||
{ location &&
|
||||
<Marker coordinate={{ latitude: location[0], longitude: location[1] }} anchor={{ x: 0.33, y: 0.33 }}>
|
||||
<Image source={require("../assets/images/marker/blue.png")} style={{width: 24, height: 24}} resizeMode="contain"/>
|
||||
@@ -345,9 +370,6 @@ export default function Display() {
|
||||
<View style={styles.topContainer}>
|
||||
<View style={styles.topheadContainer}>
|
||||
{ Logout() }
|
||||
{ penalties > 0 && gameState == GameState.PLAYING &&
|
||||
<Text style={{marginTop: 15, fontSize: 15}}>Pénalités : {penalties}</Text>
|
||||
}
|
||||
{ false && Settings() }
|
||||
</View>
|
||||
<View style={styles.teamNameContainer}>
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 (
|
||||
<teamContext.Provider value={value}>
|
||||
{children}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
|
||||
handicapTeam(teamId) {
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
@@ -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.`)
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 = [];
|
||||
}
|
||||
}
|
||||
80
traque-back/timeout_handler.js
Normal file
80
traque-back/timeout_handler.js
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<Polyline positions={[pos1, pos2]} pathOptions={{ color: 'black', weight: 3 }}/>
|
||||
)
|
||||
);
|
||||
} 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 (
|
||||
<div>
|
||||
@@ -83,10 +83,12 @@ export default function LiveMap({mapStyle, showZones, showNames, showArrows}) {
|
||||
<CustomMapContainer mapStyle={mapStyle}>
|
||||
<Zones/>
|
||||
{teams.map((team) => team.currentLocation && !team.captured &&
|
||||
<Marker key={team.id} position={team.currentLocation} icon={positionIcon}>
|
||||
{showNames && <Tooltip permanent direction="top" offset={[0.5, -15]} className="custom-tooltip">{team.name}</Tooltip>}
|
||||
{showArrows && <Arrow pos1={team.currentLocation} pos2={getTeam(team.chasing).currentLocation}/>}
|
||||
</Marker>
|
||||
<div>
|
||||
<Marker key={team.id} position={team.currentLocation} icon={positionIcon}>
|
||||
{showNames && <Tooltip permanent direction="top" offset={[0.5, -15]} className="custom-tooltip">{team.name}</Tooltip>}
|
||||
</Marker>
|
||||
{showArrows && <Arrow key={team.id} pos1={team.currentLocation} pos2={getTeam(team.chased).currentLocation}/>}
|
||||
</div>
|
||||
)}
|
||||
</CustomMapContainer>
|
||||
</div>
|
||||
|
||||
@@ -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 }) {
|
||||
<DotLine label="ID de capture" value={String(team.captureCode).padStart(4, '0')} />
|
||||
</div>
|
||||
<div>
|
||||
<DotLine label="Chasse" value={getTeamName(team.chasing) ?? NO_VALUE} />
|
||||
<DotLine label="Chassé par" value={getTeamName(team.chased) ?? NO_VALUE} />
|
||||
<DotLine label="Chasse" value={getTeam(team.chasing).name ?? NO_VALUE} />
|
||||
<DotLine label="Chassé par" value={getTeam(team.chased).name ?? NO_VALUE} />
|
||||
</div>
|
||||
<div>
|
||||
<DotLine label="Distance" value={formatDistance(team.distance)} />
|
||||
|
||||
@@ -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 (
|
||||
<div>
|
||||
<MapEventListener onLeftClick={handleLeftClick} onRightClick={handleRightClick} onMouseMove={handleMouseMove}/>
|
||||
{minCenter && minRadius && <Circle center={minCenter} radius={minRadius} color="blue" fillColor="blue" />}
|
||||
{maxCenter && maxRadius && <Circle center={maxCenter} radius={maxRadius} color="red" fillColor="red" />}
|
||||
{minCenter && minRadius && <Circle center={minCenter} radius={minRadius} color="red" fillColor="red" />}
|
||||
{maxCenter && maxRadius && <Circle center={maxCenter} radius={maxRadius} color="blue" fillColor="blue" />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className='h-full w-full bg-white p-3 gap-3 flex flex-row shadow-2xl'>
|
||||
<div className='h-full w-full gap-3 flex flex-row'>
|
||||
<div className="h-full flex-1">
|
||||
<CustomMapContainer>
|
||||
<Drawings minZone={zoneSettings.min} setMinZone={(e) => updateZoneSettings("min", e)} maxZone={zoneSettings.max} setMaxZone={(e) => updateZoneSettings("max", e)} editMode={editMode} />
|
||||
<Drawings minZone={zoneSettings.min} setMinZone={(e) => modifyZoneSettings("min", e)} maxZone={zoneSettings.max} setMaxZone={(e) => modifyZoneSettings("max", e)} editMode={editMode} />
|
||||
</CustomMapContainer>
|
||||
</div>
|
||||
<div className="h-full w-1/6 flex flex-col gap-3">
|
||||
@@ -83,21 +82,21 @@ export default function CircleZoneSelector({zoneSettings, updateZoneSettings, ap
|
||||
{editMode == EditMode.MAX && <RedButton onClick={() => setEditMode(EditMode.MIN)}>Click to edit last zone</RedButton>}
|
||||
</div>
|
||||
<div className="w-full flex flex-row gap-2 items-center justify-between">
|
||||
<p>Number</p>
|
||||
<p>Reduction number</p>
|
||||
<div className="w-16 h-10">
|
||||
<TextInput value={zoneSettings.reductionCount} onChange={(e) => updateZoneSettings("reductionCount", e.target.value)} />
|
||||
<TextInput id="reduction-number" value={zoneSettings?.reductionCount ?? ""} onChange={(e) => modifyZoneSettings("reductionCount", customStringToInt(e.target.value))} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full flex flex-row gap-2 items-center justify-between">
|
||||
<p>Duration</p>
|
||||
<p>Zone duration</p>
|
||||
<div className="w-16 h-10">
|
||||
<TextInput value={zoneSettings.duration} onChange={(e) => updateZoneSettings("duration", e.target.value)} />
|
||||
<TextInput id="duration" value={zoneSettings?.duration ?? ""} onChange={(e) => modifyZoneSettings("duration", customStringToInt(e.target.value))} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full flex flex-row gap-2 items-center justify-between">
|
||||
<p>Timeout</p>
|
||||
<div className="w-16 h-10">
|
||||
<TextInput value={allowedTimeOutOfZone} onChange={(e) => setAllowedTimeOutOfZone(e.target.value)} />
|
||||
<TextInput id="timeout" value={localOutOfZoneDelay ?? ""} onChange={(e) => setLocalOutOfZoneDelay(customStringToInt(e.target.value))} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full h-15">
|
||||
|
||||
@@ -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 (
|
||||
<Section title="Message">
|
||||
<div className="w-full h-full flex flex-col gap-3 items-center">
|
||||
<MessageInput title="Attente :" value={waitingMessage} onChange={(e) => setWaitingMessage(e.target.value)} onBlur={applySettings}/>
|
||||
<MessageInput title="Capture :" value={capturedMessage} onChange={(e) => setCapturedMessage(e.target.value)} onBlur={applySettings}/>
|
||||
<MessageInput title="Victoire :" value={winnerEndMessage} onChange={(e) => setWinnerEndMessage(e.target.value)} onBlur={applySettings}/>
|
||||
<MessageInput title="Défaite :" value={loserEndMessage} onChange={(e) => setLoserEndMessage(e.target.value)} onBlur={applySettings}/>
|
||||
</div>
|
||||
<Section title="Message" innerClassName="w-full h-full flex flex-col gap-3 items-center">
|
||||
<MessageInput id="waiting" title="Attente :" value={localGameSettings?.waiting ?? ""} onChange={(e) => modifyLocalZoneSettings("waiting", e.target.value)} onBlur={applyLocalGameSettings}/>
|
||||
<MessageInput id="captured" title="Capture :" value={localGameSettings?.captured ?? ""} onChange={(e) => modifyLocalZoneSettings("captured", e.target.value)} onBlur={applyLocalGameSettings}/>
|
||||
<MessageInput id="winner" title="Victoire :" value={localGameSettings?.winner ?? ""} onChange={(e) => modifyLocalZoneSettings("winner", e.target.value)} onBlur={applyLocalGameSettings}/>
|
||||
<MessageInput id="loser" title="Défaite :" value={localGameSettings?.loser ?? ""} onChange={(e) => modifyLocalZoneSettings("loser", e.target.value)} onBlur={applyLocalGameSettings}/>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<div className='h-full w-full bg-white p-3 gap-3 flex flex-row shadow-2xl'>
|
||||
<div className='h-full w-full gap-3 flex flex-row'>
|
||||
<div className="h-full flex-1">
|
||||
<CustomMapContainer>
|
||||
<Drawings polygons={polygons} addPolygon={addPolygon} removePolygon={removePolygon} />
|
||||
@@ -148,12 +142,12 @@ export default function PolygonZoneSelector({zoneSettings, updateZoneSettings, a
|
||||
<div className="w-full text-center">
|
||||
<h2 className="text-xl">Reduction order</h2>
|
||||
</div>
|
||||
<ReorderList droppableId="zones-order" array={zoneSettings.polygons} setArray={(polygons) => updateZoneSettings("polygons", polygons)}>
|
||||
<ReorderList droppableId="zones-order" array={zoneSettings.polygons} setArray={(polygons) => modifyZoneSettings("polygons", polygons)}>
|
||||
{ (zone, i) =>
|
||||
<div className="w-full p-2 bg-white flex flex-row gap-2 items-center justify-between">
|
||||
<p>Zone {i+1}</p>
|
||||
<div className="w-16 h-10">
|
||||
<TextInput value={zone.duration} onChange={(e) => updateDuration(i, e.target.value)}/>
|
||||
<TextInput value={zone?.duration || ""} onChange={(e) => updateDuration(i, parseInt(e.target.value, 10))}/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -161,7 +155,7 @@ export default function PolygonZoneSelector({zoneSettings, updateZoneSettings, a
|
||||
<div className="w-full flex flex-row gap-2 items-center justify-between">
|
||||
<p>Timeout</p>
|
||||
<div className="w-16 h-10">
|
||||
<TextInput value={allowedTimeOutOfZone} onChange={(e) => setAllowedTimeOutOfZone(e.target.value)} />
|
||||
<TextInput id="timeout" value={localOutOfZoneDelay ?? ""} onChange={(e) => setLocalOutOfZoneDelay(parseInt(e.target.value, 10))} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full h-15">
|
||||
|
||||
@@ -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() {
|
||||
</div>
|
||||
<Messages/>
|
||||
<Section title="Équipe" outerClassName="flex-1 min-h-0" innerClassName="flex flex-col items-center gap-3">
|
||||
<form className='w-full flex flex-row gap-3' onSubmit={handleSubmit}>
|
||||
<form className='w-full flex flex-row gap-3' onSubmit={handleTeamSubmit}>
|
||||
<div className='w-full'>
|
||||
<input name="teamName" label='Team name' value={teamName} onChange={(e) => setTeamName(e.target.value)} type="text" className="w-full h-full p-4 ring-1 ring-inset ring-gray-300" />
|
||||
</div>
|
||||
@@ -90,21 +73,21 @@ export default function ConfigurationPage() {
|
||||
<div className="w-full flex flex-row gap-2 items-center justify-between">
|
||||
<p>Interval between position updates</p>
|
||||
<div className="w-16 h-10">
|
||||
<TextInput value={allowedTimeBetweenUpdates} onChange={(e) => setAllowedTimeBetweenUpdates(e.target.value)} onBlur={applySettings} />
|
||||
<TextInput id="position-update" value={localSendPositionDelay ?? ""} onChange={(e) => setLocalSendPositionDelay(parseInt(e.target.value, 10))} onBlur={applyLocalSendPositionDelay} />
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
<div className="h-full flex-1 flex flex-col">
|
||||
<div className="w-full h-20">
|
||||
<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>}
|
||||
</div>
|
||||
<div className="w-full flex-1">
|
||||
{localZoneSettings && localZoneSettings.type == zoneTypes.circle &&
|
||||
<CircleZoneSelector zoneSettings={localZoneSettings} updateZoneSettings={updateLocalZoneSettings} applyZoneSettings={() => changeZoneSettings(localZoneSettings)}/>
|
||||
<CircleZoneSelector zoneSettings={localZoneSettings} modifyZoneSettings={modifyLocalZoneSettings} applyZoneSettings={applyLocalZoneSettings}/>
|
||||
}
|
||||
{localZoneSettings && localZoneSettings.type == zoneTypes.polygon &&
|
||||
<PolygonZoneSelector zoneSettings={localZoneSettings} updateZoneSettings={updateLocalZoneSettings} applyZoneSettings={() => changeZoneSettings(localZoneSettings)}/>
|
||||
<PolygonZoneSelector zoneSettings={localZoneSettings} modifyZoneSettings={modifyLocalZoneSettings} applyZoneSettings={applyLocalZoneSettings}/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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 (
|
||||
<input {...props} type="text" 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" />
|
||||
<input {...props} type="text" className={className} />
|
||||
)
|
||||
}
|
||||
|
||||
export function TextArea({...props}) {
|
||||
return (
|
||||
<textarea {...props} 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" />
|
||||
<textarea {...props} className={className} />
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,44 +1,52 @@
|
||||
"use client";
|
||||
import { createContext, useContext, useEffect, useMemo, useState } from "react";
|
||||
import { createContext, useContext, useMemo, useState } from "react";
|
||||
import { useSocket } from "./socketContext";
|
||||
import useSocketListener from "@/hook/useSocketListener";
|
||||
import { useAdminConnexion } from "./adminConnexionContext";
|
||||
import { GameState } from "@/util/gameState";
|
||||
|
||||
const adminContext = createContext();
|
||||
|
||||
export function AdminProvider({ children }) {
|
||||
const [teams, setTeams] = useState([]);
|
||||
const [zoneSettings, setZoneSettings] = useState(null)
|
||||
const [penaltySettings, setPenaltySettings] = useState(null);
|
||||
const [gameSettings, setGameSettings] = useState(null);
|
||||
const [zoneExtremities, setZoneExtremities] = useState(null);
|
||||
const [nextZoneDate, setNextZoneDate] = useState(null);
|
||||
const { adminSocket } = useSocket();
|
||||
const { loggedIn } = useAdminConnexion();
|
||||
// teams
|
||||
const [teams, setTeams] = useState([]);
|
||||
// game_state
|
||||
const [gameState, setGameState] = useState(GameState.SETUP);
|
||||
const [startDate, setStartDate] = useState(null);
|
||||
// current_zone
|
||||
const [zoneType, setZoneType] = useState(null);
|
||||
const [zoneExtremities, setZoneExtremities] = useState(null);
|
||||
const [nextZoneDate, setNextZoneDate] = useState(null);
|
||||
// settings
|
||||
const [messages, setMessages] = useState(null);
|
||||
const [zoneSettings, setZoneSettings] = useState(null)
|
||||
const [sendPositionDelay, setSendPositionDelay] = useState(null);
|
||||
const [outOfZoneDelay, setOutOfZoneDelay] = useState(null);
|
||||
|
||||
// Send a request to get the teams when the user logs in
|
||||
useEffect(() => {
|
||||
adminSocket.emit("get_teams");
|
||||
}, [loggedIn]);
|
||||
useSocketListener(adminSocket, "teams", setTeams);
|
||||
|
||||
function setCurrentZone(data) {
|
||||
useSocketListener(adminSocket, "game_state", (data) => {
|
||||
setGameState(data.state);
|
||||
setStartDate(data.date)
|
||||
});
|
||||
|
||||
useSocketListener(adminSocket, "current_zone", (data) => {
|
||||
setZoneType(data.type);
|
||||
setZoneExtremities({begin: data.begin, end: data.end});
|
||||
setNextZoneDate(data.endDate);
|
||||
}
|
||||
});
|
||||
|
||||
useSocketListener(adminSocket, "game_state", (data) => {setGameState(data.state); setStartDate(data.startDate)});
|
||||
useSocketListener(adminSocket, "teams", setTeams);
|
||||
useSocketListener(adminSocket, "zone_settings", setZoneSettings);
|
||||
useSocketListener(adminSocket, "game_settings", setGameSettings);
|
||||
useSocketListener(adminSocket, "penalty_settings", setPenaltySettings);
|
||||
useSocketListener(adminSocket, "current_zone", setCurrentZone);
|
||||
useSocketListener(adminSocket, "settings", (data) => {
|
||||
setMessages(data.messages);
|
||||
setZoneSettings(data.zone);
|
||||
setSendPositionDelay(data.sendPositionDelay);
|
||||
setOutOfZoneDelay(data.outOfZoneDelay);
|
||||
});
|
||||
|
||||
const value = useMemo(() => (
|
||||
{ zoneExtremities, teams, zoneSettings, penaltySettings, gameSettings, gameState, nextZoneDate, startDate }
|
||||
), [zoneSettings, teams, gameState, zoneExtremities, penaltySettings, gameSettings, nextZoneDate, startDate]);
|
||||
{ zoneSettings, teams, gameState, zoneType, zoneExtremities, sendPositionDelay, outOfZoneDelay, messages, nextZoneDate, startDate }
|
||||
), [zoneSettings, teams, gameState, zoneType, zoneExtremities, sendPositionDelay, outOfZoneDelay, messages, nextZoneDate, startDate]);
|
||||
|
||||
return (
|
||||
<adminContext.Provider value={value}>
|
||||
{children}
|
||||
|
||||
@@ -7,19 +7,10 @@ export default function useAdmin() {
|
||||
const { teams } = adminContext;
|
||||
const { adminSocket } = useSocket();
|
||||
|
||||
function pollTeams() {
|
||||
adminSocket.emit("get_teams");
|
||||
}
|
||||
|
||||
function getTeam(teamId) {
|
||||
return teams.find(team => team.id === teamId);
|
||||
}
|
||||
|
||||
function getTeamName(teamId) {
|
||||
let team = getTeam(teamId);
|
||||
return team ? team.name : "";
|
||||
}
|
||||
|
||||
function reorderTeams(newOrder) {
|
||||
adminSocket.emit("reorder_teams", newOrder);
|
||||
}
|
||||
@@ -40,17 +31,9 @@ export default function useAdmin() {
|
||||
adminSocket.emit("change_state", state);
|
||||
}
|
||||
|
||||
function changeZoneSettings(zone) {
|
||||
adminSocket.emit("set_zone_settings", zone);
|
||||
function updateSettings(settings) {
|
||||
adminSocket.emit("update_settings", settings);
|
||||
}
|
||||
|
||||
function changePenaltySettings(penalties) {
|
||||
adminSocket.emit("set_penalty_settings", penalties);
|
||||
}
|
||||
|
||||
function changeGameSettings(settings) {
|
||||
adminSocket.emit("set_game_settings", settings);
|
||||
}
|
||||
return { ...adminContext, changeGameSettings, changeZoneSettings, changePenaltySettings, pollTeams, getTeam, getTeamName, reorderTeams, addTeam, removeTeam, changeState, updateTeam };
|
||||
|
||||
return { ...adminContext, getTeam, reorderTeams, addTeam, removeTeam, updateTeam, changeState, updateSettings };
|
||||
}
|
||||
|
||||
15
traque-front/hook/useLocalVariable.jsx
Normal file
15
traque-front/hook/useLocalVariable.jsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
export default function useLocalVariable(variable, setVariable) {
|
||||
const [localVariable, setLocalVariable] = useState(variable);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalVariable(variable);
|
||||
}, [variable]);
|
||||
|
||||
function applyLocalVariable() {
|
||||
setVariable(localVariable);
|
||||
}
|
||||
|
||||
return [localVariable, setLocalVariable, applyLocalVariable];
|
||||
}
|
||||
Reference in New Issue
Block a user