mirror of
https://git.rezel.net/LudoTech/traque.git
synced 2026-04-11 00:30:19 +02:00
Server heavy refactoring 1 (not functionnal)
This commit is contained in:
13
server/traque-back/jsconfig.json
Normal file
13
server/traque-back/jsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"target": "ESNext",
|
||||
"checkJs": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
140
server/traque-back/src/core/game_manager.js
Normal file
140
server/traque-back/src/core/game_manager.js
Normal file
@@ -0,0 +1,140 @@
|
||||
import { Team } from "@/team/team.js";
|
||||
import { randint } from "@/util/util.js";
|
||||
import zoneManager from "./zone_manager.js"
|
||||
import { EventEmitter } from 'events';
|
||||
import { EVENTS } from "@/socket/playerHandler.js";
|
||||
import { DefaultState } from "@/states/default_state.js";
|
||||
import { CircularMap } from "@/util/circular_map.js";
|
||||
|
||||
|
||||
const isTeamNameValide = (teamName) => {
|
||||
if (typeof teamName !== 'string') return false;
|
||||
if (teamName.length === 0) return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
const getNewTeamId = (teams) => {
|
||||
const idLength = 6;
|
||||
let newTeamId;
|
||||
do {
|
||||
newTeamId = randint(10 ** idLength);
|
||||
} while (teams.has(newTeamId));
|
||||
return newTeamId.toString().padStart(idLength, '0');
|
||||
};
|
||||
|
||||
|
||||
|
||||
class GameManager extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
this.currentState = new DefaultState(this);
|
||||
this.teams = new CircularMap();
|
||||
this.settings = {
|
||||
zone: zoneManager.settings,
|
||||
scanDelay: 10 * 60 * 1000, // ms
|
||||
outOfZoneDelay: 5 * 60 * 1000 // ms
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// State
|
||||
|
||||
setState(StateClass) {
|
||||
this.currentState.exit();
|
||||
this.currentState = new StateClass(this);
|
||||
this.currentState.enter();
|
||||
}
|
||||
|
||||
|
||||
// Settings
|
||||
|
||||
setSettings(settings) {
|
||||
// Zones
|
||||
zoneManager.changeSettings(settings.zone);
|
||||
this.settings.zone = zoneManager.settings; // TODO : not have two copies of the same object
|
||||
// Delays
|
||||
this.settings.scanDelay = settings.scanDelay;
|
||||
this.settings.outOfZoneDelay = settings.outOfZoneDelay;
|
||||
}
|
||||
|
||||
|
||||
// Emits
|
||||
|
||||
emitTeamUpdate(target, team) {
|
||||
this.emit(EVENTS.INTERNAL.TEAM_UPDATE, target, this.currentState.getTeamMapForTeam(team));
|
||||
}
|
||||
|
||||
emitLogout(target) {
|
||||
this.emit(EVENTS.INTERNAL.LOGOUT, target);
|
||||
}
|
||||
|
||||
|
||||
// Actions
|
||||
|
||||
//// Boilerplates
|
||||
|
||||
_performOnTeam(actionName, teamId, ...args) {
|
||||
if (!this.teams.has(teamId)) return false;
|
||||
const team = this.teams.get(teamId);
|
||||
return this.currentState[actionName](team, ...args);
|
||||
}
|
||||
|
||||
//// All states
|
||||
|
||||
addTeam(teamName) {
|
||||
if (!isTeamNameValide(teamName)) return false;
|
||||
const teamId = getNewTeamId(this.teams);
|
||||
const team = new Team(teamId, teamName);
|
||||
this.teams.set(teamId, team);
|
||||
this.currentState.initTeamContext(team);
|
||||
this.currentState.onTeamOrderChange();
|
||||
return true;
|
||||
}
|
||||
|
||||
removeTeam(teamId) {
|
||||
if (!this.teams.has(teamId)) return false;
|
||||
this.emitLogout(teamId);
|
||||
this.currentState.clearTeamContext(this.teams.get(teamId));
|
||||
this.teams.delete(teamId);
|
||||
this.currentState.onTeamOrderChange();
|
||||
return true;
|
||||
}
|
||||
|
||||
reorderTeam(newTeamsOrder) {
|
||||
if (!this.teams.reorder(newTeamsOrder)) return false;
|
||||
this.currentState.onTeamOrderChange();
|
||||
return true;
|
||||
}
|
||||
|
||||
updateLocation(teamId, coords) {
|
||||
return this._performOnTeam("updateLocation", teamId, coords);
|
||||
}
|
||||
|
||||
//// Playing state
|
||||
|
||||
eliminate(teamId) {
|
||||
return this._performOnTeam("eliminate", teamId);
|
||||
}
|
||||
|
||||
revive(teamId) {
|
||||
return this._performOnTeam("revive", teamId);
|
||||
}
|
||||
|
||||
addHandicap(teamId) {
|
||||
return this._performOnTeam("addHandicap", teamId);
|
||||
}
|
||||
|
||||
clearHandicap(teamId) {
|
||||
return this._performOnTeam("clearHandicap", teamId);
|
||||
}
|
||||
|
||||
scan(teamId, coords) {
|
||||
return this._performOnTeam("updateLocation", teamId, coords) && this._performOnTeam("scan", teamId);
|
||||
}
|
||||
|
||||
capture(teamId, captureCode) {
|
||||
return this._performOnTeam("capture", teamId, captureCode);
|
||||
}
|
||||
}
|
||||
|
||||
export const gameManager = new GameManager();
|
||||
@@ -1,23 +1,11 @@
|
||||
import { playersBroadcast } from './team_socket.js';
|
||||
import { secureAdminBroadcast } from './admin_socket.js';
|
||||
import { haversineDistance, EARTH_RADIUS } from "./util.js";
|
||||
|
||||
|
||||
/* -------------------------------- Useful functions and constants -------------------------------- */
|
||||
|
||||
const zoneTypes = {
|
||||
circle: "circle",
|
||||
polygon: "polygon"
|
||||
}
|
||||
|
||||
const EARTH_RADIUS = 6_371_000; // Radius of the earth in m
|
||||
|
||||
function haversine_distance({ lat: lat1, lng: lon1 }, { lat: lat2, lng: lon2 }) {
|
||||
const degToRad = (deg) => deg * (Math.PI / 180);
|
||||
const dLat = degToRad(lat2 - lat1);
|
||||
const dLon = degToRad(lon2 - lon1);
|
||||
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(degToRad(lat1)) * Math.cos(degToRad(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
|
||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
return c * EARTH_RADIUS;
|
||||
const ZONE_TYPES = {
|
||||
CIRCLE: "circle",
|
||||
POLYGON: "polygon"
|
||||
}
|
||||
|
||||
function latlngEqual(latlng1, latlng2, epsilon = 1e-9) {
|
||||
@@ -27,17 +15,17 @@ function latlngEqual(latlng1, latlng2, epsilon = 1e-9) {
|
||||
|
||||
/* -------------------------------- Circle zones -------------------------------- */
|
||||
|
||||
const defaultCircleSettings = {type: zoneTypes.circle, min: null, max: null, reductionCount: 4, duration: 10}
|
||||
const defaultCircleSettings = {type: ZONE_TYPES.CIRCLE, min: null, max: null, reductionCount: 4, duration: 10}
|
||||
|
||||
function circleZone(center, radius, duration) {
|
||||
return {
|
||||
type: zoneTypes.circle,
|
||||
type: ZONE_TYPES.CIRCLE,
|
||||
center: center,
|
||||
radius: radius,
|
||||
duration: duration,
|
||||
|
||||
isInZone(location) {
|
||||
return haversine_distance(center, location) < this.radius;
|
||||
return haversineDistance(center, location) < this.radius;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,7 +34,7 @@ function circleSettingsToZones(settings) {
|
||||
const {min, max, reductionCount, duration} = settings;
|
||||
|
||||
if (!min || !max) return [];
|
||||
if (haversine_distance(max.center, min.center) > max.radius - min.radius) return [];
|
||||
if (haversineDistance(max.center, min.center) > max.radius - min.radius) return [];
|
||||
|
||||
const zones = [circleZone(max.center, max.radius, duration)];
|
||||
const radiusReductionLength = (max.radius - min.radius) / reductionCount;
|
||||
@@ -56,7 +44,7 @@ function circleSettingsToZones(settings) {
|
||||
for (let i = 1; i < reductionCount; i++) {
|
||||
radius -= radiusReductionLength;
|
||||
let new_center = null;
|
||||
while (!new_center || haversine_distance(new_center, min.center) > radius - min.radius) {
|
||||
while (!new_center || haversineDistance(new_center, min.center) > radius - min.radius) {
|
||||
const angle = Math.random() * 2 * Math.PI;
|
||||
const angularDistance = Math.sqrt(Math.random()) * radiusReductionLength / EARTH_RADIUS;
|
||||
const lat0Rad = center.lat * Math.PI / 180;
|
||||
@@ -83,11 +71,11 @@ function circleSettingsToZones(settings) {
|
||||
|
||||
/* -------------------------------- Polygon zones -------------------------------- */
|
||||
|
||||
const defaultPolygonSettings = {type: zoneTypes.polygon, polygons: []}
|
||||
const defaultPolygonSettings = {type: ZONE_TYPES.POLYGON, polygons: []}
|
||||
|
||||
function polygonZone(polygon, duration) {
|
||||
return {
|
||||
type: zoneTypes.polygon,
|
||||
type: ZONE_TYPES.POLYGON,
|
||||
polygon: polygon,
|
||||
duration: duration,
|
||||
|
||||
@@ -227,10 +215,10 @@ export default {
|
||||
|
||||
changeSettings(settings) {
|
||||
switch (settings.type) {
|
||||
case zoneTypes.circle:
|
||||
case ZONE_TYPES.CIRCLE:
|
||||
this.zones = circleSettingsToZones(settings);
|
||||
break;
|
||||
case zoneTypes.polygon:
|
||||
case ZONE_TYPES.POLYGON:
|
||||
this.zones = polygonSettingsToZones(settings);
|
||||
break;
|
||||
default:
|
||||
@@ -250,7 +238,5 @@ export default {
|
||||
end: this.getNextZone(),
|
||||
endDate:this.currentZone.endDate,
|
||||
};
|
||||
playersBroadcast("current_zone", zone);
|
||||
secureAdminBroadcast("current_zone", zone);
|
||||
},
|
||||
}
|
||||
@@ -1,30 +1,24 @@
|
||||
import { createServer } from "http";
|
||||
import express from "express";
|
||||
import { Server } from "socket.io";
|
||||
import { config } from "dotenv";
|
||||
import { initAdminSocketHandler } from "./admin_socket.js";
|
||||
import { initTeamSocket } from "./team_socket.js";
|
||||
import { initPhotoUpload } from "./photo.js";
|
||||
import { initAdminSocketHandler } from "@/socket/adminHandler.js";
|
||||
import { initPlayerSocketHandler } from "@/socket/playerHandler.js";
|
||||
import { initPhotoUpload } from "./services/photo.js";
|
||||
import { PORT, HOST } from "@/util/util.js";
|
||||
|
||||
config();
|
||||
const HOST = process.env.HOST;
|
||||
const PORT = process.env.PORT;
|
||||
// --- Configuration ---
|
||||
const app = express();
|
||||
const httpServer = createServer(app);
|
||||
const io = new Server(httpServer, {
|
||||
cors: { origin: "*", methods: ["GET", "POST"] }
|
||||
});
|
||||
|
||||
export const app = express();
|
||||
|
||||
const httpServer = createServer({}, app);
|
||||
// --- Initialization ---
|
||||
initPhotoUpload(app);
|
||||
initAdminSocketHandler(io);
|
||||
initPlayerSocketHandler(io);
|
||||
|
||||
// --- Server Start ---
|
||||
httpServer.listen(PORT, HOST, () => {
|
||||
console.log("Server running on http://" + HOST + ":" + PORT);
|
||||
console.log(`Server running on http://${HOST}:${PORT}`);
|
||||
});
|
||||
|
||||
export const io = new Server(httpServer, {
|
||||
cors: {
|
||||
origin: "*",
|
||||
methods: ["GET", "POST"]
|
||||
}
|
||||
});
|
||||
|
||||
initAdminSocketHandler();
|
||||
initTeamSocket();
|
||||
initPhotoUpload();
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { io } from "./index.js";
|
||||
import { createHash } from "crypto";
|
||||
import { config } from "dotenv";
|
||||
import game from "./game.js"
|
||||
@@ -16,7 +15,7 @@ export function secureAdminBroadcast(event, data) {
|
||||
|
||||
let loggedInSockets = [];
|
||||
|
||||
export function initAdminSocketHandler() {
|
||||
export function initAdminSocketHandler(io) {
|
||||
io.of("admin").on("connection", (socket) => {
|
||||
console.log("Connection of an admin");
|
||||
let loggedIn = false;
|
||||
@@ -409,11 +409,9 @@ export default {
|
||||
// Update of currentLocation
|
||||
team.currentLocation = location;
|
||||
team.lastCurrentLocationDate = dateNow;
|
||||
// If hasHandicap
|
||||
if (this.state == GameState.PLAYING && team.hasHandicap) {
|
||||
team.lastSentLocation = team.currentLocation;
|
||||
}
|
||||
// Update of enemyLocation
|
||||
if (this.state == GameState.PLAYING && enemyTeam.hasHandicap) {
|
||||
team.enemyLocation = enemyTeam.currentLocation;
|
||||
}
|
||||
// Update of ready
|
||||
@@ -3,7 +3,6 @@ This file manages team access to the server via websocket.
|
||||
It receives messages, checks permissions, manages authentication and performs actions by calling functions from other modules.
|
||||
This module also exposes functions to send messages via socket to all teams
|
||||
*/
|
||||
import { io } from "./index.js";
|
||||
import game from "./game.js";
|
||||
import zoneManager from "./zone_manager.js";
|
||||
|
||||
@@ -63,7 +62,7 @@ export function sendUpdatedTeamInformations(teamId) {
|
||||
});
|
||||
}
|
||||
|
||||
export function initTeamSocket() {
|
||||
export function initTeamSocket(io) {
|
||||
io.of("player").on("connection", (socket) => {
|
||||
console.log("Connection of a player");
|
||||
let teamId = null;
|
||||
@@ -1,11 +1,8 @@
|
||||
/*
|
||||
This module manages the handler for uploading photos, as well as serving the correct file on requests based on the team ID and current game state
|
||||
*/
|
||||
import { app } from "./index.js";
|
||||
import multer from "multer";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import game from "./game.js";
|
||||
import { gameManager } from "@/core/game_manager.js"
|
||||
|
||||
const UPLOAD_DIR = path.join(process.cwd(), "uploads");
|
||||
const IMAGES_DIR = path.join(process.cwd(), "assets", "images");
|
||||
const ALLOWED_MIME = [
|
||||
@@ -22,7 +19,10 @@ const storage = multer.diskStorage({
|
||||
},
|
||||
// Save the file with the team ID as the filename
|
||||
filename: function (req, file, callback) {
|
||||
callback(null, req.query.team);
|
||||
const teamId = req.query.team;
|
||||
if (typeof teamId === 'string') {
|
||||
callback(null, teamId);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -32,53 +32,55 @@ const upload = multer({
|
||||
fileFilter: function (req, file, callback) {
|
||||
if (ALLOWED_MIME.indexOf(file.mimetype) == -1) {
|
||||
callback(null, false);
|
||||
} else if (!game.getTeam(req.query.team)) {
|
||||
} else if (!gameManager.teams.get(req.query.team)) {
|
||||
callback(null, false);
|
||||
} else {
|
||||
callback(null, true);
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// Clean the uploads directory
|
||||
function clean() {
|
||||
const clean = () => {
|
||||
const files = fs.readdirSync(UPLOAD_DIR);
|
||||
for (const file of files) {
|
||||
const filePath = path.join(UPLOAD_DIR, file);
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export function initPhotoUpload() {
|
||||
export const initPhotoUpload = (app) => {
|
||||
clean();
|
||||
//App handler for uploading a photo and saving it to a file
|
||||
|
||||
// App handler for uploading a photo and saving it to a file
|
||||
app.post("/upload", upload.single('file'), (req, res) => {
|
||||
res.set("Access-Control-Allow-Origin", "*");
|
||||
console.log("upload", req.query)
|
||||
res.send("")
|
||||
})
|
||||
//App handler for serving the photo of a team given its secret ID
|
||||
console.log("upload", req.query);
|
||||
res.send("");
|
||||
});
|
||||
|
||||
// App handler for serving the photo of a team given its secret ID
|
||||
app.get("/photo/my", (req, res) => {
|
||||
let team = game.getTeam(req.query.team);
|
||||
let team = gameManager.teams.get(req.query.team);
|
||||
if (team) {
|
||||
const imagePath = path.join(UPLOAD_DIR, team.id);
|
||||
res.set("Content-Type", "image/png")
|
||||
res.set("Content-Type", "image/png");
|
||||
res.set("Access-Control-Allow-Origin", "*");
|
||||
res.sendFile(fs.existsSync(imagePath) ? imagePath : path.join(IMAGES_DIR, "missing_image.jpg"));
|
||||
} else {
|
||||
res.status(400).send("Team not found")
|
||||
res.status(400).send("Team not found");
|
||||
}
|
||||
})
|
||||
//App handler for serving the photo of the team chased by the team given by its secret ID
|
||||
});
|
||||
|
||||
// App handler for serving the photo of the team chased by the team given by its secret ID
|
||||
app.get("/photo/enemy", (req, res) => {
|
||||
let team = game.getTeam(req.query.team);
|
||||
let team = gameManager.teams.get(req.query.team);
|
||||
if (team) {
|
||||
const imagePath = path.join(UPLOAD_DIR, team.chasing);
|
||||
res.set("Content-Type", "image/png")
|
||||
res.set("Content-Type", "image/png");
|
||||
res.set("Access-Control-Allow-Origin", "*");
|
||||
res.sendFile(fs.existsSync(imagePath) ? imagePath : path.join(IMAGES_DIR, "missing_image.jpg"));
|
||||
} else {
|
||||
res.status(400).send("Team not found")
|
||||
res.status(400).send("Team not found");
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
124
server/traque-back/src/socket/adminHandler.js
Normal file
124
server/traque-back/src/socket/adminHandler.js
Normal file
@@ -0,0 +1,124 @@
|
||||
import { createHash } from "crypto";
|
||||
import { gameManager } from "@/core/game_manager.js"
|
||||
import { ADMIN_PASSWORD_HASH } from "@/util/util.js";
|
||||
|
||||
export const EVENTS = {
|
||||
INTERNAL: {
|
||||
TEAMS_UPDATE: "teams-update",
|
||||
SETTINGS: "settings",
|
||||
},
|
||||
IN: {
|
||||
LOGIN: "login",
|
||||
LOGOUT: "logout",
|
||||
STATE: "state",
|
||||
SETTINGS: "settings",
|
||||
ADD_TEAM: "add-team",
|
||||
REMOVE_TEAM: "remove-team",
|
||||
REORDER_TEAM: "reorder-team",
|
||||
ELIMINATE_TEAM: "eliminate-team",
|
||||
REVIVE_TEAM: "revive-team",
|
||||
},
|
||||
OUT: {
|
||||
TEAMS_UPDATE: "teams-update", // TODO : Too big ? (Send only the changing teams ?)
|
||||
SETTINGS: "settings",
|
||||
},
|
||||
};
|
||||
|
||||
const ADMIN_ROOM = "admin_room";
|
||||
|
||||
export function initAdminSocketHandler(io) {
|
||||
|
||||
// Util
|
||||
|
||||
const emit = (targetId, event, data) => {
|
||||
io.of("admin").to(targetId).emit(event, data);
|
||||
};
|
||||
|
||||
const broadcast = (event, data) => {
|
||||
io.of("admin").to(ADMIN_ROOM).emit(event, data);
|
||||
};
|
||||
|
||||
|
||||
// Admin events
|
||||
|
||||
io.of("admin").on("connection", (socket) => {
|
||||
console.log("Connection of an admin");
|
||||
|
||||
// Variables
|
||||
|
||||
let loggedIn = false;
|
||||
|
||||
|
||||
// Util
|
||||
|
||||
const logout = () => {
|
||||
if (!loggedIn) return;
|
||||
loggedIn = false;
|
||||
socket.leave(ADMIN_ROOM);
|
||||
}
|
||||
|
||||
const login = (password) => {
|
||||
if (loggedIn) return;
|
||||
if (createHash('sha256').update(password).digest('hex') !== ADMIN_PASSWORD_HASH) return false;
|
||||
loggedIn = true;
|
||||
socket.join(ADMIN_ROOM);
|
||||
}
|
||||
|
||||
|
||||
// Socket
|
||||
|
||||
socket.on("disconnect", () => {
|
||||
console.log("Disconnection of an admin");
|
||||
logout();
|
||||
});
|
||||
|
||||
|
||||
// Authentication
|
||||
|
||||
socket.on(EVENTS.IN.LOGIN, (password) => {
|
||||
login(password);
|
||||
});
|
||||
|
||||
socket.on(EVENTS.IN.LOGOUT, () => {
|
||||
logout();
|
||||
});
|
||||
|
||||
|
||||
// Actions
|
||||
|
||||
socket.on(EVENTS.IN.ADD_TEAM, (teamId) => {
|
||||
if (!loggedIn) return;
|
||||
gameManager.addTeam(teamId);
|
||||
});
|
||||
|
||||
socket.on(EVENTS.IN.REMOVE_TEAM, (teamId) => {
|
||||
if (!loggedIn) return;
|
||||
gameManager.removeTeam(teamId);
|
||||
});
|
||||
|
||||
socket.on(EVENTS.IN.REORDER_TEAM, (teamId) => {
|
||||
if (!loggedIn) return;
|
||||
gameManager.reorderTeam(teamId);
|
||||
});
|
||||
|
||||
socket.on(EVENTS.IN.ELIMINATE_TEAM, (teamId) => {
|
||||
if (!loggedIn) return;
|
||||
gameManager.eliminate(teamId);
|
||||
});
|
||||
|
||||
socket.on(EVENTS.IN.REVIVE_TEAM, (teamId) => {
|
||||
if (!loggedIn) return;
|
||||
gameManager.revive(teamId);
|
||||
});
|
||||
|
||||
socket.on(EVENTS.IN.STATE, (state) => {
|
||||
if (!loggedIn) return;
|
||||
gameManager.setState(state);
|
||||
});
|
||||
|
||||
socket.on(EVENTS.IN.SETTINGS, (settings) => {
|
||||
if (!loggedIn) return;
|
||||
gameManager.setSettings(settings);
|
||||
});
|
||||
});
|
||||
}
|
||||
111
server/traque-back/src/socket/playerHandler.js
Normal file
111
server/traque-back/src/socket/playerHandler.js
Normal file
@@ -0,0 +1,111 @@
|
||||
import { gameManager } from "@/core/game_manager.js";
|
||||
|
||||
export const EVENTS = {
|
||||
INTERNAL: {
|
||||
LOGOUT: "logout",
|
||||
TEAM_UPDATE: "team-update",
|
||||
},
|
||||
IN: {
|
||||
LOGIN: "login",
|
||||
LOGOUT: "logout",
|
||||
LOCATION: "location",
|
||||
SCAN: "scan",
|
||||
CAPTURE: "capture",
|
||||
DEVICE: "device",
|
||||
},
|
||||
OUT: {
|
||||
LOGOUT: "logout",
|
||||
TEAM_UPDATE: "team-update",
|
||||
},
|
||||
};
|
||||
|
||||
export function initPlayerSocketHandler(io) {
|
||||
|
||||
// Util
|
||||
|
||||
const emit = (targetId, event, data) => {
|
||||
io.of("player").to(targetId).emit(event, data);
|
||||
};
|
||||
|
||||
|
||||
// Game manager events
|
||||
|
||||
gameManager.on(EVENTS.INTERNAL.LOGOUT, (targetId) => {
|
||||
emit(targetId, EVENTS.OUT.LOGOUT);
|
||||
});
|
||||
|
||||
gameManager.on(EVENTS.INTERNAL.TEAM_UPDATE, (targetId, playTeamData) => {
|
||||
emit(targetId, EVENTS.OUT.TEAM_UPDATE, playTeamData);
|
||||
});
|
||||
|
||||
|
||||
// Player events
|
||||
|
||||
io.of("player").on("connect", (socket) => {
|
||||
console.log("Connection of a player");
|
||||
|
||||
// Variables
|
||||
|
||||
let teamId = null;
|
||||
|
||||
|
||||
// Util
|
||||
|
||||
const isLoggedIn = () => {
|
||||
return teamId !== null;
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
if (!isLoggedIn()) return;
|
||||
socket.leave(teamId);
|
||||
teamId = null;
|
||||
}
|
||||
|
||||
const login = (loginTeamId) => {
|
||||
if (!gameManager.teams.has(loginTeamId) || teamId === loginTeamId) return;
|
||||
logout();
|
||||
teamId = loginTeamId
|
||||
socket.join(teamId);
|
||||
}
|
||||
|
||||
|
||||
// Socket
|
||||
|
||||
socket.on("disconnect", () => {
|
||||
console.log("Disconnection of a player");
|
||||
logout();
|
||||
});
|
||||
|
||||
|
||||
// Authentication
|
||||
|
||||
socket.on(EVENTS.IN.LOGIN, (loginTeamId, callback) => {
|
||||
login(loginTeamId);
|
||||
callback(isLoggedIn());
|
||||
if (isLoggedIn()) gameManager.emitTeamUpdate(socket.id, gameManager.teams.get(teamId));
|
||||
});
|
||||
|
||||
socket.on(EVENTS.IN.LOGOUT, () => {
|
||||
logout();
|
||||
});
|
||||
|
||||
|
||||
// Actions
|
||||
|
||||
socket.on(EVENTS.IN.LOCATION, (coords) => {
|
||||
if (!isLoggedIn()) return;
|
||||
gameManager.updateLocation(teamId, coords);
|
||||
});
|
||||
|
||||
socket.on(EVENTS.IN.SCAN, (coords) => {
|
||||
if (!isLoggedIn()) return;
|
||||
gameManager.scan(teamId, coords);
|
||||
});
|
||||
|
||||
socket.on(EVENTS.IN.CAPTURE, (captureCode, callback) => {
|
||||
if (!isLoggedIn()) return;
|
||||
const success = gameManager.capture(teamId, captureCode);
|
||||
callback(success);
|
||||
});
|
||||
});
|
||||
}
|
||||
19
server/traque-back/src/states/default_state.js
Normal file
19
server/traque-back/src/states/default_state.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { GameState } from "@/states/game_state.js";
|
||||
import { DefaultTeamMapper } from "@/team/mapper/default_team_mapper.js";
|
||||
|
||||
export class DefaultState extends GameState {
|
||||
constructor(manager) {
|
||||
super(manager, new DefaultTeamMapper());
|
||||
}
|
||||
|
||||
static get stateName () {
|
||||
return "default";
|
||||
}
|
||||
|
||||
// State functions
|
||||
|
||||
updateLocation(team, coords) {
|
||||
team.updateLocation(coords);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
19
server/traque-back/src/states/finished_state.js
Normal file
19
server/traque-back/src/states/finished_state.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { GameState } from "@/states/game_state.js";
|
||||
import { FinishedTeamMapper } from "@/team/mapper/finished_team_mapper.js";
|
||||
|
||||
export class FinishedState extends GameState {
|
||||
constructor(manager) {
|
||||
super(manager, new FinishedTeamMapper());
|
||||
}
|
||||
|
||||
static get stateName () {
|
||||
return "finished";
|
||||
}
|
||||
|
||||
// State functions
|
||||
|
||||
updateLocation(team, coords) {
|
||||
team.updateLocation(coords);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
36
server/traque-back/src/states/game_state.js
Normal file
36
server/traque-back/src/states/game_state.js
Normal file
@@ -0,0 +1,36 @@
|
||||
export class GameState {
|
||||
constructor(manager, teamMapper) {
|
||||
this.manager = manager;
|
||||
this.teamMapper = teamMapper;
|
||||
}
|
||||
|
||||
// Life cycle
|
||||
|
||||
initTeamContext(_team) {}
|
||||
|
||||
enter() {
|
||||
this.manager.teams.forEach(team => this.initTeamContext(team));
|
||||
}
|
||||
|
||||
clearTeamContext(_team) {}
|
||||
|
||||
exit() {
|
||||
this.manager.teams.forEach(team => this.clearTeamContext(team));
|
||||
}
|
||||
|
||||
|
||||
// Hooks
|
||||
|
||||
onTeamOrderChange() {}
|
||||
|
||||
|
||||
// Mappers
|
||||
|
||||
getTeamMapForTeam(team) {
|
||||
return this.teamMapper.mapForTeam(team);
|
||||
}
|
||||
|
||||
getTeamMapForAdmin(team) {
|
||||
return this.teamMapper.mapForAdmin(team);
|
||||
}
|
||||
}
|
||||
36
server/traque-back/src/states/placement_state.js
Normal file
36
server/traque-back/src/states/placement_state.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import { haversineDistance } from "@/util/util.js";
|
||||
import { GameState } from "@/states/game_state.js";
|
||||
import { PlacementTeamMapper } from "@/team/mapper/placement_team_mapper.js";
|
||||
|
||||
export class PlacementState extends GameState {
|
||||
constructor(manager) {
|
||||
super(manager, new PlacementTeamMapper());
|
||||
}
|
||||
|
||||
static get stateName () {
|
||||
return "placement";
|
||||
}
|
||||
|
||||
// Life cycle
|
||||
|
||||
initTeamContext(team) {
|
||||
team.context = {
|
||||
placementZone: null,
|
||||
isInPlacementZone: true,
|
||||
};
|
||||
this.manager.emitTeamUpdate(team.id, team);
|
||||
}
|
||||
|
||||
|
||||
// State functions
|
||||
|
||||
updateLocation(team, coords) {
|
||||
team.updateLocation(coords);
|
||||
|
||||
team.context.isInPlacementZone = (
|
||||
team.context.placementZone ? haversineDistance(team.location, team.context.placementZone.center) < team.context.placementZone.radius : true
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
174
server/traque-back/src/states/playing_state.js
Normal file
174
server/traque-back/src/states/playing_state.js
Normal file
@@ -0,0 +1,174 @@
|
||||
import zoneManager from "@/core/zone_manager.js";
|
||||
import { randint } from "@/util/util.js";
|
||||
import { GameState } from "@/states/game_state.js";
|
||||
import { FinishedState } from "./finished_state.js";
|
||||
import { TimeoutManager } from "@/util/timeout_manager.js";
|
||||
import { PlayingTeamMapper } from "@/team/mapper/playing_team_mapper.js";
|
||||
|
||||
|
||||
const getNewCaptureCode = () => {
|
||||
const codeLength = 4;
|
||||
return randint(10 ** codeLength).toString().padStart(codeLength, '0');
|
||||
};
|
||||
|
||||
|
||||
export class PlayingState extends GameState {
|
||||
constructor(manager) {
|
||||
super(manager, new PlayingTeamMapper());
|
||||
}
|
||||
|
||||
static get stateName () {
|
||||
return "playing";
|
||||
}
|
||||
|
||||
// Life cycle
|
||||
|
||||
initTeamContext(team) {
|
||||
team.context = {
|
||||
// Team
|
||||
scanLocation: team.location,
|
||||
captureCode: getNewCaptureCode(),
|
||||
// Booleans
|
||||
isEliminated: false,
|
||||
isOutOfZone: false,
|
||||
hasHandicap: false,
|
||||
// Timeouts
|
||||
scanTimeout: new TimeoutManager(() => this.scan(team.id), this.manager.settings.scanDelay, true),
|
||||
outOfZoneTimeout: new TimeoutManager(() => this.addHandicap(team.id), this.manager.settings.outOfZoneDelay, true),
|
||||
// Hunter and target
|
||||
hunter: null,
|
||||
target: null,
|
||||
targetScanLocation: { coords: null, timesptamp: null },
|
||||
};
|
||||
this.manager.emitTeamUpdate(team.id, team);
|
||||
}
|
||||
|
||||
enter() {
|
||||
super.enter();
|
||||
this.onTeamOrderChange();
|
||||
zoneManager.start();
|
||||
}
|
||||
|
||||
clearTeamContext(team) {
|
||||
team.context.scanTimeout.clear();
|
||||
team.context.outOfZoneTimeout.clear();
|
||||
}
|
||||
|
||||
exit() {
|
||||
super.exit();
|
||||
zoneManager.stop();
|
||||
}
|
||||
|
||||
|
||||
// Hooks
|
||||
|
||||
onTeamOrderChange() {
|
||||
const playingTeamsOrder = this.manager.teams.order.filter(team => !team.context.isEliminated);
|
||||
const length = playingTeamsOrder.length;
|
||||
playingTeamsOrder.forEach((team, i) => {
|
||||
const hunter = this.manager.teams.get(playingTeamsOrder[(i+length-1) % length]);
|
||||
const target = this.manager.teams.get(playingTeamsOrder[(i+1) % length]);
|
||||
let hasChanged = false;
|
||||
if (!team.context.hunter.equals(hunter)) {
|
||||
team.context.hunter = hunter;
|
||||
hasChanged = true;
|
||||
}
|
||||
if (!team.context.target.equals(target)) {
|
||||
team.context.target = target;
|
||||
team.context.targetScanLocation = target.context.location;
|
||||
hasChanged = true;
|
||||
}
|
||||
if (hasChanged) {
|
||||
this.manager.emitTeamUpdate(team.id, team);
|
||||
}
|
||||
});
|
||||
|
||||
if (this.manager.teams.order.filter(team => !team.context.isEliminated).length <= 2) {
|
||||
this.manager.setState(FinishedState);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// State functions
|
||||
|
||||
eliminate(team) {
|
||||
if (team.context.isEliminated) return false;
|
||||
|
||||
this.clearTeamContext(team);
|
||||
team.context.isEliminated = true;
|
||||
this.onTeamOrderChange();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
revive(team) {
|
||||
if (!team.context.isEliminated) return false;
|
||||
|
||||
this.initTeamContext(team);
|
||||
this.onTeamOrderChange();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
addHandicap(team) {
|
||||
if (team.context.hasHandicap) return false;
|
||||
|
||||
team.context.hasHandicap = true;
|
||||
team.context.scanTimeout.clear();
|
||||
this.manager.emitTeamUpdate(team.id, team);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
clearHandicap(team) {
|
||||
if (!team.context.hasHandicap) return false;
|
||||
|
||||
team.context.hasHandicap = false;
|
||||
team.context.scanTimeout.set();
|
||||
this.manager.emitTeamUpdate(team.id, team);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
scan(team) {
|
||||
if (team.context.hasHandicap || team.context.isEliminated) return false;
|
||||
|
||||
team.context.scanLocation = team.location;
|
||||
team.context.targetScanLocation = team.context.target.context.scanLocation;
|
||||
team.context.scanTimeout.set();
|
||||
this.manager.emitTeamUpdate(team.id, team);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
capture(team, captureCode) {
|
||||
if (team.context.hasHandicap || team.context.isEliminated) return false;
|
||||
|
||||
if (captureCode != team.context.target.context.captureCode) return false;
|
||||
this.eliminate(team.context.target);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
updateLocation(team, coords) {
|
||||
team.updateLocation(coords);
|
||||
|
||||
const isOutOfZone = !zoneManager.isInZone(team.location);
|
||||
// Exit zone case
|
||||
if (isOutOfZone && !team.context.isOutOfZone) {
|
||||
team.context.isOutOfZone = true;
|
||||
team.context.outOfZoneTimeout.set();
|
||||
this.manager.emitTeamUpdate(team.id, team);
|
||||
// Enter zone case
|
||||
} else if (!isOutOfZone && team.context.isOutOfZone) {
|
||||
team.context.isOutOfZone = false;
|
||||
team.context.outOfZoneTimeout.clear();
|
||||
this.clearHandicap(team);
|
||||
this.manager.emitTeamUpdate(team.id, team);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
20
server/traque-back/src/team/mapper/default_team_mapper.js
Normal file
20
server/traque-back/src/team/mapper/default_team_mapper.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import { TeamMapper } from "./team_mapper.js";
|
||||
import { DefaultState } from "@/states/default_state.js";
|
||||
|
||||
export class DefaultTeamMapper extends TeamMapper {
|
||||
mapForTeam(team) {
|
||||
return {
|
||||
...super.mapForTeam(team),
|
||||
state: DefaultState.stateName,
|
||||
context: {}
|
||||
};
|
||||
}
|
||||
|
||||
mapForAdmin(team) {
|
||||
return {
|
||||
...super.mapForAdmin(team),
|
||||
state: DefaultState.stateName,
|
||||
context: {}
|
||||
};
|
||||
}
|
||||
}
|
||||
20
server/traque-back/src/team/mapper/finished_team_mapper.js
Normal file
20
server/traque-back/src/team/mapper/finished_team_mapper.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import { TeamMapper } from "./team_mapper.js";
|
||||
import { FinishedState } from "@/states/finished_state.js";
|
||||
|
||||
export class FinishedTeamMapper extends TeamMapper {
|
||||
mapForTeam(team) {
|
||||
return {
|
||||
...super.mapForTeam(team),
|
||||
state: FinishedState.stateName,
|
||||
context: {}
|
||||
};
|
||||
}
|
||||
|
||||
mapForAdmin(team) {
|
||||
return {
|
||||
...super.mapForAdmin(team),
|
||||
state: FinishedState.stateName,
|
||||
context: {}
|
||||
};
|
||||
}
|
||||
}
|
||||
26
server/traque-back/src/team/mapper/placement_team_mapper.js
Normal file
26
server/traque-back/src/team/mapper/placement_team_mapper.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import { TeamMapper } from "./team_mapper.js";
|
||||
import { PlacementState } from "@/states/placement_state.js";
|
||||
|
||||
export class PlacementTeamMapper extends TeamMapper {
|
||||
mapForTeam(team) {
|
||||
return {
|
||||
...super.mapForTeam(team),
|
||||
state: PlacementState.stateName,
|
||||
context: {
|
||||
placementZone: team.context.placementZone,
|
||||
isInPlacementZone: team.context.isInPlacementZone,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
mapForAdmin(team) {
|
||||
return {
|
||||
...super.mapForAdmin(team),
|
||||
state: PlacementState.stateName,
|
||||
context: {
|
||||
placementZone: team.context.placementZone,
|
||||
isInPlacementZone: team.context.isInPlacementZone,
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
56
server/traque-back/src/team/mapper/playing_team_mapper.js
Normal file
56
server/traque-back/src/team/mapper/playing_team_mapper.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import { TeamMapper } from "./team_mapper.js";
|
||||
import { PlayingState } from "@/states/playing_state.js";
|
||||
import zoneManager from "@/core/zone_manager.js";
|
||||
|
||||
export class PlayingTeamMapper extends TeamMapper {
|
||||
mapForTeam(team) {
|
||||
return {
|
||||
...super.mapForTeam(team),
|
||||
state: PlayingState.stateName,
|
||||
context: {
|
||||
// Team
|
||||
captureCode: team.context.captureCode,
|
||||
scanLocation: team.context.scanLocation,
|
||||
// Booleans
|
||||
isEliminated: team.context.isEliminated,
|
||||
isOutOfZone: team.context.isOutOfZone,
|
||||
hasHandicap: team.context.hasHandicap,
|
||||
// Timeouts
|
||||
scanRemainingTime: team.context.scanTimeout.remainingTime,
|
||||
outOfZoneRemainingTime: team.context.outOfZoneTimeout.remainingTime,
|
||||
// Target
|
||||
targetName: team.context.target?.name,
|
||||
targetScanLocation: team.context.targetScanLocation,
|
||||
targetHasHandicap: team.context.hunter?.context.hasHandicap,
|
||||
// Zone
|
||||
zoneType: zoneManager.settings.zoneType,
|
||||
zoneCurrent: zoneManager.getCurrentZone(),
|
||||
zoneNext: zoneManager.getNextZone(),
|
||||
zoneRemainingTime: zoneManager.currentZone?.endDate ? Math.max(0, zoneManager.currentZone.endDate - Date.now()) : null
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
mapForAdmin(team) {
|
||||
return {
|
||||
...super.mapForAdmin(team),
|
||||
state: PlayingState.stateName,
|
||||
context: {
|
||||
// Team
|
||||
captureCode: team.context.captureCode,
|
||||
scanLocation: team.context.scanLocation,
|
||||
// Booleans
|
||||
isEliminated: team.context.isEliminated,
|
||||
isOutOfZone: team.context.isOutOfZone,
|
||||
hasHandicap: team.context.hasHandicap,
|
||||
// Timeouts
|
||||
scanRemainingTime: team.context.scanTimeout.remainingTime,
|
||||
outOfZoneRemainingTime: team.context.outOfZoneTimeout.remainingTime,
|
||||
// Target and hunter
|
||||
hunterName: team.context.hunter.name,
|
||||
targetName: team.context.target.name,
|
||||
targetScanLocation: team.context.targetScanLocation,
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
23
server/traque-back/src/team/mapper/team_mapper.js
Normal file
23
server/traque-back/src/team/mapper/team_mapper.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import { GameState } from "@/states/game_state.js";
|
||||
|
||||
export class TeamMapper {
|
||||
mapForTeam(team) {
|
||||
return {
|
||||
id: team.id,
|
||||
name: team.name,
|
||||
state: GameState.stateName,
|
||||
context: {}
|
||||
};
|
||||
}
|
||||
|
||||
mapForAdmin(team) {
|
||||
return {
|
||||
id: team.id,
|
||||
name: team.name,
|
||||
location: team.location,
|
||||
connectedPlayerCount: team.connectedPlayerCount,
|
||||
state: GameState.stateName,
|
||||
context: {}
|
||||
};
|
||||
}
|
||||
}
|
||||
19
server/traque-back/src/team/team.js
Normal file
19
server/traque-back/src/team/team.js
Normal file
@@ -0,0 +1,19 @@
|
||||
export class Team {
|
||||
constructor(id, teamName) {
|
||||
// Identity
|
||||
this.id = id;
|
||||
this.name = teamName;
|
||||
// Location
|
||||
this.location = { coords: null, timestamp: null };
|
||||
// Context
|
||||
this.context = {};
|
||||
}
|
||||
|
||||
updateLocation(coords) {
|
||||
this.location = { coords: coords, timestamp: Date.now()}
|
||||
}
|
||||
|
||||
equals(team) {
|
||||
return this.id === team.id;
|
||||
}
|
||||
}
|
||||
33
server/traque-back/src/util/circular_map.js
Normal file
33
server/traque-back/src/util/circular_map.js
Normal file
@@ -0,0 +1,33 @@
|
||||
export class CircularMap extends Map {
|
||||
constructor(entries) {
|
||||
super(entries);
|
||||
this.order = Array.from(this.keys());
|
||||
}
|
||||
|
||||
set(key, value) {
|
||||
if (!super.has(key)) {
|
||||
this.order.push(key);
|
||||
}
|
||||
return super.set(key, value);
|
||||
}
|
||||
|
||||
delete(key) {
|
||||
if (super.delete(key)) {
|
||||
this.order = this.order.filter(k => k !== key);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.order = [];
|
||||
super.clear();
|
||||
}
|
||||
|
||||
reorder(newOrder) {
|
||||
const isValid = newOrder.length === this.size && new Set([...this.order, ...newOrder]).size === this.size;
|
||||
if (!isValid) return false;
|
||||
this.order = newOrder;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
26
server/traque-back/src/util/timeout_manager.js
Normal file
26
server/traque-back/src/util/timeout_manager.js
Normal file
@@ -0,0 +1,26 @@
|
||||
export class TimeoutManager {
|
||||
constructor(callback, delay, set = false) {
|
||||
this.callback = callback;
|
||||
this.delay = delay;
|
||||
this.id = null;
|
||||
this.date = null;
|
||||
if (set) this.set();
|
||||
}
|
||||
|
||||
get remainingTime() {
|
||||
if (!this.id) return null;
|
||||
return Math.max(0, this.date - Date.now());
|
||||
}
|
||||
|
||||
set() {
|
||||
if (this.id) clearTimeout(this.id);
|
||||
this.id = setTimeout(this.callback, this.delay);
|
||||
this.date = Date.now() + this.delay;
|
||||
}
|
||||
|
||||
clear() {
|
||||
if (this.id) clearTimeout(this.id);
|
||||
this.id = null;
|
||||
this.date = null;
|
||||
}
|
||||
}
|
||||
22
server/traque-back/src/util/util.js
Normal file
22
server/traque-back/src/util/util.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import { config } from "dotenv";
|
||||
|
||||
config();
|
||||
|
||||
export const ADMIN_PASSWORD_HASH = process.env.ADMIN_PASSWORD_HASH || "";
|
||||
export const HOST = process.env.HOST || "0.0.0.0";
|
||||
export const PORT = Number(process.env.PORT) || 3000;
|
||||
|
||||
export const EARTH_RADIUS = 6_371_000; // Radius of the earth in meters
|
||||
|
||||
export const randint = (max) => {
|
||||
return Math.floor(Math.random() * max);
|
||||
}
|
||||
|
||||
export const haversineDistance = ({ lat: lat1, lng: lon1 }, { lat: lat2, lng: lon2 }) => {
|
||||
// Return the distance in meters between the two points of coordinates
|
||||
const degToRad = (deg) => deg * (Math.PI / 180);
|
||||
const dLat = degToRad(lat2 - lat1);
|
||||
const dLon = degToRad(lon2 - lon1);
|
||||
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(degToRad(lat1)) * Math.cos(degToRad(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
|
||||
return 2 * EARTH_RADIUS * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
}
|
||||
Reference in New Issue
Block a user