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

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

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