mirror of
https://git.rezel.net/LudoTech/traque.git
synced 2026-04-10 16:30:18 +02:00
Server heavy refactoring 2 (not functionnal)
This commit is contained in:
3947
server/traque-back/package-lock.json
generated
Normal file
3947
server/traque-back/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,7 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@turf/turf": "^7.3.4",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
|
|||||||
36
server/traque-back/src/config/events.js
Normal file
36
server/traque-back/src/config/events.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
export const PLAYER_HANDLER_EVENTS = {
|
||||||
|
LOGIN: "login",
|
||||||
|
LOGOUT: "logout",
|
||||||
|
LOCATION: "location",
|
||||||
|
SCAN: "scan",
|
||||||
|
CAPTURE: "capture",
|
||||||
|
DEVICE: "device",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ADMIN_HANDLER_EVENTS = {
|
||||||
|
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",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GAME_MANAGER_EVENTS = {
|
||||||
|
INIT_PLAYER: "init-player",
|
||||||
|
INIT_ADMIN: "init-admin",
|
||||||
|
UPDATE_GAME: "update-game",
|
||||||
|
DELETE_TEAM: "delete-team",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PLAYER_SYNCHRONIZER_EVENTS = {
|
||||||
|
UPDATE_FULL: "update-full",
|
||||||
|
LOGOUT: "logout",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ADMIN_SYNCHRONIZER_EVENTS = {
|
||||||
|
UPDATE_FULL: "update-full",
|
||||||
|
};
|
||||||
44
server/traque-back/src/config/game.js
Normal file
44
server/traque-back/src/config/game.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { DEFAULT_ZONES_SETTINGS } from "@/config/zone.js";
|
||||||
|
import { DefaultState, PlacementState, PlayingState, FinishedState } from '@/core/states/game/index.js';
|
||||||
|
|
||||||
|
export const STATE_SETTINGS = {
|
||||||
|
ENTRY_STATE_CLASS: DefaultState,
|
||||||
|
TRANSITION_MATRIX: {
|
||||||
|
[DefaultState.name]: {
|
||||||
|
[DefaultState.name]: (state) => false,
|
||||||
|
[PlacementState.name]: (state) => state.canGameStart(),
|
||||||
|
[PlayingState.name]: (state) => false,
|
||||||
|
[FinishedState.name]: (state) => false,
|
||||||
|
},
|
||||||
|
[PlacementState.name]: {
|
||||||
|
[DefaultState.name]: (state) => true,
|
||||||
|
[PlacementState.name]: (state) => false,
|
||||||
|
[PlayingState.name]: (state) => true,
|
||||||
|
[FinishedState.name]: (state) => false,
|
||||||
|
},
|
||||||
|
[PlayingState.name]: {
|
||||||
|
[DefaultState.name]: (state) => true,
|
||||||
|
[PlacementState.name]: (state) => true,
|
||||||
|
[PlayingState.name]: (state) => false,
|
||||||
|
[FinishedState.name]: (state) => state.isGameOver(),
|
||||||
|
},
|
||||||
|
[FinishedState.name]: {
|
||||||
|
[DefaultState.name]: (state) => true,
|
||||||
|
[PlacementState.name]: (state) => false,
|
||||||
|
[PlayingState.name]: (state) => false,
|
||||||
|
[FinishedState.name]: (state) => false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_GAME_SETTINGS = {
|
||||||
|
playingZones: DEFAULT_ZONES_SETTINGS.CIRCLE,
|
||||||
|
placementZones: {},
|
||||||
|
scanDelay: 10 * 60 * 1000, // ms
|
||||||
|
outOfZoneDelay: 5 * 60 * 1000 // ms
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TEAM_ID_LENGTH = 6;
|
||||||
|
export const CAPTURE_CODE_LENGTH = 4;
|
||||||
|
|
||||||
|
export const RESTART_TIMERS = true;
|
||||||
6
server/traque-back/src/config/server.js
Normal file
6
server/traque-back/src/config/server.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
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;
|
||||||
13
server/traque-back/src/config/zone.js
Normal file
13
server/traque-back/src/config/zone.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
export const ZONE_TYPES = {
|
||||||
|
CIRCLE: "circle",
|
||||||
|
POLYGON: "polygon"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_ZONES_SETTINGS = {
|
||||||
|
CIRCLE: { type: ZONE_TYPES.CIRCLE, min: null, max: null, reductionCount: 5, duration: 20 * 60 * 1000 },
|
||||||
|
POLYGON: { type: ZONE_TYPES.POLYGON, polygons: [] },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TURF_DISTANCE_UNIT = 'meters';
|
||||||
|
export const TURF_CIRCLE_STEPS = 32;
|
||||||
|
export const TURF_BUFFER_SIZE = 1;
|
||||||
70
server/traque-back/src/core/factories/game_zone_factory.js
Normal file
70
server/traque-back/src/core/factories/game_zone_factory.js
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import * as turf from '@turf/turf';
|
||||||
|
import { ZoneWithDuration } from '@/core/models/zone.js';
|
||||||
|
import { TURF_BUFFER_SIZE, TURF_CIRCLE_STEPS, TURF_DISTANCE_UNIT, ZONE_TYPES } from '@/config/zone.js';
|
||||||
|
|
||||||
|
export const settingsToZoneList = (settings) => {
|
||||||
|
if (!settings) return [];
|
||||||
|
|
||||||
|
switch (settings.type) {
|
||||||
|
case ZONE_TYPES.CIRCLE:
|
||||||
|
return circleSettingsToZoneList(settings);
|
||||||
|
case ZONE_TYPES.POLYGON:
|
||||||
|
return polygonSettingsToZoneList(settings);
|
||||||
|
default:
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const circleSettingsToZoneList = ({ min, max, reductionCount, duration }) => {
|
||||||
|
if (min == null || max == null) return [];
|
||||||
|
|
||||||
|
const zones = [];
|
||||||
|
const add = (center, radius) => zones.push(new ZoneWithDuration(turf.circle(turf.point(center), radius, { steps: TURF_CIRCLE_STEPS, units: TURF_DISTANCE_UNIT }), duration));
|
||||||
|
|
||||||
|
// Add max zone
|
||||||
|
add(turf.point(max.center), max.radius);
|
||||||
|
|
||||||
|
// Add intermediates zones
|
||||||
|
const radiusReductionLength = (max.radius - min.radius) / reductionCount;
|
||||||
|
let center = turf.point(max.center);
|
||||||
|
let radius = max.radius;
|
||||||
|
for (let i = 1; i < reductionCount; i++) {
|
||||||
|
radius -= radiusReductionLength;
|
||||||
|
|
||||||
|
let tempCenter;
|
||||||
|
do {
|
||||||
|
const distance = radius * Math.sqrt(Math.random());
|
||||||
|
const bearing = Math.random() * 360;
|
||||||
|
tempCenter = turf.destination(center, distance, bearing, { units: 'meters' });
|
||||||
|
} while (turf.distance(tempCenter, turf.point(min.center)) > radius - min.radius);
|
||||||
|
center = tempCenter;
|
||||||
|
|
||||||
|
add(center, radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add min zone
|
||||||
|
add(turf.point(min.center), min.radius);
|
||||||
|
|
||||||
|
return zones;
|
||||||
|
};
|
||||||
|
|
||||||
|
const polygonSettingsToZoneList = ({ polygons }) => {
|
||||||
|
const polygonsCount = polygons.length;
|
||||||
|
if (polygonsCount === 0) return [];
|
||||||
|
|
||||||
|
// Convert polygons to turf polygon with a buffer
|
||||||
|
const bufferedPolygons = polygons.map(item => ({
|
||||||
|
polygon: turf.buffer(turf.polygon([[...item.polygon, item.polygon[0]]]), TURF_BUFFER_SIZE, { units: TURF_DISTANCE_UNIT }),
|
||||||
|
duration: item.duration
|
||||||
|
}));
|
||||||
|
|
||||||
|
const inversedZones = [bufferedPolygons[polygonsCount-1]];
|
||||||
|
|
||||||
|
for (let i = polygonsCount-2; 0 <= i; i--) {
|
||||||
|
const { polygon, duration } = bufferedPolygons[i];
|
||||||
|
const union = turf.union(turf.featureCollection([inversedZones[inversedZones.length-1].polygon, polygon]));
|
||||||
|
if (union && union.geometry.type === 'Polygon') inversedZones.push(new ZoneWithDuration(union, duration));
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...inversedZones].reverse();
|
||||||
|
};
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import * as turf from '@turf/turf';
|
||||||
|
import { Zone } from '@/core/models/zone.js';
|
||||||
|
import { TURF_CIRCLE_STEPS, TURF_DISTANCE_UNIT } from '@/config/zone.js';
|
||||||
|
|
||||||
|
export const settingsToZone = (settings) => {
|
||||||
|
if (!settings) return null;
|
||||||
|
const { center, radius } = settings;
|
||||||
|
return new Zone(turf.circle(turf.point(center), radius, { steps: TURF_CIRCLE_STEPS, units: TURF_DISTANCE_UNIT }));
|
||||||
|
};
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
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();
|
|
||||||
122
server/traque-back/src/core/managers/game_manager.js
Normal file
122
server/traque-back/src/core/managers/game_manager.js
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import { EventEmitter } from 'events';
|
||||||
|
import { ZoneManager } from "@/core/managers/zone_manager.js";
|
||||||
|
import { TeamManager } from '@/core/managers/team_manager.js';
|
||||||
|
import { GAME_MANAGER_EVENTS } from "@/config/events.js";
|
||||||
|
|
||||||
|
|
||||||
|
export class GameManager extends EventEmitter {
|
||||||
|
constructor(stateSettings, gameSettings) {
|
||||||
|
super();
|
||||||
|
// Data
|
||||||
|
this.teams = new TeamManager();
|
||||||
|
this.zoneManager = new ZoneManager();
|
||||||
|
this.settings = gameSettings;
|
||||||
|
// State
|
||||||
|
this.state = null;
|
||||||
|
this.transitionMatrix = stateSettings.TRANSITION_MATRIX;
|
||||||
|
this.setState(stateSettings.ENTRY_STATE_CLASS);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --------------- ACTIONS --------------- //
|
||||||
|
|
||||||
|
// State
|
||||||
|
|
||||||
|
setState(StateClass) {
|
||||||
|
if (!this._canTransitionTo(StateClass)) return;
|
||||||
|
this.state?.exit();
|
||||||
|
this.state = new StateClass(this.teams, this.zoneManager);
|
||||||
|
this.state.enter(this.settings);
|
||||||
|
this.emit(GAME_MANAGER_EVENTS.UPDATE_GAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Settings
|
||||||
|
|
||||||
|
setSettings(settings) {
|
||||||
|
this.settings = settings;
|
||||||
|
this.state.applySettings(settings);
|
||||||
|
this.zoneManager.updateZones(settings.playingZones);
|
||||||
|
this.emit(GAME_MANAGER_EVENTS.UPDATE_GAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Teams
|
||||||
|
|
||||||
|
addTeam(teamName) {
|
||||||
|
const team = this.teams.add(teamName);
|
||||||
|
if (team == null) return false;
|
||||||
|
this.state.initTeam(team);
|
||||||
|
this.emit(GAME_MANAGER_EVENTS.UPDATE_GAME);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeTeam(teamId) {
|
||||||
|
if (!this.teams.has(teamId)) return false;
|
||||||
|
this.state.clearTeam(this.teams.get(teamId));
|
||||||
|
this.teams.delete(teamId);
|
||||||
|
this.emit(GAME_MANAGER_EVENTS.DELETE_TEAM, teamId);
|
||||||
|
this.emit(GAME_MANAGER_EVENTS.UPDATE_GAME);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
reorderTeam(newTeamsOrder) {
|
||||||
|
if (!this.teams.reorder(newTeamsOrder)) return false;
|
||||||
|
this.emit(GAME_MANAGER_EVENTS.UPDATE_GAME);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Team state
|
||||||
|
|
||||||
|
eliminate(teamId) {
|
||||||
|
return this._teamAction(teamId, "eliminate");
|
||||||
|
}
|
||||||
|
|
||||||
|
revive(teamId) {
|
||||||
|
return this._teamAction(teamId, "revive");
|
||||||
|
}
|
||||||
|
|
||||||
|
addHandicap(teamId) {
|
||||||
|
return this._teamAction(teamId, "addHandicap");
|
||||||
|
}
|
||||||
|
|
||||||
|
clearHandicap(teamId) {
|
||||||
|
return this._teamAction(teamId, "clearHandicap");
|
||||||
|
}
|
||||||
|
|
||||||
|
scan(teamId) {
|
||||||
|
return this._teamAction(teamId, "scan");
|
||||||
|
}
|
||||||
|
|
||||||
|
capture(teamId, captureCode) {
|
||||||
|
return this._teamAction(teamId, "capture", captureCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateLocation(teamId, coords) {
|
||||||
|
return this._teamAction(teamId, "updateLocation", coords);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --------------- OTHER --------------- //
|
||||||
|
|
||||||
|
// Login handlers
|
||||||
|
|
||||||
|
onPlayerLogin(socketId, teamId) {
|
||||||
|
this.emit(GAME_MANAGER_EVENTS.INIT_PLAYER, socketId, teamId);
|
||||||
|
}
|
||||||
|
|
||||||
|
onAdminLogin(socketId) {
|
||||||
|
this.emit(GAME_MANAGER_EVENTS.INIT_ADMIN, socketId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Util
|
||||||
|
|
||||||
|
_canTransitionTo(StateClass) {
|
||||||
|
return this.state === null || this.transitionMatrix[this.state.name][StateClass.name](this.state);
|
||||||
|
};
|
||||||
|
|
||||||
|
_teamAction(teamId, actionName, ...args) {
|
||||||
|
if (!this.teams.has(teamId) || typeof this.state[actionName] !== 'function') return false;
|
||||||
|
const success = this.state[actionName](this.teams.get(teamId), ...args);
|
||||||
|
this.emit(GAME_MANAGER_EVENTS.UPDATE_GAME);
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
}
|
||||||
59
server/traque-back/src/core/managers/team_manager.js
Normal file
59
server/traque-back/src/core/managers/team_manager.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { Team } from "@/core/models/team.js";
|
||||||
|
|
||||||
|
export class TeamManager {
|
||||||
|
constructor() {
|
||||||
|
this._map = new Map();
|
||||||
|
this.order = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Read
|
||||||
|
|
||||||
|
get size() {
|
||||||
|
return this._map.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
get(id) {
|
||||||
|
return this._map.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
has(id) {
|
||||||
|
return this._map.has(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
forEach(callback) {
|
||||||
|
for (const id of this.order) {
|
||||||
|
callback(this._map.get(id), id, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Write
|
||||||
|
|
||||||
|
add(teamName) {
|
||||||
|
if (!Team.isTeamNameValid(teamName)) return null;
|
||||||
|
let id; do { id = Team.getNewTeamId() } while (this.has(id));
|
||||||
|
const team = new Team(id, teamName);
|
||||||
|
if (!this.has(id)) this.order.push(id);
|
||||||
|
this._map.set(id, team);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(id) {
|
||||||
|
if (!this._map.delete(id)) return false;
|
||||||
|
this.order = this.order.filter(i => i !== id);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.order = [];
|
||||||
|
this._map.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
73
server/traque-back/src/core/managers/zone_manager.js
Normal file
73
server/traque-back/src/core/managers/zone_manager.js
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { Scheduler } from "@/util/scheduler.js";
|
||||||
|
import { settingsToZoneList } from "@/core/factories/game_zone_factory.js";
|
||||||
|
|
||||||
|
export class ZoneManager {
|
||||||
|
constructor() {
|
||||||
|
this._zones = [];
|
||||||
|
this._currentZoneId = null;
|
||||||
|
this._scheduledZoneTransition = new Scheduler();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
get firstZonePolygon() {
|
||||||
|
return this._zones[0]?.polygon ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get currentZonePolygon() {
|
||||||
|
return this._currentZone?.polygon ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get nextZonePolygon() {
|
||||||
|
if (!this._isActive) return null;
|
||||||
|
return this._zones[this._currentZoneId + 1]?.polygon ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get dateOfZoneTransition() {
|
||||||
|
return this._scheduledZoneTransition.dateOfExecution;
|
||||||
|
}
|
||||||
|
|
||||||
|
get timeToZoneTransition() {
|
||||||
|
return this._scheduledZoneTransition.timeToExecution;
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
this._jumpToNextZone();
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
this._currentZoneId = null;
|
||||||
|
this._scheduledZoneTransition.interrupt();
|
||||||
|
}
|
||||||
|
|
||||||
|
isInZone(location) {
|
||||||
|
return this._isZonesEmpty || this._currentZone?.isInZone(location);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateZones(settings) {
|
||||||
|
this._zones = settingsToZoneList(settings);
|
||||||
|
if (this._isActive) {
|
||||||
|
this.stop();
|
||||||
|
this.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
get _currentZone() {
|
||||||
|
if (!this._isActive) return null;
|
||||||
|
return this._zones[this._currentZoneId].polygon;
|
||||||
|
}
|
||||||
|
|
||||||
|
get _isZonesEmpty() {
|
||||||
|
return this._zones.length === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
get _isActive() {
|
||||||
|
return this._currentZoneId !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_jumpToNextZone() {
|
||||||
|
if (this._isZonesEmpty) return;
|
||||||
|
this._currentZoneId = this._isActive ? this._currentZoneId + 1 : 0;
|
||||||
|
if (this._currentZoneId + 1 < this._zones.length) this._scheduledZoneTransition.start(() => this._jumpToNextZone(), this._currentZone.duration);
|
||||||
|
}
|
||||||
|
}
|
||||||
32
server/traque-back/src/core/models/team.js
Normal file
32
server/traque-back/src/core/models/team.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { CAPTURE_CODE_LENGTH, TEAM_ID_LENGTH } from "@/config/game.js";
|
||||||
|
import { randint } from "@/util/random.js";
|
||||||
|
|
||||||
|
export class Team {
|
||||||
|
constructor(id, teamName) {
|
||||||
|
this.id = id;
|
||||||
|
this.name = teamName;
|
||||||
|
this.location = { coords: null, timestamp: null };
|
||||||
|
this.state = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static isTeamNameValid = (teamName) => {
|
||||||
|
return typeof teamName === 'string' && teamName.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static getNewTeamId = () => {
|
||||||
|
return randint(10 ** TEAM_ID_LENGTH).toString().padStart(TEAM_ID_LENGTH, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
static getNewCaptureCode = () => {
|
||||||
|
return randint(10 ** CAPTURE_CODE_LENGTH).toString().padStart(CAPTURE_CODE_LENGTH, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
equals(team) {
|
||||||
|
return this.id === team.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateLocation(coords) {
|
||||||
|
this.location = { coords: coords, timestamp: Date.now() };
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
28
server/traque-back/src/core/models/zone.js
Normal file
28
server/traque-back/src/core/models/zone.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import * as turf from '@turf/turf';
|
||||||
|
|
||||||
|
export class Zone {
|
||||||
|
constructor(turfPolygon) {
|
||||||
|
this._turfPolygon = turfPolygon;
|
||||||
|
}
|
||||||
|
|
||||||
|
get polygon() {
|
||||||
|
// Return a [latitude, longitude] list
|
||||||
|
return this._turfPolygon.geometry.coordinates[0].slice(0, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
isInZone(location) {
|
||||||
|
// location : [latitude, longitude]
|
||||||
|
return turf.booleanPointInPolygon(turf.point(location), this._turfPolygon);
|
||||||
|
}
|
||||||
|
|
||||||
|
equals(zone) {
|
||||||
|
return turf.booleanEqual(this._turfPolygon, zone._turfPolygon);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export class ZoneWithDuration extends Zone {
|
||||||
|
constructor(turfPolygon, duration) {
|
||||||
|
super(turfPolygon);
|
||||||
|
this.duration = duration;
|
||||||
|
}
|
||||||
|
};
|
||||||
44
server/traque-back/src/core/states/game/default_state.js
Normal file
44
server/traque-back/src/core/states/game/default_state.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { DefaultTeam } from "@/core/states/teams/default_team.js";
|
||||||
|
|
||||||
|
export class DefaultState {
|
||||||
|
constructor(teams, zoneManager) {
|
||||||
|
this.teams = teams;
|
||||||
|
this.zoneManager = zoneManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get name () {
|
||||||
|
return "default";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --------------- LIFE CYCLE --------------- //
|
||||||
|
|
||||||
|
initTeam(team, settings) {
|
||||||
|
team.state = new DefaultTeam(team).init(settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
enter(settings) {
|
||||||
|
this.teams.forEach(team => this.initTeam(team, settings));
|
||||||
|
}
|
||||||
|
|
||||||
|
clearTeam(_team) {}
|
||||||
|
|
||||||
|
exit() {
|
||||||
|
this.teams.forEach(team => this.clearTeam(team));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --------------- ACTIONS --------------- //
|
||||||
|
|
||||||
|
updateLocation(team, coords) {
|
||||||
|
team.updateLocation(coords);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --------------- OTHER --------------- //
|
||||||
|
|
||||||
|
canGameStart() {
|
||||||
|
return this.teams.size >= 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
37
server/traque-back/src/core/states/game/finished_state.js
Normal file
37
server/traque-back/src/core/states/game/finished_state.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { FinishedTeam } from "@/core/states/teams/finished_team.js";
|
||||||
|
|
||||||
|
export class FinishedState {
|
||||||
|
constructor(teams, zoneManager) {
|
||||||
|
this.teams = teams;
|
||||||
|
this.zoneManager = zoneManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get name () {
|
||||||
|
return "finished";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --------------- LIFE CYCLE --------------- //
|
||||||
|
|
||||||
|
initTeam(team, settings) {
|
||||||
|
team.state = new FinishedTeam(team).init(settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
enter(settings) {
|
||||||
|
this.teams.forEach(team => this.initTeam(team, settings));
|
||||||
|
}
|
||||||
|
|
||||||
|
clearTeam(_team) {}
|
||||||
|
|
||||||
|
exit() {
|
||||||
|
this.teams.forEach(team => this.clearTeam(team));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --------------- ACTIONS --------------- //
|
||||||
|
|
||||||
|
updateLocation(team, coords) {
|
||||||
|
team.updateLocation(coords);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
4
server/traque-back/src/core/states/game/index.js
Normal file
4
server/traque-back/src/core/states/game/index.js
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export { DefaultState } from '@/core/states/game/default_state.js';
|
||||||
|
export { PlacementState } from '@/core/states/game/placement_state.js';
|
||||||
|
export { PlayingState } from '@/core/states/game/playing_state.js';
|
||||||
|
export { FinishedState } from '@/core/states/game/finished_state.js';
|
||||||
42
server/traque-back/src/core/states/game/placement_state.js
Normal file
42
server/traque-back/src/core/states/game/placement_state.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { PlacementTeam } from "@/core/states/teams/placement_team.js";
|
||||||
|
|
||||||
|
export class PlacementState {
|
||||||
|
constructor(teams, zoneManager) {
|
||||||
|
this.teams = teams;
|
||||||
|
this.zoneManager = zoneManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get name () {
|
||||||
|
return "placement";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --------------- LIFE CYCLE --------------- //
|
||||||
|
|
||||||
|
initTeam(team, settings) {
|
||||||
|
team.state = new PlacementTeam(team).init(settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
enter(settings) {
|
||||||
|
this.teams.forEach(team => this.initTeam(team, settings));
|
||||||
|
}
|
||||||
|
|
||||||
|
clearTeam(_team) {}
|
||||||
|
|
||||||
|
exit() {
|
||||||
|
this.teams.forEach(team => this.clearTeam(team));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --------------- ACTIONS --------------- //
|
||||||
|
|
||||||
|
applySettings(settings) {
|
||||||
|
this.teams.forEach(team => team.state.applySettings(settings));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateLocation(team, coords) {
|
||||||
|
team.updateLocation(coords);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
93
server/traque-back/src/core/states/game/playing_state.js
Normal file
93
server/traque-back/src/core/states/game/playing_state.js
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import { PlayingTeam } from "@/core/states/teams/playing_team.js";
|
||||||
|
|
||||||
|
export class PlayingState {
|
||||||
|
constructor(teams, zoneManager) {
|
||||||
|
this.teams = teams;
|
||||||
|
this.zoneManager = zoneManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get name () {
|
||||||
|
return "playing";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --------------- LIFE CYCLE --------------- //
|
||||||
|
|
||||||
|
initTeam(team, settings) {
|
||||||
|
team.state = new PlayingTeam(team, this.zoneManager).init(settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
enter(settings) {
|
||||||
|
this.teams.forEach(team => this.initTeam(team, settings));
|
||||||
|
this.zoneManager.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
clearTeam(team) {
|
||||||
|
team.state.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
exit() {
|
||||||
|
this.teams.forEach(team => this.clearTeam(team));
|
||||||
|
this.zoneManager.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --------------- ACTIONS --------------- //
|
||||||
|
|
||||||
|
applySettings(settings) {
|
||||||
|
this.teams.forEach(team => team.state.applySettings(settings));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
eliminate(team) {
|
||||||
|
return team.state.eliminate();
|
||||||
|
}
|
||||||
|
|
||||||
|
revive(team) {
|
||||||
|
return team.state.revive();
|
||||||
|
}
|
||||||
|
|
||||||
|
addHandicap(team) {
|
||||||
|
return team.state.addHandicap();
|
||||||
|
}
|
||||||
|
|
||||||
|
clearHandicap(team) {
|
||||||
|
return team.state.clearHandicap();
|
||||||
|
}
|
||||||
|
|
||||||
|
scan(team) {
|
||||||
|
return team.state.scan(this.getTarget(team));
|
||||||
|
}
|
||||||
|
|
||||||
|
capture(team, captureCode) {
|
||||||
|
return team.state.capture(this.getTarget(team), captureCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateLocation(team, coords) {
|
||||||
|
team.updateLocation(coords);
|
||||||
|
return team.state.updateIsInZone();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --------------- OTHER --------------- //
|
||||||
|
|
||||||
|
get _playingTeams() {
|
||||||
|
return this.teams.order.filter(team => !team.state.isEliminated);
|
||||||
|
}
|
||||||
|
|
||||||
|
getHunter(team) {
|
||||||
|
const length = this._playingTeams.length;
|
||||||
|
const i = this.teams.order.indexOf(team.id);
|
||||||
|
return this._playingTeams[(i+length-1) % length];
|
||||||
|
}
|
||||||
|
|
||||||
|
getTarget(team) {
|
||||||
|
const length = this._playingTeams.length;
|
||||||
|
const i = this.teams.order.indexOf(team.id);
|
||||||
|
return this._playingTeams[(i+1) % length];
|
||||||
|
}
|
||||||
|
|
||||||
|
isGameOver() {
|
||||||
|
return this._playingTeams.length <= 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
14
server/traque-back/src/core/states/teams/default_team.js
Normal file
14
server/traque-back/src/core/states/teams/default_team.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
export class DefaultTeam {
|
||||||
|
constructor(team) {
|
||||||
|
this.team = team;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --------------- LIFE CYCLE --------------- //
|
||||||
|
|
||||||
|
init(_settings) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {}
|
||||||
|
}
|
||||||
14
server/traque-back/src/core/states/teams/finished_team.js
Normal file
14
server/traque-back/src/core/states/teams/finished_team.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
export class FinishedTeam {
|
||||||
|
constructor(team) {
|
||||||
|
this.team = team;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --------------- LIFE CYCLE --------------- //
|
||||||
|
|
||||||
|
init(_settings) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {}
|
||||||
|
}
|
||||||
25
server/traque-back/src/core/states/teams/placement_team.js
Normal file
25
server/traque-back/src/core/states/teams/placement_team.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { settingsToZone } from "@/core/factories/placement_zone_factory.js";
|
||||||
|
|
||||||
|
export class PlacementTeam {
|
||||||
|
constructor(team) {
|
||||||
|
this.team = team;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --------------- LIFE CYCLE --------------- //
|
||||||
|
|
||||||
|
init(settings) {
|
||||||
|
this.placementZone = settingsToZone(settings.placementZones[this.team.id]);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {}
|
||||||
|
|
||||||
|
|
||||||
|
// --------------- ACTIONS --------------- //
|
||||||
|
|
||||||
|
applySettings(settings) {
|
||||||
|
this.placementZone = settingsToZone(settings.placementZones[this.team.id]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
100
server/traque-back/src/core/states/teams/playing_team.js
Normal file
100
server/traque-back/src/core/states/teams/playing_team.js
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import { Team } from "@/core/models/team.js";
|
||||||
|
import { ScheduledTask } from "@/util/scheduler.js";
|
||||||
|
import { RESTART_TIMERS } from "@/config/game.js";
|
||||||
|
|
||||||
|
export class PlayingTeam {
|
||||||
|
constructor(team, zoneManager) {
|
||||||
|
this.team = team;
|
||||||
|
this.zoneManager = zoneManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --------------- LIFE CYCLE --------------- //
|
||||||
|
|
||||||
|
init(settings) {
|
||||||
|
// Team
|
||||||
|
this.scanLocation = this.team.location;
|
||||||
|
this.captureCode = Team.getNewCaptureCode();
|
||||||
|
// Booleans
|
||||||
|
this.isEliminated = false;
|
||||||
|
this.isOutOfZone = false;
|
||||||
|
this.hasHandicap = false;
|
||||||
|
// Scheduled taks
|
||||||
|
this.scheduledScan = new ScheduledTask(() => this.scan(), settings.scanDelay).start();
|
||||||
|
this.scheduledHandicap = new ScheduledTask(() => this.addHandicap(), settings.outOfZoneDelay);
|
||||||
|
// Target
|
||||||
|
this.targetScanLocation = { coords: null, timesptamp: null };
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.scheduledScan.interrupt();
|
||||||
|
this.scheduledHandicap.interrupt();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --------------- ACTIONS --------------- //
|
||||||
|
|
||||||
|
applySettings(settings) {
|
||||||
|
this.scheduledScan.setDelay(settings.scanDelay, RESTART_TIMERS);
|
||||||
|
this.scheduledHandicap.setDelay(settings.outOfZoneDelay, RESTART_TIMERS);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
eliminate() {
|
||||||
|
if (this.isEliminated) return false;
|
||||||
|
this.clear();
|
||||||
|
this.isEliminated = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
revive() {
|
||||||
|
if (!this.isEliminated) return false;
|
||||||
|
this.init();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
addHandicap() {
|
||||||
|
if (this.hasHandicap) return false;
|
||||||
|
this.hasHandicap = true;
|
||||||
|
this.scheduledScan.interrupt();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearHandicap() {
|
||||||
|
if (!this.hasHandicap) return false;
|
||||||
|
this.hasHandicap = false;
|
||||||
|
this.scheduledScan.start();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
scan(target) {
|
||||||
|
if (this.hasHandicap || this.isEliminated) return false;
|
||||||
|
this.scanLocation = this.team.location;
|
||||||
|
this.targetScanLocation = target.state.scanLocation;
|
||||||
|
this.scheduledScan.start();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
capture(target, captureCode) {
|
||||||
|
if (this.hasHandicap || this.isEliminated) return false;
|
||||||
|
if (captureCode != target.state.captureCode) return false;
|
||||||
|
target.state.eliminate();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateLocation() {
|
||||||
|
const isOutOfZone = !this.zoneManager.isInZone(this.team.location);
|
||||||
|
// Exit zone case
|
||||||
|
if (isOutOfZone && !this.isOutOfZone) {
|
||||||
|
this.isOutOfZone = true;
|
||||||
|
this.scheduledHandicap.start();
|
||||||
|
// Enter zone case
|
||||||
|
} else if (!isOutOfZone && this.isOutOfZone) {
|
||||||
|
this.isOutOfZone = false;
|
||||||
|
this.scheduledHandicap.interrupt();
|
||||||
|
this.clearHandicap();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,242 +0,0 @@
|
|||||||
import { haversineDistance, EARTH_RADIUS } from "./util.js";
|
|
||||||
|
|
||||||
|
|
||||||
/* -------------------------------- Useful functions and constants -------------------------------- */
|
|
||||||
|
|
||||||
const ZONE_TYPES = {
|
|
||||||
CIRCLE: "circle",
|
|
||||||
POLYGON: "polygon"
|
|
||||||
}
|
|
||||||
|
|
||||||
function latlngEqual(latlng1, latlng2, epsilon = 1e-9) {
|
|
||||||
return Math.abs(latlng1.lat - latlng2.lat) < epsilon && Math.abs(latlng1.lng - latlng2.lng) < epsilon;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* -------------------------------- Circle zones -------------------------------- */
|
|
||||||
|
|
||||||
const defaultCircleSettings = {type: ZONE_TYPES.CIRCLE, min: null, max: null, reductionCount: 4, duration: 10}
|
|
||||||
|
|
||||||
function circleZone(center, radius, duration) {
|
|
||||||
return {
|
|
||||||
type: ZONE_TYPES.CIRCLE,
|
|
||||||
center: center,
|
|
||||||
radius: radius,
|
|
||||||
duration: duration,
|
|
||||||
|
|
||||||
isInZone(location) {
|
|
||||||
return haversineDistance(center, location) < this.radius;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function circleSettingsToZones(settings) {
|
|
||||||
const {min, max, reductionCount, duration} = settings;
|
|
||||||
|
|
||||||
if (!min || !max) 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;
|
|
||||||
let center = max.center;
|
|
||||||
let radius = max.radius;
|
|
||||||
|
|
||||||
for (let i = 1; i < reductionCount; i++) {
|
|
||||||
radius -= radiusReductionLength;
|
|
||||||
let new_center = null;
|
|
||||||
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;
|
|
||||||
const lon0Rad = center.lng * Math.PI / 180;
|
|
||||||
const latRad = Math.asin(
|
|
||||||
Math.sin(lat0Rad) * Math.cos(angularDistance) +
|
|
||||||
Math.cos(lat0Rad) * Math.sin(angularDistance) * Math.cos(angle)
|
|
||||||
);
|
|
||||||
|
|
||||||
const lonRad = lon0Rad + Math.atan2(
|
|
||||||
Math.sin(angle) * Math.sin(angularDistance) * Math.cos(lat0Rad),
|
|
||||||
Math.cos(angularDistance) - Math.sin(lat0Rad) * Math.sin(latRad)
|
|
||||||
);
|
|
||||||
new_center = {lat: latRad * 180 / Math.PI, lng: lonRad * 180 / Math.PI};
|
|
||||||
}
|
|
||||||
center = new_center;
|
|
||||||
zones.push(circleZone(center, radius, duration))
|
|
||||||
}
|
|
||||||
zones.push(circleZone(min.center, min.radius, 0));
|
|
||||||
|
|
||||||
return zones;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* -------------------------------- Polygon zones -------------------------------- */
|
|
||||||
|
|
||||||
const defaultPolygonSettings = {type: ZONE_TYPES.POLYGON, polygons: []}
|
|
||||||
|
|
||||||
function polygonZone(polygon, duration) {
|
|
||||||
return {
|
|
||||||
type: ZONE_TYPES.POLYGON,
|
|
||||||
polygon: polygon,
|
|
||||||
duration: duration,
|
|
||||||
|
|
||||||
isInZone(location) {
|
|
||||||
const {lat: x, lng: y} = location;
|
|
||||||
let inside = false;
|
|
||||||
|
|
||||||
for (let i = 0, j = this.polygon.length - 1; i < this.polygon.length; j = i++) {
|
|
||||||
const {lat: xi, lng: yi} = this.polygon[i];
|
|
||||||
const {lat: xj, lng: yj} = this.polygon[j];
|
|
||||||
|
|
||||||
const intersects = ((yi > y) !== (yj > y)) && (x < ((xj - xi) * (y - yi)) / (yj - yi) + xi);
|
|
||||||
|
|
||||||
if (intersects) inside = !inside;
|
|
||||||
}
|
|
||||||
|
|
||||||
return inside;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function mergePolygons(poly1, poly2) {
|
|
||||||
// poly1 and poly2 are clockwise, not overlaping and touching polygons. If those two polygons were on a map, they would be
|
|
||||||
// one against each other, and the merge would make a new clockwise polygon out of the outer border of the two polygons.
|
|
||||||
// If it happens that poly1 and poly2 are not touching, poly1 would be returned untouched.
|
|
||||||
// Basically because polygons are clockwise, the alogorithm starts from a point A in poly1 not shared by poly2, and
|
|
||||||
// when a point is shared by poly1 and poly2, the algorithm continues in poly2, and so on until point A.
|
|
||||||
|
|
||||||
const getPointIndex = (point, array) => {
|
|
||||||
for (let i = 0; i < array.length; i++) {
|
|
||||||
if (latlngEqual(array[i], point)) return i;
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the index of the first point of poly1 that doesn't belong to merge (it exists)
|
|
||||||
let i = 0;
|
|
||||||
while (getPointIndex(poly1[i], poly2) != -1) i++;
|
|
||||||
// Starting the merge from that point
|
|
||||||
const merge = [poly1[i]];
|
|
||||||
i = (i + 1) % poly1.length;
|
|
||||||
let currentArray = poly1;
|
|
||||||
let otherArray = poly2;
|
|
||||||
while (!latlngEqual(currentArray[i], merge[0])) {
|
|
||||||
const j = getPointIndex(currentArray[i], otherArray);
|
|
||||||
if (j != -1) {
|
|
||||||
[currentArray, otherArray] = [otherArray, currentArray];
|
|
||||||
i = j;
|
|
||||||
}
|
|
||||||
merge.push(currentArray[i]);
|
|
||||||
i = (i + 1) % currentArray.length;
|
|
||||||
}
|
|
||||||
return merge;
|
|
||||||
}
|
|
||||||
|
|
||||||
function polygonSettingsToZones(settings) {
|
|
||||||
const {polygons} = settings;
|
|
||||||
|
|
||||||
const zones = [];
|
|
||||||
|
|
||||||
for (const { polygon, duration } of polygons.slice().reverse()) {
|
|
||||||
const length = zones.length;
|
|
||||||
|
|
||||||
if (length == 0) {
|
|
||||||
zones.push(polygonZone(
|
|
||||||
polygon,
|
|
||||||
duration
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
zones.push(polygonZone(
|
|
||||||
mergePolygons(zones[length-1].polygon, polygon),
|
|
||||||
duration
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return zones.slice().reverse();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* -------------------------------- Zone manager -------------------------------- */
|
|
||||||
|
|
||||||
export default {
|
|
||||||
isRunning: false,
|
|
||||||
zones: [], // A zone has to be connected space that doesn't contain an earth pole
|
|
||||||
currentZone: null,
|
|
||||||
settings: defaultPolygonSettings,
|
|
||||||
|
|
||||||
start() {
|
|
||||||
if (this.isRunning) return;
|
|
||||||
this.isRunning = true;
|
|
||||||
this.currentZone = { id: -1, timeoutId: null, endDate: null };
|
|
||||||
this.goNextZone();
|
|
||||||
},
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
if (!this.isRunning) return;
|
|
||||||
clearTimeout(this.currentZone.timeoutId);
|
|
||||||
this.isRunning = false;
|
|
||||||
this.currentZone = null;
|
|
||||||
},
|
|
||||||
|
|
||||||
goNextZone() {
|
|
||||||
if (!this.isRunning) return;
|
|
||||||
this.currentZone.id++;
|
|
||||||
if (this.currentZone.id >= this.zones.length - 1) {
|
|
||||||
this.currentZone.endDate = Date.now();
|
|
||||||
} else {
|
|
||||||
this.currentZone.timeoutId = setTimeout(() => this.goNextZone(), this.getCurrentZone().duration * 60 * 1000);
|
|
||||||
this.currentZone.endDate = Date.now() + this.getCurrentZone().duration * 60 * 1000;
|
|
||||||
}
|
|
||||||
this.zoneBroadcast();
|
|
||||||
},
|
|
||||||
|
|
||||||
getCurrentZone() {
|
|
||||||
if (!this.isRunning) return null;
|
|
||||||
return this.zones[this.currentZone.id];
|
|
||||||
},
|
|
||||||
|
|
||||||
getNextZone() {
|
|
||||||
if (!this.isRunning) return null;
|
|
||||||
if (this.currentZone.id + 1 < this.zones.length) {
|
|
||||||
return this.zones[this.currentZone.id + 1];
|
|
||||||
} else {
|
|
||||||
return this.zones[this.currentZone.id];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
isInZone(location) {
|
|
||||||
if (!this.isRunning) return false;
|
|
||||||
if (this.zones.length == 0) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return this.getCurrentZone().isInZone(location);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
changeSettings(settings) {
|
|
||||||
switch (settings.type) {
|
|
||||||
case ZONE_TYPES.CIRCLE:
|
|
||||||
this.zones = circleSettingsToZones(settings);
|
|
||||||
break;
|
|
||||||
case ZONE_TYPES.POLYGON:
|
|
||||||
this.zones = polygonSettingsToZones(settings);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
this.zones = [];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
this.settings = settings;
|
|
||||||
this.stop();
|
|
||||||
this.start();
|
|
||||||
this.zoneBroadcast();
|
|
||||||
},
|
|
||||||
|
|
||||||
zoneBroadcast() {
|
|
||||||
if (!this.isRunning) return;
|
|
||||||
const zone = {
|
|
||||||
begin: this.getCurrentZone(),
|
|
||||||
end: this.getNextZone(),
|
|
||||||
endDate:this.currentZone.endDate,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
}
|
|
||||||
65
server/traque-back/src/externals/api/photo.js
vendored
Normal file
65
server/traque-back/src/externals/api/photo.js
vendored
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import multer from "multer";
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
export class PhotoService {
|
||||||
|
constructor(gameManager) {
|
||||||
|
this.gameManager = gameManager;
|
||||||
|
this.uploadDir = path.join(process.cwd(), "teams_photos");
|
||||||
|
this.missingImage = path.join(process.cwd(), "assets", "images", "missing_image.jpg");
|
||||||
|
this.allowedMime = ["image/png", "image/jpeg", "image/gif"];
|
||||||
|
}
|
||||||
|
|
||||||
|
_initStorage() {
|
||||||
|
if (fs.existsSync(this.uploadDir)) fs.rmSync(this.uploadDir, { recursive: true });
|
||||||
|
fs.mkdirSync(this.uploadDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
_setupMulter() {
|
||||||
|
const storage = multer.diskStorage({
|
||||||
|
destination: (req, file, cb) => cb(null, this.uploadDir),
|
||||||
|
filename: (req, file, cb) => cb(null, `${req.query.team}`)
|
||||||
|
});
|
||||||
|
|
||||||
|
this.upload = multer({
|
||||||
|
storage,
|
||||||
|
fileFilter: (req, file, cb) => {
|
||||||
|
const isAllowed = this.allowedMime.includes(file.mimetype);
|
||||||
|
const teamExists = this.gameManager.teams.has(req.query.team);
|
||||||
|
cb(null, isAllowed && teamExists);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_sendTeamImage(res, imageId) {
|
||||||
|
const imagePath = path.join(this.uploadDir, imageId);
|
||||||
|
res.set({
|
||||||
|
"Content-Type": "image/png",
|
||||||
|
"Access-Control-Allow-Origin": "*"
|
||||||
|
});
|
||||||
|
res.sendFile(fs.existsSync(imagePath) ? imagePath : this.missingImage);
|
||||||
|
}
|
||||||
|
|
||||||
|
init(app) {
|
||||||
|
this._initStorage();
|
||||||
|
this._setupMulter();
|
||||||
|
|
||||||
|
app.post("/upload", this.upload.single('file'), (req, res) => {
|
||||||
|
res.set("Access-Control-Allow-Origin", "*").send("");
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/photo/my", (req, res) => {
|
||||||
|
const team = this.gameManager.teams.get(req.query.team);
|
||||||
|
if (!team) return res.status(400).send("Team not found");
|
||||||
|
this._sendTeamImage(res, team.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/photo/enemy", (req, res) => {
|
||||||
|
const team = this.gameManager.teams.get(req.query.team);
|
||||||
|
if (!team) return res.status(400).send("Team not found");
|
||||||
|
this._sendTeamImage(res, team.target.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
79
server/traque-back/src/externals/handlers/adminHandler.js
vendored
Normal file
79
server/traque-back/src/externals/handlers/adminHandler.js
vendored
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import { createHash } from "crypto";
|
||||||
|
import { ADMIN_PASSWORD_HASH } from "@/config/server.js";
|
||||||
|
import { ADMIN_HANDLER_EVENTS } from "@/config/events.js";
|
||||||
|
|
||||||
|
export class AdminHandler {
|
||||||
|
constructor(gameManager) {
|
||||||
|
this.gameManager = gameManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
init(io) {
|
||||||
|
io.on("connection", (socket) => {
|
||||||
|
console.log("Connection of an admin");
|
||||||
|
new AdminConnection(socket, this.gameManager);
|
||||||
|
});
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AdminConnection {
|
||||||
|
constructor(socket, gameManager) {
|
||||||
|
this._socket = socket;
|
||||||
|
this._gameManager = gameManager;
|
||||||
|
this._isLoggedIn = false;
|
||||||
|
this._setupListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
_login(password) {
|
||||||
|
if (this._isLoggedIn) return;
|
||||||
|
|
||||||
|
const hash = createHash('sha256').update(password).digest('hex');
|
||||||
|
if (hash !== ADMIN_PASSWORD_HASH) return false;
|
||||||
|
|
||||||
|
this._isLoggedIn = true;
|
||||||
|
this._gameManager.onAdminLogin(this._socket.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logout() {
|
||||||
|
if (!this._isLoggedIn) return;
|
||||||
|
this._isLoggedIn = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_setupListeners() {
|
||||||
|
|
||||||
|
// Authentication
|
||||||
|
|
||||||
|
this._socket.on("disconnect", () => {
|
||||||
|
console.log("Disconnection of an admin");
|
||||||
|
this._logout()
|
||||||
|
});
|
||||||
|
|
||||||
|
this._socket.on(ADMIN_HANDLER_EVENTS.LOGIN, (password, callback) => {
|
||||||
|
this._login(password);
|
||||||
|
callback(this._isLoggedIn);
|
||||||
|
});
|
||||||
|
|
||||||
|
this._socket.on(ADMIN_HANDLER_EVENTS.LOGOUT, () => {
|
||||||
|
this._logout()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
|
||||||
|
const protectedActions = {
|
||||||
|
[ADMIN_HANDLER_EVENTS.ADD_TEAM]: (id) => this._gameManager.addTeam(id),
|
||||||
|
[ADMIN_HANDLER_EVENTS.REMOVE_TEAM]: (id) => this._gameManager.removeTeam(id),
|
||||||
|
[ADMIN_HANDLER_EVENTS.REORDER_TEAM]: (id) => this._gameManager.reorderTeam(id),
|
||||||
|
[ADMIN_HANDLER_EVENTS.ELIMINATE_TEAM]: (id) => this._gameManager.eliminate(id),
|
||||||
|
[ADMIN_HANDLER_EVENTS.REVIVE_TEAM]: (id) => this._gameManager.revive(id),
|
||||||
|
[ADMIN_HANDLER_EVENTS.STATE]: (state) => this._gameManager.setState(state),
|
||||||
|
[ADMIN_HANDLER_EVENTS.SETTINGS]: (settings) => this._gameManager.setSettings(settings),
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.entries(protectedActions).forEach(([event, action]) => {
|
||||||
|
this._socket.on(event, (data) => {
|
||||||
|
if (this._isLoggedIn) action(data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
80
server/traque-back/src/externals/handlers/playerHandler.js
vendored
Normal file
80
server/traque-back/src/externals/handlers/playerHandler.js
vendored
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import { PLAYER_HANDLER_EVENTS } from "@/config/events.js";
|
||||||
|
|
||||||
|
export class PlayerHandler {
|
||||||
|
constructor(gameManager) {
|
||||||
|
this.gameManager = gameManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
init(io) {
|
||||||
|
io.on("connection", (socket) => {
|
||||||
|
console.log("Connection of a player");
|
||||||
|
new PlayerConnection(socket, this.gameManager);
|
||||||
|
});
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PlayerConnection {
|
||||||
|
constructor(socket, gameManager) {
|
||||||
|
this._socket = socket;
|
||||||
|
this._gameManager = gameManager;
|
||||||
|
this._teamId = null;
|
||||||
|
this._setupListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
_isLoggedIn() {
|
||||||
|
return this._teamId !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_login(loginTeamId) {
|
||||||
|
if (!this._gameManager.teams.has(loginTeamId) || this._teamId === loginTeamId) return;
|
||||||
|
this._logout();
|
||||||
|
this._teamId = loginTeamId;
|
||||||
|
this._socket.join(this._teamId);
|
||||||
|
this._gameManager.onPlayerLogin(this._socket.id, this._teamId);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logout() {
|
||||||
|
if (!this._isLoggedIn()) return;
|
||||||
|
this._socket.leave(this._teamId);
|
||||||
|
this._teamId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_setupListeners() {
|
||||||
|
|
||||||
|
// Authentication
|
||||||
|
|
||||||
|
this._socket.on("disconnect", () => {
|
||||||
|
console.log("Disconnection of a player");
|
||||||
|
this._logout()
|
||||||
|
});
|
||||||
|
|
||||||
|
this._socket.on(PLAYER_HANDLER_EVENTS.LOGIN, (loginTeamId, callback) => {
|
||||||
|
this._login(loginTeamId);
|
||||||
|
callback(this._isLoggedIn());
|
||||||
|
});
|
||||||
|
|
||||||
|
this._socket.on(PLAYER_HANDLER_EVENTS.LOGOUT, () => {
|
||||||
|
this._logout()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
|
||||||
|
this._socket.on(PLAYER_HANDLER_EVENTS.LOCATION, (coords) => {
|
||||||
|
if (this._isLoggedIn()) return;
|
||||||
|
this._gameManager.updateLocation(this._teamId, coords);
|
||||||
|
});
|
||||||
|
|
||||||
|
this._socket.on(PLAYER_HANDLER_EVENTS.SCAN, (coords) => {
|
||||||
|
if (this._isLoggedIn()) return;
|
||||||
|
this._gameManager.updateLocation(this._teamId, coords);
|
||||||
|
this._gameManager.scan(this._teamId);
|
||||||
|
});
|
||||||
|
|
||||||
|
this._socket.on(PLAYER_HANDLER_EVENTS.CAPTURE, (captureCode, callback) => {
|
||||||
|
if (this._isLoggedIn()) return;
|
||||||
|
callback(this._gameManager.capture(this._teamId, captureCode));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
69
server/traque-back/src/externals/mappers/admin_mapper.js
vendored
Normal file
69
server/traque-back/src/externals/mappers/admin_mapper.js
vendored
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { DefaultState, PlacementState, PlayingState, FinishedState } from "@/core/states/game/index.js";
|
||||||
|
|
||||||
|
const TEAM_STATE_MAP = {
|
||||||
|
[DefaultState.name]: (_team, _gameState) => ({}),
|
||||||
|
|
||||||
|
[PlacementState.name]: (team, _gameState) => ({
|
||||||
|
placementZone: team.state.placementZone?.polygon ?? null,
|
||||||
|
isInPlacementZone: team.state.placementZone?.isInZone(team.location) ?? true,
|
||||||
|
}),
|
||||||
|
|
||||||
|
[PlayingState.name]: (team, gameState) => ({
|
||||||
|
// Team
|
||||||
|
captureCode: team.state.captureCode,
|
||||||
|
scanLocation: team.state.scanLocation,
|
||||||
|
// Booleans
|
||||||
|
isEliminated: team.state.isEliminated,
|
||||||
|
isOutOfZone: team.state.isOutOfZone,
|
||||||
|
hasHandicap: team.state.hasHandicap,
|
||||||
|
// Scheduled taks
|
||||||
|
scanDate: team.state.scheduledScan.dateOfExecution,
|
||||||
|
handicapDate: team.state.scheduledHandicap.dateOfExecution,
|
||||||
|
// Target and hunter
|
||||||
|
hunterName: gameState.getHunter(team).name,
|
||||||
|
targetName: gameState.getTarget(team).name,
|
||||||
|
targetScanLocation: team.state.targetScanLocation,
|
||||||
|
}),
|
||||||
|
|
||||||
|
[FinishedState.name]: (_team, _gameState) => ({}),
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export class AdminMapper {
|
||||||
|
constructor(gameManager) {
|
||||||
|
this.gameManager = gameManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
map() {
|
||||||
|
const stateName = this.gameManager.state.name;
|
||||||
|
|
||||||
|
const teamsDto = {};
|
||||||
|
this.gameManager.teams.forEach((team, teamId) => {
|
||||||
|
teamsDto[teamId] = {
|
||||||
|
id: team.id,
|
||||||
|
name: team.name,
|
||||||
|
location: team.location,
|
||||||
|
state: TEAM_STATE_MAP[stateName](team, this.gameManager.state)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const zonesDto = {
|
||||||
|
firstZone: this.gameManager.zoneManager.firstZonePolygon,
|
||||||
|
currentZone: this.gameManager.zoneManager.currentZonePolygon,
|
||||||
|
nextZone: this.gameManager.zoneManager.nextZonePolygon,
|
||||||
|
zoneTransitionDate: this.gameManager.zoneManager.dateOfZoneTransition
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
stateName: stateName,
|
||||||
|
teams: teamsDto,
|
||||||
|
teamsOrder: this.gameManager.teams.order,
|
||||||
|
zones: zonesDto,
|
||||||
|
settings: this.gameManager.settings
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hash(dto) {
|
||||||
|
return JSON.stringify(dto);
|
||||||
|
}
|
||||||
|
};
|
||||||
54
server/traque-back/src/externals/mappers/player_mapper.js
vendored
Normal file
54
server/traque-back/src/externals/mappers/player_mapper.js
vendored
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { DefaultState, PlacementState, PlayingState, FinishedState } from "@/core/states/game/index.js";
|
||||||
|
|
||||||
|
const TEAM_STATE_MAP = {
|
||||||
|
[DefaultState.name]: (_team, _gameState) => ({}),
|
||||||
|
|
||||||
|
[PlacementState.name]: (team, _gameState) => ({
|
||||||
|
placementZone: team.state.placementZone?.polygon ?? null,
|
||||||
|
isInPlacementZone: team.state.placementZone?.isInZone(team.location) ?? true,
|
||||||
|
playingZone: team.state.zoneManager.firstZonePolygon,
|
||||||
|
}),
|
||||||
|
|
||||||
|
[PlayingState.name]: (team, gameState) => ({
|
||||||
|
// Team
|
||||||
|
captureCode: team.state.captureCode,
|
||||||
|
scanLocation: team.state.scanLocation,
|
||||||
|
// Booleans
|
||||||
|
isEliminated: team.state.isEliminated,
|
||||||
|
isOutOfZone: team.state.isOutOfZone,
|
||||||
|
hasHandicap: team.state.hasHandicap,
|
||||||
|
// Scheduled taks
|
||||||
|
scanDate: team.state.scheduledScan.dateOfExecution,
|
||||||
|
handicapDate: team.state.scheduledHandicap.dateOfExecution,
|
||||||
|
// Target
|
||||||
|
targetName: gameState.getTarget(team).name,
|
||||||
|
targetHasHandicap: gameState.getTarget(team).state.hasHandicap,
|
||||||
|
targetScanLocation: team.state.targetScanLocation,
|
||||||
|
// Game zone
|
||||||
|
currentZone: team.state.zoneManager.currentZonePolygon,
|
||||||
|
nextZone: team.state.zoneManager.nextZonePolygon,
|
||||||
|
zoneTransitionDate: team.state.zoneManager.dateOfZoneTransition
|
||||||
|
}),
|
||||||
|
|
||||||
|
[FinishedState.name]: (_team, _gameState) => ({}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export class PlayerMapper {
|
||||||
|
constructor(gameState, team) {
|
||||||
|
this.gameState = gameState;
|
||||||
|
this.team = team;
|
||||||
|
}
|
||||||
|
|
||||||
|
map() {
|
||||||
|
return {
|
||||||
|
id: this.team.id,
|
||||||
|
name: this.team.name,
|
||||||
|
stateName: this.gameState.name,
|
||||||
|
state: TEAM_STATE_MAP[this.gameState.name](this.team, this.gameState)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
hash(dto) {
|
||||||
|
return JSON.stringify(dto);
|
||||||
|
}
|
||||||
|
};
|
||||||
24
server/traque-back/src/externals/synchronizers/admin_synchronizer.js
vendored
Normal file
24
server/traque-back/src/externals/synchronizers/admin_synchronizer.js
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { AdminMapper } from "@/externals/mappers/admin_mapper.js";
|
||||||
|
import { StateTracker } from "@/util/state_tracker.js";
|
||||||
|
import { GAME_MANAGER_EVENTS, ADMIN_SYNCHRONIZER_EVENTS } from "@/config/events.js";
|
||||||
|
|
||||||
|
export class AdminSynchronizer {
|
||||||
|
constructor(gameManager) {
|
||||||
|
this.gameManager = gameManager;
|
||||||
|
this.gameStateTracker = new StateTracker(new AdminMapper(this.gameManager));
|
||||||
|
}
|
||||||
|
|
||||||
|
init(io) {
|
||||||
|
this.gameManager.on(GAME_MANAGER_EVENTS.INIT_ADMIN, (socketId) => {
|
||||||
|
const { dto } = this.gameStateTracker.getSyncDto();
|
||||||
|
io.to(socketId).emit(ADMIN_SYNCHRONIZER_EVENTS.UPDATE_FULL, dto);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.gameManager.on(GAME_MANAGER_EVENTS.UPDATE_GAME, () => {
|
||||||
|
const { dto, hasChanged } = this.gameStateTracker.getSyncDto();
|
||||||
|
if (hasChanged) io.emit(ADMIN_SYNCHRONIZER_EVENTS.UPDATE_FULL, dto);
|
||||||
|
});
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
39
server/traque-back/src/externals/synchronizers/player_synchronizer.js
vendored
Normal file
39
server/traque-back/src/externals/synchronizers/player_synchronizer.js
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { GAME_MANAGER_EVENTS, PLAYER_SYNCHRONIZER_EVENTS } from "@/config/events.js";
|
||||||
|
import { PlayerMapper } from "@/externals/mappers/player_mapper.js";
|
||||||
|
import { StateTracker } from "@/util/state_tracker.js";
|
||||||
|
|
||||||
|
export class PlayerSynchronizer {
|
||||||
|
constructor(gameManager) {
|
||||||
|
this.gameManager = gameManager;
|
||||||
|
this.teamsStateTracker = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
init(io) {
|
||||||
|
this.gameManager.on(GAME_MANAGER_EVENTS.INIT_PLAYER, (socketId, teamId) => {
|
||||||
|
const { dto } = this._getSyncDtoOfTeam(this.gameManager.teams.get(teamId));
|
||||||
|
io.to(socketId).emit(PLAYER_SYNCHRONIZER_EVENTS.UPDATE_FULL, dto);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.gameManager.on(GAME_MANAGER_EVENTS.UPDATE_GAME, () => {
|
||||||
|
this.gameManager.teams.forEach((team, teamId) => {
|
||||||
|
const { dto, hasChanged } = this._getSyncDtoOfTeam(team);
|
||||||
|
if (hasChanged) io.to(teamId).emit(PLAYER_SYNCHRONIZER_EVENTS.UPDATE_FULL, dto);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
this.gameManager.on(GAME_MANAGER_EVENTS.DELETE_TEAM, (teamId) => {
|
||||||
|
this.teamsStateTracker.delete(teamId);
|
||||||
|
io.to(teamId).emit(PLAYER_SYNCHRONIZER_EVENTS.LOGOUT);
|
||||||
|
});
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
_getSyncDtoOfTeam(team) {
|
||||||
|
if (!this.teamsStateTracker.has(team.id)) {
|
||||||
|
const mapper = new PlayerMapper(this.gameManager.state, team);
|
||||||
|
this.teamsStateTracker.set(team.id, new StateTracker(mapper));
|
||||||
|
}
|
||||||
|
return this.teamsStateTracker.get(team.id).getSyncDto();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,24 +1,40 @@
|
|||||||
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 { initAdminSocketHandler } from "@/socket/adminHandler.js";
|
// Core
|
||||||
import { initPlayerSocketHandler } from "@/socket/playerHandler.js";
|
import { GameManager } from "@/core/managers/game_manager.js";
|
||||||
import { initPhotoUpload } from "./services/photo.js";
|
// Externals
|
||||||
import { PORT, HOST } from "@/util/util.js";
|
import { PhotoService } from "@/externals/api/photo.js";
|
||||||
|
import { PlayerSynchronizer } from "@/externals/synchronizers/player_synchronizer.js";
|
||||||
|
import { PlayerHandler } from "@/externals/handlers/playerHandler.js";
|
||||||
|
import { AdminSynchronizer } from "@/externals/synchronizers/admin_synchronizer.js";
|
||||||
|
import { AdminHandler } from "@/externals/handlers/adminHandler.js";
|
||||||
|
// Config
|
||||||
|
import { PORT, HOST } from "@/config/server.js";
|
||||||
|
import { DEFAULT_GAME_SETTINGS, STATE_SETTINGS } from "@/config/game.js";
|
||||||
|
|
||||||
// --- Configuration ---
|
|
||||||
|
// Configuration
|
||||||
const app = express();
|
const app = express();
|
||||||
const httpServer = createServer(app);
|
const httpServer = createServer(app);
|
||||||
const io = new Server(httpServer, {
|
const io = new Server(httpServer, {
|
||||||
cors: { origin: "*", methods: ["GET", "POST"] }
|
cors: { origin: "*", methods: ["GET", "POST"] }
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Initialization ---
|
|
||||||
initPhotoUpload(app);
|
|
||||||
initAdminSocketHandler(io);
|
|
||||||
initPlayerSocketHandler(io);
|
|
||||||
|
|
||||||
// --- Server Start ---
|
// Initialization
|
||||||
|
const gameManager = new GameManager(STATE_SETTINGS, DEFAULT_GAME_SETTINGS);
|
||||||
|
|
||||||
|
new PhotoService(gameManager).init(app);
|
||||||
|
|
||||||
|
new PlayerHandler(gameManager).init(io.of("player"));
|
||||||
|
new AdminHandler(gameManager).init(io.of("admin"));
|
||||||
|
|
||||||
|
new PlayerSynchronizer(gameManager).init(io.of("player"));
|
||||||
|
new AdminSynchronizer(gameManager).init(io.of("admin"));
|
||||||
|
|
||||||
|
|
||||||
|
// 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}`);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,98 +0,0 @@
|
|||||||
import { createHash } from "crypto";
|
|
||||||
import { config } from "dotenv";
|
|
||||||
import game from "./game.js"
|
|
||||||
import zoneManager from "./zone_manager.js"
|
|
||||||
|
|
||||||
config();
|
|
||||||
|
|
||||||
const ADMIN_PASSWORD_HASH = process.env.ADMIN_PASSWORD_HASH;
|
|
||||||
|
|
||||||
export function secureAdminBroadcast(event, data) {
|
|
||||||
loggedInSockets.forEach(s => {
|
|
||||||
io.of("admin").to(s).emit(event, data);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let loggedInSockets = [];
|
|
||||||
|
|
||||||
export function initAdminSocketHandler(io) {
|
|
||||||
io.of("admin").on("connection", (socket) => {
|
|
||||||
console.log("Connection of an admin");
|
|
||||||
let loggedIn = false;
|
|
||||||
|
|
||||||
const login = (password) => {
|
|
||||||
if (loggedIn) return false;
|
|
||||||
if (createHash('sha256').update(password).digest('hex') !== ADMIN_PASSWORD_HASH) return false;
|
|
||||||
loggedInSockets.push(socket.id);
|
|
||||||
loggedIn = true;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const logout = () => {
|
|
||||||
if (!loggedIn) return false;
|
|
||||||
loggedInSockets = loggedInSockets.filter(s => s !== socket.id);
|
|
||||||
loggedIn = false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.on("disconnect", () => {
|
|
||||||
console.log("Disconnection of an admin");
|
|
||||||
logout();
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("logout", () => {
|
|
||||||
logout();
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("login", (password) => {
|
|
||||||
if (!login(password)) return;
|
|
||||||
socket.emit("teams", game.teams);
|
|
||||||
socket.emit("game_state", {
|
|
||||||
state: game.state,
|
|
||||||
date: game.startDate
|
|
||||||
});
|
|
||||||
socket.emit("current_zone", {
|
|
||||||
begin: zoneManager.getCurrentZone(),
|
|
||||||
end: zoneManager.getNextZone(),
|
|
||||||
endDate: zoneManager.currentZone?.endDate,
|
|
||||||
});
|
|
||||||
socket.emit("settings", game.getAdminSettings());
|
|
||||||
socket.emit("login_response", true);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("add_team", (teamName) => {
|
|
||||||
if (!loggedIn) return;
|
|
||||||
game.addTeam(teamName);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("remove_team", (teamId) => {
|
|
||||||
if (!loggedIn) return;
|
|
||||||
game.removeTeam(teamId);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("reorder_teams", (newOrder) => {
|
|
||||||
if (!loggedIn) return;
|
|
||||||
game.reorderTeams(newOrder);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("capture_team", (teamId) => {
|
|
||||||
if (!loggedIn) return;
|
|
||||||
game.switchCapturedTeam(teamId);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("placement_team", (teamId, placementZone) => {
|
|
||||||
if (!loggedIn) return;
|
|
||||||
game.placementTeam(teamId, placementZone);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("change_state", (state) => {
|
|
||||||
if (!loggedIn) return;
|
|
||||||
game.setState(state);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("update_settings", (settings) => {
|
|
||||||
if (!loggedIn) return;
|
|
||||||
game.changeSettings(settings);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,504 +0,0 @@
|
|||||||
import { secureAdminBroadcast } from "./admin_socket.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";
|
|
||||||
|
|
||||||
function randint(max) {
|
|
||||||
return Math.floor(Math.random() * max);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDistanceFromLatLon({ lat: lat1, lng: lon1 }, { lat: lat2, lng: lon2 }) {
|
|
||||||
const degToRad = (deg) => deg * (Math.PI / 180);
|
|
||||||
var R = 6371; // Radius of the earth in km
|
|
||||||
var dLat = degToRad(lat2 - lat1);
|
|
||||||
var dLon = degToRad(lon2 - lon1);
|
|
||||||
var 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)
|
|
||||||
;
|
|
||||||
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
||||||
var d = R * c; // Distance in km
|
|
||||||
return d * 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isInCircle(position, center, radius) {
|
|
||||||
return getDistanceFromLatLon(position, center) < radius;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const GameState = {
|
|
||||||
SETUP: "setup",
|
|
||||||
PLACEMENT: "placement",
|
|
||||||
PLAYING: "playing",
|
|
||||||
FINISHED: "finished"
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
// List of teams, as objects. To see the fields see the addTeam method
|
|
||||||
teams: [],
|
|
||||||
// Current state of the game
|
|
||||||
state: GameState.SETUP,
|
|
||||||
// Date since the state changed
|
|
||||||
startDate: null,
|
|
||||||
// Messages
|
|
||||||
messages: {
|
|
||||||
waiting: "",
|
|
||||||
captured: "",
|
|
||||||
winner: "",
|
|
||||||
loser: "",
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* ------------------------------- USEFUL FUNCTIONS ------------------------------- */
|
|
||||||
|
|
||||||
getNewTeamId() {
|
|
||||||
let id = randint(1_000_000);
|
|
||||||
while (this.teams.find(t => t.id === id)) id = randint(1_000_000);
|
|
||||||
return id.toString().padStart(6, '0');
|
|
||||||
},
|
|
||||||
|
|
||||||
checkEndGame() {
|
|
||||||
if (this.teams.filter(team => !team.captured).length <= 2) this.setState(GameState.FINISHED);
|
|
||||||
},
|
|
||||||
|
|
||||||
updateChasingChain() {
|
|
||||||
const playingTeams = this.teams.filter(team => !team.captured);
|
|
||||||
|
|
||||||
for (let i = 0; i < playingTeams.length; i++) {
|
|
||||||
playingTeams[i].chasing = playingTeams[(i+1) % playingTeams.length].id;
|
|
||||||
playingTeams[i].chased = playingTeams[(playingTeams.length + i-1) % playingTeams.length].id;
|
|
||||||
sendUpdatedTeamInformations(playingTeams[i].id);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
initLastSentLocations() {
|
|
||||||
const dateNow = Date.now();
|
|
||||||
// Update of lastSentLocation
|
|
||||||
for (const team of this.teams) {
|
|
||||||
team.lastSentLocation = team.currentLocation;
|
|
||||||
team.locationSendDeadline = dateNow + sendPositionTimeouts.delay * 60 * 1000;
|
|
||||||
sendPositionTimeouts.set(team.id);
|
|
||||||
sendUpdatedTeamInformations(team.id);
|
|
||||||
}
|
|
||||||
// Update of enemyLocation now we have the lastSentLocation of the enemy
|
|
||||||
for (const team of this.teams) {
|
|
||||||
team.enemyLocation = this.getTeam(team.chasing)?.lastSentLocation;
|
|
||||||
sendUpdatedTeamInformations(team.id);
|
|
||||||
}
|
|
||||||
// Broadcast new infos
|
|
||||||
secureAdminBroadcast("teams", this.teams);
|
|
||||||
},
|
|
||||||
|
|
||||||
resetTeamsInfos() {
|
|
||||||
for (const team of this.teams) {
|
|
||||||
// Chasing
|
|
||||||
team.captured = false;
|
|
||||||
team.chasing = null;
|
|
||||||
team.chased = null;
|
|
||||||
// Locations
|
|
||||||
team.lastSentLocation = null;
|
|
||||||
team.locationSendDeadline = null;
|
|
||||||
team.enemyLocation = null;
|
|
||||||
// Placement
|
|
||||||
team.ready = false;
|
|
||||||
// Zone
|
|
||||||
team.outOfZone = false;
|
|
||||||
team.outOfZoneDeadline = null;
|
|
||||||
team.hasHandicap = false;
|
|
||||||
// Stats
|
|
||||||
team.distance = 0;
|
|
||||||
team.nCaptures = 0;
|
|
||||||
team.nSentLocation = 0;
|
|
||||||
team.nObserved = 0;
|
|
||||||
team.finishDate = null;
|
|
||||||
sendUpdatedTeamInformations(team.id);
|
|
||||||
}
|
|
||||||
this.updateChasingChain();
|
|
||||||
secureAdminBroadcast("teams", this.teams);
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* ------------------------------- STATE AND SETTINGS FUNCTIONS ------------------------------- */
|
|
||||||
|
|
||||||
getAdminSettings() {
|
|
||||||
return {
|
|
||||||
messages: this.messages,
|
|
||||||
zone: zoneManager.settings,
|
|
||||||
sendPositionDelay: sendPositionTimeouts.delay,
|
|
||||||
outOfZoneDelay: outOfZoneTimeouts.delay
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
getPlayerSettings() {
|
|
||||||
return {
|
|
||||||
messages: this.messages,
|
|
||||||
zone: {type: zoneManager.settings.type},
|
|
||||||
sendPositionDelay: sendPositionTimeouts.delay,
|
|
||||||
outOfZoneDelay: outOfZoneTimeouts.delay
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
changeSettings(newSettings) {
|
|
||||||
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);
|
|
||||||
// Broadcast new infos
|
|
||||||
secureAdminBroadcast("settings", this.getAdminSettings());
|
|
||||||
playersBroadcast("settings", this.getPlayerSettings());
|
|
||||||
},
|
|
||||||
|
|
||||||
setState(newState) {
|
|
||||||
const dateNow = Date.now();
|
|
||||||
if (newState == this.state) return true;
|
|
||||||
switch (newState) {
|
|
||||||
case GameState.SETUP:
|
|
||||||
trajectory.stop();
|
|
||||||
zoneManager.stop();
|
|
||||||
sendPositionTimeouts.clearAll();
|
|
||||||
outOfZoneTimeouts.clearAll();
|
|
||||||
this.resetTeamsInfos();
|
|
||||||
this.startDate = null;
|
|
||||||
break;
|
|
||||||
case GameState.PLACEMENT:
|
|
||||||
if (this.state == GameState.FINISHED || this.teams.length < 3) {
|
|
||||||
secureAdminBroadcast("game_state", {state: this.state, date: this.startDate});
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
trajectory.stop();
|
|
||||||
zoneManager.stop();
|
|
||||||
sendPositionTimeouts.clearAll();
|
|
||||||
outOfZoneTimeouts.clearAll();
|
|
||||||
this.startDate = null;
|
|
||||||
break;
|
|
||||||
case GameState.PLAYING:
|
|
||||||
if (this.state == GameState.FINISHED || this.teams.length < 3) {
|
|
||||||
secureAdminBroadcast("game_state", {state: this.state, date: this.startDate});
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
trajectory.start();
|
|
||||||
zoneManager.start();
|
|
||||||
this.initLastSentLocations();
|
|
||||||
this.startDate = dateNow;
|
|
||||||
break;
|
|
||||||
case GameState.FINISHED:
|
|
||||||
if (this.state != GameState.PLAYING) {
|
|
||||||
secureAdminBroadcast("game_state", {state: this.state, date: this.startDate});
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
trajectory.stop();
|
|
||||||
zoneManager.stop();
|
|
||||||
sendPositionTimeouts.clearAll();
|
|
||||||
outOfZoneTimeouts.clearAll();
|
|
||||||
this.teams.forEach(team => {if (!team.finishDate) team.finishDate = dateNow});
|
|
||||||
secureAdminBroadcast("teams", this.teams);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// Update the state
|
|
||||||
this.state = newState;
|
|
||||||
// Broadcast new infos
|
|
||||||
secureAdminBroadcast("game_state", {state: newState, date: this.startDate});
|
|
||||||
playersBroadcast("game_state", {state: newState, date: this.startDate});
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* ------------------------------- MANAGE PLAYERS FUNCTIONS ------------------------------- */
|
|
||||||
|
|
||||||
addPlayer(teamId, socketId) {
|
|
||||||
// Test of parameters
|
|
||||||
if (!this.hasTeam(teamId)) return false;
|
|
||||||
// Variables
|
|
||||||
const team = this.getTeam(teamId);
|
|
||||||
// Add the player
|
|
||||||
team.sockets.push(socketId);
|
|
||||||
// Broadcast new infos
|
|
||||||
secureAdminBroadcast("teams", this.teams);
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
|
|
||||||
removePlayer(teamId, socketId) {
|
|
||||||
// Test of parameters
|
|
||||||
if (!this.hasTeam(teamId)) return false;
|
|
||||||
// Variables
|
|
||||||
const team = this.getTeam(teamId);
|
|
||||||
// Remove the player and its data
|
|
||||||
if (this.isPlayerCapitain(teamId, socketId)) {
|
|
||||||
team.battery = null;
|
|
||||||
team.phoneModel = null;
|
|
||||||
team.phoneName = null;
|
|
||||||
}
|
|
||||||
team.sockets = team.sockets.filter((sid) => sid != socketId);
|
|
||||||
// Broadcast new infos
|
|
||||||
secureAdminBroadcast("teams", this.teams);
|
|
||||||
sendUpdatedTeamInformations(team.id);
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
|
|
||||||
isPlayerCapitain(teamId, socketId) {
|
|
||||||
return this.getTeam(teamId).sockets.indexOf(socketId) == 0;
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* ------------------------------- MANAGE TEAMS FUNCTIONS ------------------------------- */
|
|
||||||
|
|
||||||
getTeam(teamId) {
|
|
||||||
return this.teams.find(t => t.id === teamId);
|
|
||||||
},
|
|
||||||
|
|
||||||
hasTeam(teamId) {
|
|
||||||
return this.teams.some(t => t.id === teamId);
|
|
||||||
},
|
|
||||||
|
|
||||||
addTeam(teamName) {
|
|
||||||
this.teams.push({
|
|
||||||
// Identification
|
|
||||||
sockets: [],
|
|
||||||
name: teamName,
|
|
||||||
id: this.getNewTeamId(),
|
|
||||||
captureCode: randint(10_000),
|
|
||||||
// Chasing
|
|
||||||
captured: false,
|
|
||||||
chasing: null,
|
|
||||||
chased: null,
|
|
||||||
// Locations
|
|
||||||
lastSentLocation: null,
|
|
||||||
locationSendDeadline: null,
|
|
||||||
currentLocation: null,
|
|
||||||
lastCurrentLocationDate: null,
|
|
||||||
enemyLocation: null,
|
|
||||||
// Placement
|
|
||||||
startingArea: null,
|
|
||||||
ready: false,
|
|
||||||
// Zone
|
|
||||||
outOfZone: false,
|
|
||||||
outOfZoneDeadline: null,
|
|
||||||
hasHandicap: false,
|
|
||||||
// Stats
|
|
||||||
distance: 0,
|
|
||||||
nCaptures: 0,
|
|
||||||
nSentLocation: 0,
|
|
||||||
nObserved: 0,
|
|
||||||
finishDate: null,
|
|
||||||
// First socket infos
|
|
||||||
phoneModel: null,
|
|
||||||
phoneName: null,
|
|
||||||
battery: null,
|
|
||||||
});
|
|
||||||
this.updateChasingChain();
|
|
||||||
// Broadcast new infos
|
|
||||||
secureAdminBroadcast("teams", this.teams);
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
|
|
||||||
removeTeam(teamId) {
|
|
||||||
// Test of parameters
|
|
||||||
if (!this.hasTeam(teamId)) return false;
|
|
||||||
// Logout the team
|
|
||||||
teamBroadcast(teamId, "logout");
|
|
||||||
this.teams = this.teams.filter(t => t.id !== teamId);
|
|
||||||
sendPositionTimeouts.clear(teamId);
|
|
||||||
outOfZoneTimeouts.clear(teamId);
|
|
||||||
this.updateChasingChain();
|
|
||||||
this.checkEndGame();
|
|
||||||
// Broadcast new infos
|
|
||||||
secureAdminBroadcast("teams", this.teams);
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
|
|
||||||
updateTeam(teamId, newInfos) {
|
|
||||||
// Test of parameters
|
|
||||||
if (!this.hasTeam(teamId)) return false;
|
|
||||||
// Update
|
|
||||||
this.teams = this.teams.map(team => team.id == teamId ? {...team, ...newInfos} : team);
|
|
||||||
// Broadcast new infos
|
|
||||||
secureAdminBroadcast("teams", this.teams);
|
|
||||||
sendUpdatedTeamInformations(teamId);
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
|
|
||||||
switchCapturedTeam(teamId) {
|
|
||||||
// Test of parameters
|
|
||||||
if (!this.hasTeam(teamId)) return false;
|
|
||||||
// Variables
|
|
||||||
const team = this.getTeam(teamId);
|
|
||||||
const dateNow = Date.now();
|
|
||||||
// Switch team.captured
|
|
||||||
if (this.state != GameState.PLAYING) return false;
|
|
||||||
if (team.captured) {
|
|
||||||
team.captured = false;
|
|
||||||
team.finishDate = null;
|
|
||||||
team.lastSentLocation = team.currentLocation;
|
|
||||||
team.locationSendDeadline = dateNow + sendPositionTimeouts.delay * 60 * 1000;
|
|
||||||
sendPositionTimeouts.set(team.id);
|
|
||||||
} else {
|
|
||||||
team.captured = true;
|
|
||||||
team.finishDate = dateNow;
|
|
||||||
team.chasing = null;
|
|
||||||
team.chased = null;
|
|
||||||
sendPositionTimeouts.clear(team.id);
|
|
||||||
outOfZoneTimeouts.clear(team.id);
|
|
||||||
}
|
|
||||||
this.updateChasingChain();
|
|
||||||
this.checkEndGame();
|
|
||||||
// Broadcast new infos
|
|
||||||
secureAdminBroadcast("teams", this.teams);
|
|
||||||
sendUpdatedTeamInformations(team.id);
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
|
|
||||||
placementTeam(teamId, placementZone) {
|
|
||||||
// Test of parameters
|
|
||||||
if (!this.hasTeam(teamId)) return false;
|
|
||||||
// Variables
|
|
||||||
const team = this.getTeam(teamId);
|
|
||||||
// Make the capture
|
|
||||||
team.startingArea = placementZone;
|
|
||||||
// Broadcast new infos
|
|
||||||
secureAdminBroadcast("teams", this.teams);
|
|
||||||
sendUpdatedTeamInformations(team.id);
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
|
|
||||||
reorderTeams(newOrder) {
|
|
||||||
// Update teams
|
|
||||||
const teamMap = new Map(this.teams.map(team => [team.id, team]));
|
|
||||||
this.teams = newOrder.map(id => teamMap.get(id));
|
|
||||||
this.updateChasingChain();
|
|
||||||
// Broadcast new infos
|
|
||||||
secureAdminBroadcast("teams", this.teams);
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
|
|
||||||
handicapTeam(teamId) {
|
|
||||||
// Test of parameters
|
|
||||||
if (!this.hasTeam(teamId)) return false;
|
|
||||||
// Variables
|
|
||||||
const team = this.getTeam(teamId);
|
|
||||||
// Make the capture
|
|
||||||
team.hasHandicap = true;
|
|
||||||
sendPositionTimeouts.clear(team.id);
|
|
||||||
// Broadcast new infos
|
|
||||||
secureAdminBroadcast("teams", this.teams);
|
|
||||||
sendUpdatedTeamInformations(team.id);
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* ------------------------------- PLAYERS ACTIONS FUNCTIONS ------------------------------- */
|
|
||||||
|
|
||||||
updateLocation(teamId, location) {
|
|
||||||
// Test of parameters
|
|
||||||
if (!this.hasTeam(teamId)) return false;
|
|
||||||
if (!this.hasTeam(this.getTeam(teamId).chasing)) return false;
|
|
||||||
if (!location) return false;
|
|
||||||
// Variables
|
|
||||||
const team = this.getTeam(teamId);
|
|
||||||
const enemyTeam = this.getTeam(team.chasing);
|
|
||||||
const dateNow = Date.now();
|
|
||||||
// Update distance
|
|
||||||
if (this.state == GameState.PLAYING && team.currentLocation) {
|
|
||||||
team.distance += Math.floor(getDistanceFromLatLon({lat: location[0], lng: location[1]}, {lat: team.currentLocation[0], lng: team.currentLocation[1]}));
|
|
||||||
}
|
|
||||||
// Update of currentLocation
|
|
||||||
team.currentLocation = location;
|
|
||||||
team.lastCurrentLocationDate = dateNow;
|
|
||||||
// If hasHandicap
|
|
||||||
if (this.state == GameState.PLAYING && team.hasHandicap) {
|
|
||||||
team.lastSentLocation = team.currentLocation;
|
|
||||||
team.enemyLocation = enemyTeam.currentLocation;
|
|
||||||
}
|
|
||||||
// Update of ready
|
|
||||||
if (this.state == GameState.PLACEMENT && team.startingArea) {
|
|
||||||
team.ready = isInCircle({ lat: location[0], lng: location[1] }, team.startingArea.center, team.startingArea.radius);
|
|
||||||
}
|
|
||||||
// Update out of zone
|
|
||||||
if (this.state == GameState.PLAYING) {
|
|
||||||
const teamCurrentlyOutOfZone = !zoneManager.isInZone({ lat: location[0], lng: location[1] })
|
|
||||||
if (teamCurrentlyOutOfZone && !team.outOfZone) {
|
|
||||||
team.outOfZone = true;
|
|
||||||
team.outOfZoneDeadline = dateNow + outOfZoneTimeouts.delay * 60 * 1000;
|
|
||||||
outOfZoneTimeouts.set(teamId);
|
|
||||||
} else if (!teamCurrentlyOutOfZone && team.outOfZone) {
|
|
||||||
team.outOfZone = false;
|
|
||||||
team.outOfZoneDeadline = null;
|
|
||||||
team.hasHandicap = false;
|
|
||||||
if (!sendPositionTimeouts.has(team.id)) {
|
|
||||||
team.locationSendDeadline = dateNow + sendPositionTimeouts.delay * 60 * 1000;
|
|
||||||
sendPositionTimeouts.set(team.id);
|
|
||||||
}
|
|
||||||
outOfZoneTimeouts.clear(teamId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Broadcast new infos
|
|
||||||
secureAdminBroadcast("teams", this.teams);
|
|
||||||
sendUpdatedTeamInformations(team.id);
|
|
||||||
// Update of events of the game
|
|
||||||
trajectory.writePosition(dateNow, team.id, location[0], location[1]);
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
|
|
||||||
sendLocation(teamId) {
|
|
||||||
// Conditions
|
|
||||||
if (this.state != GameState.PLAYING) return false;
|
|
||||||
// Test of parameters
|
|
||||||
if (!this.hasTeam(teamId)) return false;
|
|
||||||
if (!this.hasTeam(this.getTeam(teamId).chasing)) return false;
|
|
||||||
// Variables
|
|
||||||
const team = this.getTeam(teamId);
|
|
||||||
const enemyTeam = this.getTeam(team.chasing);
|
|
||||||
const dateNow = Date.now();
|
|
||||||
// Update team
|
|
||||||
team.nSentLocation++;
|
|
||||||
team.lastSentLocation = team.currentLocation;
|
|
||||||
team.enemyLocation = enemyTeam.lastSentLocation;
|
|
||||||
team.locationSendDeadline = dateNow + sendPositionTimeouts.delay * 60 * 1000;
|
|
||||||
sendPositionTimeouts.set(team.id);
|
|
||||||
// Update enemy
|
|
||||||
enemyTeam.nObserved++;
|
|
||||||
// Broadcast new infos
|
|
||||||
secureAdminBroadcast("teams", this.teams);
|
|
||||||
sendUpdatedTeamInformations(team.id);
|
|
||||||
sendUpdatedTeamInformations(enemyTeam.id);
|
|
||||||
// Update of events of the game
|
|
||||||
trajectory.writeSeePosition(dateNow, team.id, enemyTeam.id);
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
|
|
||||||
tryCapture(teamId, captureCode) {
|
|
||||||
// Conditions
|
|
||||||
if (this.state != GameState.PLAYING) return false;
|
|
||||||
// Test of parameters
|
|
||||||
if (!this.hasTeam(teamId)) return false;
|
|
||||||
if (!this.hasTeam(this.getTeam(teamId).chasing)) return false;
|
|
||||||
// Variables
|
|
||||||
const team = this.getTeam(teamId);
|
|
||||||
const enemyTeam = this.getTeam(team.chasing);
|
|
||||||
const dateNow = Date.now();
|
|
||||||
// Verify the capture
|
|
||||||
if (enemyTeam.captureCode != captureCode) return false;
|
|
||||||
// Make the capture
|
|
||||||
team.nCaptures++;
|
|
||||||
enemyTeam.captured = true;
|
|
||||||
enemyTeam.finishDate = dateNow;
|
|
||||||
enemyTeam.chasing = null;
|
|
||||||
enemyTeam.chased = null;
|
|
||||||
sendPositionTimeouts.clear(enemyTeam.id);
|
|
||||||
outOfZoneTimeouts.clear(enemyTeam.id);
|
|
||||||
this.updateChasingChain();
|
|
||||||
this.checkEndGame();
|
|
||||||
// Broadcast new infos
|
|
||||||
secureAdminBroadcast("teams", this.teams);
|
|
||||||
sendUpdatedTeamInformations(team.id);
|
|
||||||
sendUpdatedTeamInformations(enemyTeam.id);
|
|
||||||
// Update of events of the game
|
|
||||||
trajectory.writeCapture(dateNow, team.id, enemyTeam.id);
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
/*
|
|
||||||
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 game from "./game.js";
|
|
||||||
import zoneManager from "./zone_manager.js";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send a socket message to all the players of a team
|
|
||||||
* @param {String} teamId The team that will receive the message
|
|
||||||
* @param {String} event Event name
|
|
||||||
* @param {*} data The payload
|
|
||||||
*/
|
|
||||||
export function teamBroadcast(teamId, event, data) {
|
|
||||||
game.getTeam(teamId).sockets.forEach(socketId => io.of("player").to(socketId).emit(event, data));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send a message to all logged in players
|
|
||||||
* @param {String} event Event name
|
|
||||||
* @param {String} data payload
|
|
||||||
*/
|
|
||||||
export function playersBroadcast(event, data) {
|
|
||||||
game.teams.forEach(team => teamBroadcast(team.id, event, data));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send a socket message to all the players of a team
|
|
||||||
* @param {String} teamId The team that will receive the message
|
|
||||||
*/
|
|
||||||
export function sendUpdatedTeamInformations(teamId) {
|
|
||||||
// Test of parameters
|
|
||||||
if (!game.hasTeam(teamId)) return false;
|
|
||||||
// Variables
|
|
||||||
const team = game.getTeam(teamId);
|
|
||||||
const enemyTeam = game.getTeam(team.chasing);
|
|
||||||
teamBroadcast(teamId, "update_team", {
|
|
||||||
// Identification
|
|
||||||
name: team.name,
|
|
||||||
captureCode: team.captureCode,
|
|
||||||
// Chasing
|
|
||||||
captured: team.captured,
|
|
||||||
enemyName: enemyTeam?.name,
|
|
||||||
// Locations
|
|
||||||
lastSentLocation: team.lastSentLocation,
|
|
||||||
enemyLocation: team.enemyLocation,
|
|
||||||
// Placement phase
|
|
||||||
startingArea: team.startingArea,
|
|
||||||
ready: team.ready,
|
|
||||||
// Constraints
|
|
||||||
outOfZone: team.outOfZone,
|
|
||||||
outOfZoneDeadline: team.outOfZoneDeadline,
|
|
||||||
locationSendDeadline: team.locationSendDeadline,
|
|
||||||
hasHandicap: team.hasHandicap,
|
|
||||||
enemyHasHandicap: enemyTeam?.hasHandicap,
|
|
||||||
// Stats
|
|
||||||
distance: team.distance,
|
|
||||||
nCaptures: team.nCaptures,
|
|
||||||
nSentLocation: team.nSentLocation,
|
|
||||||
finishDate: team.finishDate,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function initTeamSocket(io) {
|
|
||||||
io.of("player").on("connection", (socket) => {
|
|
||||||
console.log("Connection of a player");
|
|
||||||
let teamId = null;
|
|
||||||
|
|
||||||
const login = (loginTeamId) => {
|
|
||||||
logout();
|
|
||||||
if (!game.addPlayer(loginTeamId, socket.id)) return false;
|
|
||||||
teamId = loginTeamId;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const logout = () => {
|
|
||||||
if (!teamId) return;
|
|
||||||
game.removePlayer(teamId, socket.id);
|
|
||||||
teamId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.on("disconnect", () => {
|
|
||||||
console.log("Disconnection of a player");
|
|
||||||
logout();
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("logout", () => {
|
|
||||||
logout();
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("login", (loginTeamId, callback) => {
|
|
||||||
if (!login(loginTeamId)) {
|
|
||||||
callback({ isLoggedIn: false, message: "Login denied" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
sendUpdatedTeamInformations(loginTeamId);
|
|
||||||
socket.emit("game_state", {
|
|
||||||
state: game.state,
|
|
||||||
date: game.startDate
|
|
||||||
});
|
|
||||||
socket.emit("current_zone", {
|
|
||||||
begin: zoneManager.getCurrentZone(),
|
|
||||||
end: zoneManager.getNextZone(),
|
|
||||||
endDate: zoneManager.currentZone?.endDate,
|
|
||||||
});
|
|
||||||
socket.emit("settings", game.getPlayerSettings());
|
|
||||||
callback({ isLoggedIn : true, message: "Logged in"});
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("update_position", (position) => {
|
|
||||||
if (!teamId) return;
|
|
||||||
if (game.isPlayerCapitain(teamId, socket.id)) {
|
|
||||||
game.updateLocation(teamId, position);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("send_position", () => {
|
|
||||||
if (!teamId) return;
|
|
||||||
game.sendLocation(teamId);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("capture", (captureCode, callback) => {
|
|
||||||
if (!teamId) return;
|
|
||||||
if (game.tryCapture(teamId, captureCode)) {
|
|
||||||
callback({ hasCaptured : true, message: "Capture successful" });
|
|
||||||
} else {
|
|
||||||
callback({ hasCaptured : false, message: "Capture denied" });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("device_info", (infos) => {
|
|
||||||
if (!teamId) return;
|
|
||||||
if (game.isPlayerCapitain(teamId, socket.id)) {
|
|
||||||
game.updateTeam(teamId, {phoneModel: infos.model, phoneName: infos.name});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("battery_update", (batteryLevel) => {
|
|
||||||
if (!teamId) return;
|
|
||||||
if (game.isPlayerCapitain(teamId, socket.id)) {
|
|
||||||
game.updateTeam(teamId, {battery: batteryLevel});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
import game from "./game.js";
|
|
||||||
|
|
||||||
class TimeoutManager {
|
|
||||||
constructor() {
|
|
||||||
this.timeouts = new Map();
|
|
||||||
}
|
|
||||||
|
|
||||||
has(key) {
|
|
||||||
return this.timeouts.has(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
has(teamID) {
|
|
||||||
return this.timeoutManager.has(teamID);
|
|
||||||
},
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
has(teamID) {
|
|
||||||
return this.timeoutManager.has(teamID);
|
|
||||||
},
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
import fs from "fs";
|
|
||||||
import path from "path";
|
|
||||||
const TRAJECTORIES_DIR = path.join(process.cwd(), "trajectories");
|
|
||||||
const EXTENSION = "txt";
|
|
||||||
|
|
||||||
// Useful functions
|
|
||||||
|
|
||||||
function teamIDToPath(teamID) {
|
|
||||||
return path.join(TRAJECTORIES_DIR, teamID + "." + EXTENSION);
|
|
||||||
}
|
|
||||||
|
|
||||||
function dataToLine(...data) {
|
|
||||||
return data.join(',');
|
|
||||||
}
|
|
||||||
|
|
||||||
const errorFile = (err) => {
|
|
||||||
if (err) console.error("Error appending to file:", err);
|
|
||||||
};
|
|
||||||
|
|
||||||
function addLineToFile(teamID, line) {
|
|
||||||
// Insert the line in the file of teamID depending on the date (lines are sorted by date)
|
|
||||||
if (!fs.existsSync(teamIDToPath(teamID))) {
|
|
||||||
fs.writeFile(teamIDToPath(teamID), line + '\n', errorFile);
|
|
||||||
} else {
|
|
||||||
fs.readFile(teamIDToPath(teamID), 'utf8', (err, data) => {
|
|
||||||
if (err) {
|
|
||||||
errorFile(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let lines = data.trim().split('\n');
|
|
||||||
const newDate = parseInt(line.split(',')[0], 10);
|
|
||||||
let insertIndex = lines.length;
|
|
||||||
for (let i = lines.length - 1; i >= 0; i--) {
|
|
||||||
const date = parseInt(lines[i].split(',')[0], 10);
|
|
||||||
if (date <= newDate) {
|
|
||||||
insertIndex = i + 1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lines.splice(insertIndex, 0, line);
|
|
||||||
fs.writeFile(teamIDToPath(teamID), lines.join('\n') + '\n', errorFile);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function initTrajectories() {
|
|
||||||
const files = fs.readdirSync(TRAJECTORIES_DIR);
|
|
||||||
for (const file of files) fs.unlinkSync(path.join(TRAJECTORIES_DIR, file));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Export functions
|
|
||||||
|
|
||||||
export default {
|
|
||||||
isRecording: false,
|
|
||||||
|
|
||||||
start() {
|
|
||||||
initTrajectories();
|
|
||||||
this.isRecording = true;
|
|
||||||
},
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
this.isRecording = false;
|
|
||||||
},
|
|
||||||
|
|
||||||
writePosition(date, teamID, lon, lat) {
|
|
||||||
if (this.isRecording) {
|
|
||||||
addLineToFile(teamID, dataToLine(date, "position", lon, lat));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
writeCapture(date, teamID, capturedTeamID) {
|
|
||||||
if (this.isRecording) {
|
|
||||||
addLineToFile(teamID, dataToLine(date, "capture", capturedTeamID));
|
|
||||||
addLineToFile(capturedTeamID, dataToLine(date, "captured", teamID));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
writeSeePosition(date, teamID, seenTeamID) {
|
|
||||||
if (this.isRecording) {
|
|
||||||
addLineToFile(teamID, dataToLine(date, "see"));
|
|
||||||
addLineToFile(seenTeamID, dataToLine(date, "seen"));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
writeOutOfZone(date, teamID, isOutOfZone) {
|
|
||||||
if (this.isRecording) {
|
|
||||||
addLineToFile(teamID, dataToLine(date, "zone", isOutOfZone));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
import multer from "multer";
|
|
||||||
import fs from "fs";
|
|
||||||
import path from "path";
|
|
||||||
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 = [
|
|
||||||
"image/png",
|
|
||||||
"image/jpeg",
|
|
||||||
"image/gif"
|
|
||||||
]
|
|
||||||
|
|
||||||
// Setup multer (the file upload middleware)
|
|
||||||
const storage = multer.diskStorage({
|
|
||||||
// Save the file in the uploads directory
|
|
||||||
destination: function (req, file, callback) {
|
|
||||||
callback(null, UPLOAD_DIR);
|
|
||||||
},
|
|
||||||
// Save the file with the team ID as the filename
|
|
||||||
filename: function (req, file, callback) {
|
|
||||||
const teamId = req.query.team;
|
|
||||||
if (typeof teamId === 'string') {
|
|
||||||
callback(null, teamId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const upload = multer({
|
|
||||||
storage,
|
|
||||||
// Only upload the file if it is a valid mime type and the team POST parameter is a valid team
|
|
||||||
fileFilter: function (req, file, callback) {
|
|
||||||
if (ALLOWED_MIME.indexOf(file.mimetype) == -1) {
|
|
||||||
callback(null, false);
|
|
||||||
} else if (!gameManager.teams.get(req.query.team)) {
|
|
||||||
callback(null, false);
|
|
||||||
} else {
|
|
||||||
callback(null, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const clean = () => {
|
|
||||||
const files = fs.readdirSync(UPLOAD_DIR);
|
|
||||||
for (const file of files) {
|
|
||||||
const filePath = path.join(UPLOAD_DIR, file);
|
|
||||||
fs.unlinkSync(filePath);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const initPhotoUpload = (app) => {
|
|
||||||
clean();
|
|
||||||
|
|
||||||
// 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
|
|
||||||
app.get("/photo/my", (req, res) => {
|
|
||||||
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("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");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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 = gameManager.teams.get(req.query.team);
|
|
||||||
if (team) {
|
|
||||||
const imagePath = path.join(UPLOAD_DIR, team.chasing);
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
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: {}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
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: {}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
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,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
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,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
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: {}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
1
server/traque-back/src/util/random.js
Normal file
1
server/traque-back/src/util/random.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const randint = (max) => Math.floor(Math.random() * max);
|
||||||
56
server/traque-back/src/util/scheduler.js
Normal file
56
server/traque-back/src/util/scheduler.js
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
export class Scheduler {
|
||||||
|
constructor() {
|
||||||
|
this._id = null;
|
||||||
|
this._date = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isActive() {
|
||||||
|
return this._id !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get dateOfExecution() {
|
||||||
|
return this._date;
|
||||||
|
}
|
||||||
|
|
||||||
|
get timeToExecution() {
|
||||||
|
return this.isActive ? Math.max(0, this._date - Date.now()) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
start(callback, delay) {
|
||||||
|
this.interrupt();
|
||||||
|
this._id = setTimeout(() => { this._clean(); callback(); }, delay);
|
||||||
|
this._date = Date.now() + delay;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
interrupt() {
|
||||||
|
if (!this.isActive) return;
|
||||||
|
clearTimeout(this._id);
|
||||||
|
this._clean();
|
||||||
|
}
|
||||||
|
|
||||||
|
_clean() {
|
||||||
|
this._id = null;
|
||||||
|
this._date = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ScheduledTask extends Scheduler {
|
||||||
|
constructor(callback, delay) {
|
||||||
|
super();
|
||||||
|
this._callback = callback;
|
||||||
|
this._delay = delay;
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
return super.start(this._callback, this._delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
setDelay(delay, restart = false) {
|
||||||
|
this._delay = delay;
|
||||||
|
if (restart && this.isActive) {
|
||||||
|
this.interrupt();
|
||||||
|
this.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
server/traque-back/src/util/state_tracker.js
Normal file
14
server/traque-back/src/util/state_tracker.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
export class StateTracker {
|
||||||
|
constructor(mapper) {
|
||||||
|
this._mapper = mapper;
|
||||||
|
this._hash = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSyncDto() {
|
||||||
|
const dto = this._mapper.map();
|
||||||
|
const currentHash = this._mapper.hash(dto);
|
||||||
|
const hasChanged = currentHash !== this._hash;
|
||||||
|
this._hash = currentHash;
|
||||||
|
return { dto, hasChanged };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
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