mirror of
https://git.rezel.net/LudoTech/traque.git
synced 2026-04-10 16:30:18 +02:00
Server heavy refactoring 1 (not functionnal)
This commit is contained in:
@@ -12,6 +12,8 @@
|
|||||||
- [x] Indiquer que l'équipe est hors zone.
|
- [x] Indiquer que l'équipe est hors zone.
|
||||||
- [x] Mettre les stats dans le tiroir (distance, temps, vitesse moy, nb captures, nb envoi)
|
- [x] Mettre les stats dans le tiroir (distance, temps, vitesse moy, nb captures, nb envoi)
|
||||||
- [x] Traduction anglaise
|
- [x] Traduction anglaise
|
||||||
|
- [ ] Rajouter un service dans le manifest (voir comment font les apps de sport)
|
||||||
|
- [ ] Ajouter des timers à la notif
|
||||||
- [ ] Implémenter des notifs lors du background (hors zone, position envoyée, update zone)
|
- [ ] Implémenter des notifs lors du background (hors zone, position envoyée, update zone)
|
||||||
- [ ] Créer le menu paramètre (idées de section : langue, photo équipe, notifs, mode sombre, unitées)
|
- [ ] Créer le menu paramètre (idées de section : langue, photo équipe, notifs, mode sombre, unitées)
|
||||||
- [ ] Afficher la trajectoire passée sur la carte (désactivable)
|
- [ ] Afficher la trajectoire passée sur la carte (désactivable)
|
||||||
|
|||||||
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 { haversineDistance, EARTH_RADIUS } from "./util.js";
|
||||||
import { secureAdminBroadcast } from './admin_socket.js';
|
|
||||||
|
|
||||||
|
|
||||||
/* -------------------------------- Useful functions and constants -------------------------------- */
|
/* -------------------------------- Useful functions and constants -------------------------------- */
|
||||||
|
|
||||||
const zoneTypes = {
|
const ZONE_TYPES = {
|
||||||
circle: "circle",
|
CIRCLE: "circle",
|
||||||
polygon: "polygon"
|
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function latlngEqual(latlng1, latlng2, epsilon = 1e-9) {
|
function latlngEqual(latlng1, latlng2, epsilon = 1e-9) {
|
||||||
@@ -27,17 +15,17 @@ function latlngEqual(latlng1, latlng2, epsilon = 1e-9) {
|
|||||||
|
|
||||||
/* -------------------------------- Circle zones -------------------------------- */
|
/* -------------------------------- 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) {
|
function circleZone(center, radius, duration) {
|
||||||
return {
|
return {
|
||||||
type: zoneTypes.circle,
|
type: ZONE_TYPES.CIRCLE,
|
||||||
center: center,
|
center: center,
|
||||||
radius: radius,
|
radius: radius,
|
||||||
duration: duration,
|
duration: duration,
|
||||||
|
|
||||||
isInZone(location) {
|
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;
|
const {min, max, reductionCount, duration} = settings;
|
||||||
|
|
||||||
if (!min || !max) return [];
|
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 zones = [circleZone(max.center, max.radius, duration)];
|
||||||
const radiusReductionLength = (max.radius - min.radius) / reductionCount;
|
const radiusReductionLength = (max.radius - min.radius) / reductionCount;
|
||||||
@@ -56,7 +44,7 @@ function circleSettingsToZones(settings) {
|
|||||||
for (let i = 1; i < reductionCount; i++) {
|
for (let i = 1; i < reductionCount; i++) {
|
||||||
radius -= radiusReductionLength;
|
radius -= radiusReductionLength;
|
||||||
let new_center = null;
|
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 angle = Math.random() * 2 * Math.PI;
|
||||||
const angularDistance = Math.sqrt(Math.random()) * radiusReductionLength / EARTH_RADIUS;
|
const angularDistance = Math.sqrt(Math.random()) * radiusReductionLength / EARTH_RADIUS;
|
||||||
const lat0Rad = center.lat * Math.PI / 180;
|
const lat0Rad = center.lat * Math.PI / 180;
|
||||||
@@ -83,11 +71,11 @@ function circleSettingsToZones(settings) {
|
|||||||
|
|
||||||
/* -------------------------------- Polygon zones -------------------------------- */
|
/* -------------------------------- Polygon zones -------------------------------- */
|
||||||
|
|
||||||
const defaultPolygonSettings = {type: zoneTypes.polygon, polygons: []}
|
const defaultPolygonSettings = {type: ZONE_TYPES.POLYGON, polygons: []}
|
||||||
|
|
||||||
function polygonZone(polygon, duration) {
|
function polygonZone(polygon, duration) {
|
||||||
return {
|
return {
|
||||||
type: zoneTypes.polygon,
|
type: ZONE_TYPES.POLYGON,
|
||||||
polygon: polygon,
|
polygon: polygon,
|
||||||
duration: duration,
|
duration: duration,
|
||||||
|
|
||||||
@@ -227,10 +215,10 @@ export default {
|
|||||||
|
|
||||||
changeSettings(settings) {
|
changeSettings(settings) {
|
||||||
switch (settings.type) {
|
switch (settings.type) {
|
||||||
case zoneTypes.circle:
|
case ZONE_TYPES.CIRCLE:
|
||||||
this.zones = circleSettingsToZones(settings);
|
this.zones = circleSettingsToZones(settings);
|
||||||
break;
|
break;
|
||||||
case zoneTypes.polygon:
|
case ZONE_TYPES.POLYGON:
|
||||||
this.zones = polygonSettingsToZones(settings);
|
this.zones = polygonSettingsToZones(settings);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@@ -250,7 +238,5 @@ export default {
|
|||||||
end: this.getNextZone(),
|
end: this.getNextZone(),
|
||||||
endDate:this.currentZone.endDate,
|
endDate:this.currentZone.endDate,
|
||||||
};
|
};
|
||||||
playersBroadcast("current_zone", zone);
|
|
||||||
secureAdminBroadcast("current_zone", zone);
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -1,30 +1,24 @@
|
|||||||
import { createServer } from "http";
|
import { createServer } from "http";
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import { Server } from "socket.io";
|
import { Server } from "socket.io";
|
||||||
import { config } from "dotenv";
|
import { initAdminSocketHandler } from "@/socket/adminHandler.js";
|
||||||
import { initAdminSocketHandler } from "./admin_socket.js";
|
import { initPlayerSocketHandler } from "@/socket/playerHandler.js";
|
||||||
import { initTeamSocket } from "./team_socket.js";
|
import { initPhotoUpload } from "./services/photo.js";
|
||||||
import { initPhotoUpload } from "./photo.js";
|
import { PORT, HOST } from "@/util/util.js";
|
||||||
|
|
||||||
config();
|
// --- Configuration ---
|
||||||
const HOST = process.env.HOST;
|
const app = express();
|
||||||
const PORT = process.env.PORT;
|
const httpServer = createServer(app);
|
||||||
|
const io = new Server(httpServer, {
|
||||||
|
cors: { origin: "*", methods: ["GET", "POST"] }
|
||||||
|
});
|
||||||
|
|
||||||
export const app = express();
|
// --- Initialization ---
|
||||||
|
initPhotoUpload(app);
|
||||||
const httpServer = createServer({}, app);
|
initAdminSocketHandler(io);
|
||||||
|
initPlayerSocketHandler(io);
|
||||||
|
|
||||||
|
// --- Server Start ---
|
||||||
httpServer.listen(PORT, HOST, () => {
|
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 { createHash } from "crypto";
|
||||||
import { config } from "dotenv";
|
import { config } from "dotenv";
|
||||||
import game from "./game.js"
|
import game from "./game.js"
|
||||||
@@ -16,7 +15,7 @@ export function secureAdminBroadcast(event, data) {
|
|||||||
|
|
||||||
let loggedInSockets = [];
|
let loggedInSockets = [];
|
||||||
|
|
||||||
export function initAdminSocketHandler() {
|
export function initAdminSocketHandler(io) {
|
||||||
io.of("admin").on("connection", (socket) => {
|
io.of("admin").on("connection", (socket) => {
|
||||||
console.log("Connection of an admin");
|
console.log("Connection of an admin");
|
||||||
let loggedIn = false;
|
let loggedIn = false;
|
||||||
@@ -409,11 +409,9 @@ export default {
|
|||||||
// Update of currentLocation
|
// Update of currentLocation
|
||||||
team.currentLocation = location;
|
team.currentLocation = location;
|
||||||
team.lastCurrentLocationDate = dateNow;
|
team.lastCurrentLocationDate = dateNow;
|
||||||
|
// If hasHandicap
|
||||||
if (this.state == GameState.PLAYING && team.hasHandicap) {
|
if (this.state == GameState.PLAYING && team.hasHandicap) {
|
||||||
team.lastSentLocation = team.currentLocation;
|
team.lastSentLocation = team.currentLocation;
|
||||||
}
|
|
||||||
// Update of enemyLocation
|
|
||||||
if (this.state == GameState.PLAYING && enemyTeam.hasHandicap) {
|
|
||||||
team.enemyLocation = enemyTeam.currentLocation;
|
team.enemyLocation = enemyTeam.currentLocation;
|
||||||
}
|
}
|
||||||
// Update of ready
|
// 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.
|
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
|
This module also exposes functions to send messages via socket to all teams
|
||||||
*/
|
*/
|
||||||
import { io } from "./index.js";
|
|
||||||
import game from "./game.js";
|
import game from "./game.js";
|
||||||
import zoneManager from "./zone_manager.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) => {
|
io.of("player").on("connection", (socket) => {
|
||||||
console.log("Connection of a player");
|
console.log("Connection of a player");
|
||||||
let teamId = null;
|
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 multer from "multer";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
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 UPLOAD_DIR = path.join(process.cwd(), "uploads");
|
||||||
const IMAGES_DIR = path.join(process.cwd(), "assets", "images");
|
const IMAGES_DIR = path.join(process.cwd(), "assets", "images");
|
||||||
const ALLOWED_MIME = [
|
const ALLOWED_MIME = [
|
||||||
@@ -22,7 +19,10 @@ const storage = multer.diskStorage({
|
|||||||
},
|
},
|
||||||
// Save the file with the team ID as the filename
|
// Save the file with the team ID as the filename
|
||||||
filename: function (req, file, callback) {
|
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) {
|
fileFilter: function (req, file, callback) {
|
||||||
if (ALLOWED_MIME.indexOf(file.mimetype) == -1) {
|
if (ALLOWED_MIME.indexOf(file.mimetype) == -1) {
|
||||||
callback(null, false);
|
callback(null, false);
|
||||||
} else if (!game.getTeam(req.query.team)) {
|
} else if (!gameManager.teams.get(req.query.team)) {
|
||||||
callback(null, false);
|
callback(null, false);
|
||||||
} else {
|
} else {
|
||||||
callback(null, true);
|
callback(null, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
// Clean the uploads directory
|
const clean = () => {
|
||||||
function clean() {
|
|
||||||
const files = fs.readdirSync(UPLOAD_DIR);
|
const files = fs.readdirSync(UPLOAD_DIR);
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const filePath = path.join(UPLOAD_DIR, file);
|
const filePath = path.join(UPLOAD_DIR, file);
|
||||||
fs.unlinkSync(filePath);
|
fs.unlinkSync(filePath);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export function initPhotoUpload() {
|
export const initPhotoUpload = (app) => {
|
||||||
clean();
|
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) => {
|
app.post("/upload", upload.single('file'), (req, res) => {
|
||||||
res.set("Access-Control-Allow-Origin", "*");
|
res.set("Access-Control-Allow-Origin", "*");
|
||||||
console.log("upload", req.query)
|
console.log("upload", req.query);
|
||||||
res.send("")
|
res.send("");
|
||||||
})
|
});
|
||||||
//App handler for serving the photo of a team given its secret ID
|
|
||||||
|
// App handler for serving the photo of a team given its secret ID
|
||||||
app.get("/photo/my", (req, res) => {
|
app.get("/photo/my", (req, res) => {
|
||||||
let team = game.getTeam(req.query.team);
|
let team = gameManager.teams.get(req.query.team);
|
||||||
if (team) {
|
if (team) {
|
||||||
const imagePath = path.join(UPLOAD_DIR, team.id);
|
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.set("Access-Control-Allow-Origin", "*");
|
||||||
res.sendFile(fs.existsSync(imagePath) ? imagePath : path.join(IMAGES_DIR, "missing_image.jpg"));
|
res.sendFile(fs.existsSync(imagePath) ? imagePath : path.join(IMAGES_DIR, "missing_image.jpg"));
|
||||||
} else {
|
} 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) => {
|
app.get("/photo/enemy", (req, res) => {
|
||||||
let team = game.getTeam(req.query.team);
|
let team = gameManager.teams.get(req.query.team);
|
||||||
if (team) {
|
if (team) {
|
||||||
const imagePath = path.join(UPLOAD_DIR, team.chasing);
|
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.set("Access-Control-Allow-Origin", "*");
|
||||||
res.sendFile(fs.existsSync(imagePath) ? imagePath : path.join(IMAGES_DIR, "missing_image.jpg"));
|
res.sendFile(fs.existsSync(imagePath) ? imagePath : path.join(IMAGES_DIR, "missing_image.jpg"));
|
||||||
} else {
|
} 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