Remove out of zone penality + upgrades

This commit is contained in:
Sebastien Riviere
2025-09-03 23:10:50 +02:00
parent 062a69aae3
commit 51b99a699f
22 changed files with 429 additions and 620 deletions

View File

@@ -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

View File

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

View File

@@ -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");

View File

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

View File

@@ -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();

View File

@@ -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)) {
if (!loggedIn) return;
game.addTeam(teamName);
secureAdminBroadcast("teams", game.teams);
} else {
socket.emit("error", "Error adding team");
}
});
// 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)) {
if (!loggedIn) return;
game.removeTeam(teamId);
secureAdminBroadcast("teams", game.teams);
} else {
socket.emit("error", "Error removing team");
}
});
// 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)) {
if (!loggedIn) return;
game.reorderTeams(newOrder);
secureAdminBroadcast("teams", game.teams);
game.teams.forEach(t => sendUpdatedTeamInformations(t.id))
} else {
socket.emit("error", "Error reordering 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)) {
if (!loggedIn) return;
game.updateTeam(teamId, newTeam);
secureAdminBroadcast("teams", game.teams);
sendUpdatedTeamInformations(teamId)
sendUpdatedTeamInformations(game.getTeam(teamId).chased)
}
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);
});
});
}

View File

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

View File

@@ -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.`)
}
})
},
}

View File

@@ -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) {

View File

@@ -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 = [];
}
}

View 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;
}
}

View File

@@ -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);
},
}

View File

@@ -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 &&
<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>}
{showArrows && <Arrow pos1={team.currentLocation} pos2={getTeam(team.chasing).currentLocation}/>}
</Marker>
{showArrows && <Arrow key={team.id} pos1={team.currentLocation} pos2={getTeam(team.chased).currentLocation}/>}
</div>
)}
</CustomMapContainer>
</div>

View File

@@ -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)} />

View File

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

View File

@@ -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>
);
}

View File

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

View File

@@ -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>

View File

@@ -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} />
)
}

View File

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

View File

@@ -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 };
}

View 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];
}