diff --git a/front/.eslintrc.json b/front/.eslintrc.json index 0cee14a3..3aab37d9 100644 --- a/front/.eslintrc.json +++ b/front/.eslintrc.json @@ -7,7 +7,8 @@ }, "extends": [ "eslint:recommended", - "plugin:@typescript-eslint/eslint-recommended" + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended-requiring-type-checking" ], "globals": { "Atomics": "readonly", @@ -16,12 +17,14 @@ "parser": "@typescript-eslint/parser", "parserOptions": { "ecmaVersion": 2018, - "sourceType": "module" + "sourceType": "module", + "project": "./tsconfig.json" }, "plugins": [ "@typescript-eslint" ], "rules": { - "no-unused-vars": "off" + "no-unused-vars": "off", + "@typescript-eslint/no-explicit-any": "error" } -} \ No newline at end of file +} diff --git a/front/package.json b/front/package.json index dba6ea03..d73eb50d 100644 --- a/front/package.json +++ b/front/package.json @@ -22,6 +22,7 @@ "@types/simple-peer": "^9.6.0", "@types/socket.io-client": "^1.4.32", "phaser": "^3.22.0", + "queue-typescript": "^1.0.1", "simple-peer": "^9.6.2", "socket.io-client": "^2.3.0" }, diff --git a/front/src/Connection.ts b/front/src/Connection.ts index baa9c8a4..c4ac92c6 100644 --- a/front/src/Connection.ts +++ b/front/src/Connection.ts @@ -1,4 +1,3 @@ -import {GameManager} from "./Phaser/Game/GameManager"; import Axios from "axios"; import {API_URL} from "./Enum/EnvironmentVariable"; import {MessageUI} from "./Logger/MessageUI"; @@ -8,12 +7,12 @@ const SocketIo = require('socket.io-client'); import Socket = SocketIOClient.Socket; import {PlayerAnimationNames} from "./Phaser/Player/Animation"; import {UserSimplePeer} from "./WebRtc/SimplePeer"; +import {SignalData} from "simple-peer"; enum EventMessage{ WEBRTC_SIGNAL = "webrtc-signal", WEBRTC_START = "webrtc-start", - WEBRTC_JOIN_ROOM = "webrtc-join-room", JOIN_ROOM = "join-room", // bi-directional USER_POSITION = "user-position", // bi-directional USER_MOVED = "user-moved", // From server to client @@ -25,22 +24,6 @@ enum EventMessage{ SET_PLAYER_DETAILS = "set-player-details", // Send the name and character to the server (on connect), receive back the id. CONNECT_ERROR = "connect_error", - RECONNECT = "reconnect", - RECONNECTING = "reconnecting", - RECONNECT_ERROR = "reconnect_error", - RECONNECT_FAILED = "reconnect_failed" -} - -class Message { - userId: string; - name: string; - character: string; - - constructor(userId : string, name: string, character: string) { - this.userId = userId; - this.name = name; - this.character = character; - } } export interface PointInterface { @@ -70,15 +53,6 @@ export interface MessageUserMovedInterface { position: PointInterface; } -class MessageUserPosition extends Message implements MessageUserPositionInterface{ - position: PointInterface; - - constructor(userId : string, point : Point, name: string, character: string) { - super(userId, name, character); - this.position = point; - } -} - export interface MessageUserJoined { userId: string; name: string; @@ -86,11 +60,6 @@ export interface MessageUserJoined { position: PointInterface } -export interface ListMessageUserPositionInterface { - roomId: string; - listUsersPosition: Array; -} - export interface PositionInterface { x: number, y: number @@ -110,182 +79,117 @@ export interface WebRtcDisconnectMessageInterface { userId: string } -export interface ConnectionInterface { - socket: Socket|null; - token: string|null; - name: string|null; - userId: string|null; - - createConnection(name: string, characterSelected: string): Promise; - - loadStartMap(): Promise; - - joinARoom(roomId: string, startX: number, startY: number, direction: string, moving: boolean): void; - - sharePosition(x: number, y: number, direction: string, moving: boolean): void; - - /*webrtc*/ - sendWebrtcSignal(signal: any, roomId: string, userId?: string|null, receiverId?: string): void; - - receiveWebrtcSignal(callBack: Function): void; - - receiveWebrtcStart(callBack: (message: WebRtcStartMessageInterface) => void): void; - - disconnectMessage(callBack: (message: WebRtcDisconnectMessageInterface) => void): void; +export interface WebRtcSignalMessageInterface { + userId: string, + receiverId: string, + roomId: string, + signal: SignalData } -export class Connection implements ConnectionInterface { - socket: Socket|null = null; - token: string|null = null; - name: string|null = null; // TODO: drop "name" storage here - character: string|null = null; - userId: string|null = null; +export interface StartMapInterface { + mapUrlStart: string, + startInstance: string +} - GameManager: GameManager; +export class Connection implements Connection { + private readonly socket: Socket; + private userId: string|null = null; - lastPositionShared: PointInterface|null = null; - lastRoom: string|null = null; + private constructor(token: string) { - constructor(GameManager: GameManager) { - this.GameManager = GameManager; + this.socket = SocketIo(`${API_URL}`, { + query: { + token: token + }, + reconnection: false // Reconnection is handled by the application itself + }); + + this.socket.on(EventMessage.MESSAGE_ERROR, (message: string) => { + console.error(EventMessage.MESSAGE_ERROR, message); + }) } - createConnection(name: string, characterSelected: string): Promise { - this.name = name; - this.character = characterSelected; + public static createConnection(name: string, characterSelected: string): Promise { return Axios.post(`${API_URL}/login`, {name: name}) .then((res) => { - this.token = res.data.token; - this.socket = SocketIo(`${API_URL}`, { - query: { - token: this.token - } + + return new Promise((resolve, reject) => { + const connection = new Connection(res.data.token); + + connection.onConnectError((error: object) => { + console.log('An error occurred while connecting to socket server. Retrying'); + reject(error); + }); + + connection.socket.emit(EventMessage.SET_PLAYER_DETAILS, { + name: name, + character: characterSelected + } as SetPlayerDetailsMessage, (id: string) => { + connection.userId = id; + }); + + resolve(connection); }); - - //listen event - this.disconnectServer(); - this.errorMessage(); - this.groupUpdatedOrCreated(); - this.groupDeleted(); - this.onUserJoins(); - this.onUserMoved(); - this.onUserLeft(); - - return this.connectSocketServer(); }) .catch((err) => { - console.error(err); - throw err; + // Let's retry in 4-6 seconds + return new Promise((resolve, reject) => { + setTimeout(() => { + Connection.createConnection(name, characterSelected).then((connection) => resolve(connection)) + .catch((error) => reject(error)); + }, 4000 + Math.floor(Math.random() * 2000) ); + }); }); } - private getSocket(): Socket { - if (this.socket === null) { - throw new Error('Socket not initialized while using Connection') - } - return this.socket; + public closeConnection(): void { + this.socket?.close(); } - /** - * - * @param character - */ - connectSocketServer(): Promise{ - return new Promise((resolve, reject) => { - this.getSocket().emit(EventMessage.SET_PLAYER_DETAILS, { - name: this.name, - character: this.character - } as SetPlayerDetailsMessage, (id: string) => { - this.userId = id; + + public joinARoom(roomId: string, startX: number, startY: number, direction: string, moving: boolean): Promise { + const promise = new Promise((resolve, reject) => { + this.socket.emit(EventMessage.JOIN_ROOM, { roomId, position: {x: startX, y: startY, direction, moving }}, (userPositions: MessageUserPositionInterface[]) => { + resolve(userPositions); }); - - //if try to reconnect with last position - /*if(this.lastRoom) { - //join the room - this.joinARoom(this.lastRoom, - this.lastPositionShared ? this.lastPositionShared.x : 0, - this.lastPositionShared ? this.lastPositionShared.y : 0, - this.lastPositionShared ? this.lastPositionShared.direction : PlayerAnimationNames.WalkDown, - this.lastPositionShared ? this.lastPositionShared.moving : false); - }*/ - - /*if(this.lastPositionShared) { - - //share your first position - this.sharePosition( - this.lastPositionShared ? this.lastPositionShared.x : 0, - this.lastPositionShared ? this.lastPositionShared.y : 0, - this.lastPositionShared.direction, - this.lastPositionShared.moving - ); - }*/ - - resolve(this); - }); + }) + return promise; } - //TODO add middleware with access token to secure api - loadStartMap() : Promise { - return Axios.get(`${API_URL}/start-map`) - .then((res) => { - return res.data; - }).catch((err) => { - console.error(err); - throw err; - }); - } - - joinARoom(roomId: string, startX: number, startY: number, direction: string, moving: boolean): void { - let point = new Point(startX, startY, direction, moving); - this.lastPositionShared = point; - this.getSocket().emit(EventMessage.JOIN_ROOM, { roomId, position: {x: startX, y: startY, direction, moving }}, (userPositions: MessageUserPositionInterface[]) => { - this.GameManager.initUsersPosition(userPositions); - }); - this.lastRoom = roomId; - } - - sharePosition(x : number, y : number, direction : string, moving: boolean) : void{ + public sharePosition(x : number, y : number, direction : string, moving: boolean) : void{ if(!this.socket){ return; } - let point = new Point(x, y, direction, moving); - this.lastPositionShared = point; - this.getSocket().emit(EventMessage.USER_POSITION, point); + const point = new Point(x, y, direction, moving); + this.socket.emit(EventMessage.USER_POSITION, point); } - private onUserJoins(): void { - this.getSocket().on(EventMessage.JOIN_ROOM, (message: MessageUserJoined) => { - this.GameManager.onUserJoins(message); - }); + public onUserJoins(callback: (message: MessageUserJoined) => void): void { + this.socket.on(EventMessage.JOIN_ROOM, callback); } - private onUserMoved(): void { - this.getSocket().on(EventMessage.USER_MOVED, (message: MessageUserMovedInterface) => { - this.GameManager.onUserMoved(message); - }); + public onUserMoved(callback: (message: MessageUserMovedInterface) => void): void { + this.socket.on(EventMessage.USER_MOVED, callback); } - private onUserLeft(): void { - this.getSocket().on(EventMessage.USER_LEFT, (userId: string) => { - this.GameManager.onUserLeft(userId); - }); + public onUserLeft(callback: (userId: string) => void): void { + this.socket.on(EventMessage.USER_LEFT, callback); } - private groupUpdatedOrCreated(): void { - this.getSocket().on(EventMessage.GROUP_CREATE_UPDATE, (groupCreateUpdateMessage: GroupCreatedUpdatedMessageInterface) => { - //console.log('Group ', groupCreateUpdateMessage.groupId, " position :", groupCreateUpdateMessage.position.x, groupCreateUpdateMessage.position.y) - this.GameManager.shareGroupPosition(groupCreateUpdateMessage); - }) + public onGroupUpdatedOrCreated(callback: (groupCreateUpdateMessage: GroupCreatedUpdatedMessageInterface) => void): void { + this.socket.on(EventMessage.GROUP_CREATE_UPDATE, callback); } - private groupDeleted(): void { - this.getSocket().on(EventMessage.GROUP_DELETE, (groupId: string) => { - this.GameManager.deleteGroup(groupId); - }) + public onGroupDeleted(callback: (groupId: string) => void): void { + this.socket.on(EventMessage.GROUP_DELETE, callback) } - sendWebrtcSignal(signal: any, roomId: string, userId? : string|null, receiverId? : string) { - return this.getSocket().emit(EventMessage.WEBRTC_SIGNAL, { + public onConnectError(callback: (error: object) => void): void { + this.socket.on(EventMessage.CONNECT_ERROR, callback) + } + + public sendWebrtcSignal(signal: unknown, roomId: string, userId? : string|null, receiverId? : string) { + return this.socket.emit(EventMessage.WEBRTC_SIGNAL, { userId: userId ? userId : this.userId, receiverId: receiverId ? receiverId : this.userId, roomId: roomId, @@ -293,48 +197,30 @@ export class Connection implements ConnectionInterface { }); } - receiveWebrtcStart(callback: (message: WebRtcStartMessageInterface) => void) { - this.getSocket().on(EventMessage.WEBRTC_START, callback); + public receiveWebrtcStart(callback: (message: WebRtcStartMessageInterface) => void) { + this.socket.on(EventMessage.WEBRTC_START, callback); } - receiveWebrtcSignal(callback: Function) { - return this.getSocket().on(EventMessage.WEBRTC_SIGNAL, callback); + public receiveWebrtcSignal(callback: (message: WebRtcSignalMessageInterface) => void) { + return this.socket.on(EventMessage.WEBRTC_SIGNAL, callback); } - private errorMessage(): void { - this.getSocket().on(EventMessage.MESSAGE_ERROR, (message: string) => { - console.error(EventMessage.MESSAGE_ERROR, message); - }) - } - - private disconnectServer(): void { - this.getSocket().on(EventMessage.CONNECT_ERROR, () => { - this.GameManager.switchToDisconnectedScene(); - }); - - this.getSocket().on(EventMessage.RECONNECTING, () => { - console.log('Trying to reconnect'); - }); - - this.getSocket().on(EventMessage.RECONNECT_ERROR, () => { - console.log('Error while trying to reconnect.'); - }); - - this.getSocket().on(EventMessage.RECONNECT_FAILED, () => { - console.error('Reconnection failed. Giving up.'); - }); - - this.getSocket().on(EventMessage.RECONNECT, () => { - console.log('Reconnect event triggered'); - this.connectSocketServer(); - if (this.lastPositionShared === null) { - throw new Error('No last position shared found while reconnecting'); + public onServerDisconnected(callback: (reason: string) => void): void { + this.socket.on('disconnect', (reason: string) => { + if (reason === 'io client disconnect') { + // The client asks for disconnect, let's not trigger any event. + return; } - this.GameManager.reconnectToGameScene(this.lastPositionShared); + callback(reason); }); + + } + + public getUserId(): string|null { + return this.userId; } disconnectMessage(callback: (message: WebRtcDisconnectMessageInterface) => void): void { - this.getSocket().on(EventMessage.WEBRTC_DISCONNECT, callback); + this.socket.on(EventMessage.WEBRTC_DISCONNECT, callback); } } diff --git a/front/src/Cypress/CypressAsserter.ts b/front/src/Cypress/CypressAsserter.ts index 95adb156..82eeab1f 100644 --- a/front/src/Cypress/CypressAsserter.ts +++ b/front/src/Cypress/CypressAsserter.ts @@ -1,13 +1,17 @@ -declare let window:any; +declare let window:WindowWithCypressAsserter; + +interface WindowWithCypressAsserter extends Window { + cypressAsserter: CypressAsserter; +} //this class is used to communicate with cypress, our e2e testing client //Since cypress cannot manipulate canvas, we notified it with console logs class CypressAsserter { - + constructor() { window.cypressAsserter = this } - + gameStarted() { console.log('Started the game') } @@ -29,4 +33,4 @@ class CypressAsserter { } } -export const cypressAsserter = new CypressAsserter() \ No newline at end of file +export const cypressAsserter = new CypressAsserter() diff --git a/front/src/Enum/EnvironmentVariable.ts b/front/src/Enum/EnvironmentVariable.ts index 2fbf7979..6e0edd8f 100644 --- a/front/src/Enum/EnvironmentVariable.ts +++ b/front/src/Enum/EnvironmentVariable.ts @@ -1,4 +1,4 @@ -const DEBUG_MODE: boolean = process.env.DEBUG_MODE as any === true; +const DEBUG_MODE: boolean = process.env.DEBUG_MODE == "true"; const API_URL = process.env.API_URL || "http://api.workadventure.localhost"; const RESOLUTION = 3; const ZOOM_LEVEL = 1/*3/4*/; diff --git a/front/src/Logger/MessageUI.ts b/front/src/Logger/MessageUI.ts index 6011fb73..2a581091 100644 --- a/front/src/Logger/MessageUI.ts +++ b/front/src/Logger/MessageUI.ts @@ -2,7 +2,7 @@ export class MessageUI { static warningMessage(text: string){ this.removeMessage(); - let body = document.getElementById("body"); + const body = document.getElementById("body"); body?.insertAdjacentHTML('afterbegin', `
${text} @@ -12,13 +12,13 @@ export class MessageUI { static removeMessage(id : string|null = null) { if(!id){ - let messages = document.getElementsByClassName("message-info"); + const messages = document.getElementsByClassName("message-info"); for (let i = 0; i < messages.length; i++){ messages.item(i)?.remove(); } return; } - let previousElement = document.getElementById(id); + const previousElement = document.getElementById(id); if (!previousElement) { return; } diff --git a/front/src/Phaser/Components/TextInput.ts b/front/src/Phaser/Components/TextInput.ts index 92ddcb56..3a20eadf 100644 --- a/front/src/Phaser/Components/TextInput.ts +++ b/front/src/Phaser/Components/TextInput.ts @@ -10,9 +10,9 @@ export class TextInput extends Phaser.GameObjects.BitmapText { this.underLine = this.scene.add.text(x, y+1, '_______', { fontFamily: 'Arial', fontSize: "32px", color: '#ffffff'}) - let keySpace = this.scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE); - let keyBackspace = this.scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.BACKSPACE); - this.scene.input.keyboard.on('keydown', (event: any) => { + const keySpace = this.scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE); + const keyBackspace = this.scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.BACKSPACE); + this.scene.input.keyboard.on('keydown', (event: KeyboardEvent) => { if (event.keyCode === 8 && this.text.length > 0) { this.deleteLetter(); } else if ((event.keyCode === 32 || (event.keyCode >= 48 && event.keyCode <= 90)) && this.text.length < maxLength) { diff --git a/front/src/Phaser/Entity/Character.ts b/front/src/Phaser/Entity/Character.ts index ba6a8228..7453dc75 100644 --- a/front/src/Phaser/Entity/Character.ts +++ b/front/src/Phaser/Entity/Character.ts @@ -2,7 +2,12 @@ import {PlayerAnimationNames} from "../Player/Animation"; import {SpeechBubble} from "./SpeechBubble"; import BitmapText = Phaser.GameObjects.BitmapText; -export const PLAYER_RESOURCES: Array = [ +export interface PlayerResourceDescriptionInterface { + name: string, + img: string +} + +export const PLAYER_RESOURCES: Array = [ {name: "male1", img: "resources/characters/pipoya/Male 01-1.png" /*, x: 32, y: 32*/}, {name: "male2", img: "resources/characters/pipoya/Male 02-2.png"/*, x: 64, y: 32*/}, {name: "male3", img: "resources/characters/pipoya/Male 03-4.png"/*, x: 96, y: 32*/}, diff --git a/front/src/Phaser/Entity/SpeechBubble.ts b/front/src/Phaser/Entity/SpeechBubble.ts index f2385290..30518890 100644 --- a/front/src/Phaser/Entity/SpeechBubble.ts +++ b/front/src/Phaser/Entity/SpeechBubble.ts @@ -13,10 +13,10 @@ export class SpeechBubble { */ constructor(scene: Scene, player: Character, text: string = "") { - let bubbleHeight = 50; - let bubblePadding = 10; - let bubbleWidth = bubblePadding * 2 + text.length * 10; - let arrowHeight = bubbleHeight / 4; + const bubbleHeight = 50; + const bubblePadding = 10; + const bubbleWidth = bubblePadding * 2 + text.length * 10; + const arrowHeight = bubbleHeight / 4; this.bubble = scene.add.graphics({ x: player.x + 16, y: player.y - 80 }); @@ -35,12 +35,12 @@ export class SpeechBubble { this.bubble.fillRoundedRect(0, 0, bubbleWidth, bubbleHeight, 16); // Calculate arrow coordinates - let point1X = Math.floor(bubbleWidth / 7); - let point1Y = bubbleHeight; - let point2X = Math.floor((bubbleWidth / 7) * 2); - let point2Y = bubbleHeight; - let point3X = Math.floor(bubbleWidth / 7); - let point3Y = Math.floor(bubbleHeight + arrowHeight); + const point1X = Math.floor(bubbleWidth / 7); + const point1Y = bubbleHeight; + const point2X = Math.floor((bubbleWidth / 7) * 2); + const point2Y = bubbleHeight; + const point3X = Math.floor(bubbleWidth / 7); + const point3Y = Math.floor(bubbleHeight + arrowHeight); // bubble arrow shadow this.bubble.lineStyle(4, 0x222222, 0.5); @@ -54,7 +54,7 @@ export class SpeechBubble { this.content = scene.add.text(0, 0, text, { fontFamily: 'Arial', fontSize: 20, color: '#000000', align: 'center', wordWrap: { width: bubbleWidth - (bubblePadding * 2) } }); - let bounds = this.content.getBounds(); + const bounds = this.content.getBounds(); this.content.setPosition(this.bubble.x + (bubbleWidth / 2) - (bounds.width / 2), this.bubble.y + (bubbleHeight / 2) - (bounds.height / 2)); } @@ -68,10 +68,10 @@ export class SpeechBubble { this.bubble.setPosition((x + 16), (y - 80)); } if (this.content) { - let bubbleHeight = 50; - let bubblePadding = 10; - let bubbleWidth = bubblePadding * 2 + this.content.text.length * 10; - let bounds = this.content.getBounds(); + const bubbleHeight = 50; + const bubblePadding = 10; + const bubbleWidth = bubblePadding * 2 + this.content.text.length * 10; + const bounds = this.content.getBounds(); //this.content.setPosition(x, y); this.content.setPosition(this.bubble.x + (bubbleWidth / 2) - (bounds.width / 2), this.bubble.y + (bubbleHeight / 2) - (bounds.height / 2)); } diff --git a/front/src/Phaser/Game/GameManager.ts b/front/src/Phaser/Game/GameManager.ts index 7ed0137f..04cb1bbe 100644 --- a/front/src/Phaser/Game/GameManager.ts +++ b/front/src/Phaser/Game/GameManager.ts @@ -1,24 +1,9 @@ import {GameScene} from "./GameScene"; import { - Connection, - GroupCreatedUpdatedMessageInterface, - ListMessageUserPositionInterface, - MessageUserJoined, - MessageUserMovedInterface, - MessageUserPositionInterface, - Point, - PointInterface + StartMapInterface } from "../../Connection"; -import {SimplePeer} from "../../WebRtc/SimplePeer"; -import {AddPlayerInterface} from "./AddPlayerInterface"; -import {ReconnectingScene, ReconnectingSceneName} from "../Reconnecting/ReconnectingScene"; -import ScenePlugin = Phaser.Scenes.ScenePlugin; -import {Scene} from "phaser"; - -/*export enum StatusGameManagerEnum { - IN_PROGRESS = 1, - CURRENT_USER_CREATED = 2 -}*/ +import Axios from "axios"; +import {API_URL} from "../../Enum/EnvironmentVariable"; export interface HasMovedEvent { direction: string; @@ -27,200 +12,43 @@ export interface HasMovedEvent { y: number; } -export interface MapObject { - key: string, - url: string -} - export class GameManager { - //status: number; - private ConnectionInstance: Connection; - private currentGameScene: GameScene|null = null; private playerName: string; - SimplePeer : SimplePeer; private characterUserSelected: string; - constructor() { - //this.status = StatusGameManagerEnum.IN_PROGRESS; - } - - connect(name: string, characterUserSelected : string) { + public storePlayerDetails(name: string, characterUserSelected : string): void { this.playerName = name; this.characterUserSelected = characterUserSelected; - this.ConnectionInstance = new Connection(this); - return this.ConnectionInstance.createConnection(name, characterUserSelected).then((data : any) => { - this.SimplePeer = new SimplePeer(this.ConnectionInstance); - return data; - }).catch((err) => { - throw err; - }); } - loadStartMap(){ - return this.ConnectionInstance.loadStartMap().then((data) => { - return data; - }).catch((err) => { - throw err; - }); - } - - setCurrentGameScene(gameScene: GameScene) { - this.currentGameScene = gameScene; - } - - - /** - * Permit to create player in started room - */ - /*createCurrentPlayer(): void { - //Get started room send by the backend - this.currentGameScene.createCurrentPlayer(); - //this.status = StatusGameManagerEnum.CURRENT_USER_CREATED; - }*/ - - joinRoom(sceneKey: string, startX: number, startY: number, direction: string, moving: boolean){ - this.ConnectionInstance.joinARoom(sceneKey, startX, startY, direction, moving); - } - - onUserJoins(message: MessageUserJoined): void { - let userMessage: AddPlayerInterface = { - userId: message.userId, - character: message.character, - name: message.name, - position: message.position - } - this.getCurrentGameScene().addPlayer(userMessage); - } - - onUserMoved(message: MessageUserMovedInterface): void { - this.getCurrentGameScene().updatePlayerPosition(message); - } - - onUserLeft(userId: string): void { - this.getCurrentGameScene().removePlayer(userId); - } - - initUsersPosition(usersPosition: MessageUserPositionInterface[]): void { - // Shall we wait for room to be loaded? - /*if (this.status === StatusGameManagerEnum.IN_PROGRESS) { - return; - }*/ - try { - this.getCurrentGameScene().initUsersPosition(usersPosition) - } catch (e) { - console.error(e); - } - } - - /** - * Share group position in game - */ - shareGroupPosition(groupPositionMessage: GroupCreatedUpdatedMessageInterface): void { - /*if (this.status === StatusGameManagerEnum.IN_PROGRESS) { - return; - }*/ - try { - this.getCurrentGameScene().shareGroupPosition(groupPositionMessage) - } catch (e) { - console.error(e); - } - } - - deleteGroup(groupId: string): void { - /*if (this.status === StatusGameManagerEnum.IN_PROGRESS) { - return; - }*/ - try { - this.getCurrentGameScene().deleteGroup(groupId) - } catch (e) { - console.error(e); - } + loadStartMap() : Promise { + return Axios.get(`${API_URL}/start-map`) + .then((res) => { + return res.data; + }).catch((err) => { + console.error(err); + throw err; + }); } getPlayerName(): string { return this.playerName; } - getPlayerId(): string|null { - return this.ConnectionInstance.userId; - } - getCharacterSelected(): string { return this.characterUserSelected; } - pushPlayerPosition(event: HasMovedEvent) { - this.ConnectionInstance.sharePosition(event.x, event.y, event.direction, event.moving); - } - loadMap(mapUrl: string, scene: Phaser.Scenes.ScenePlugin, instance: string): string { - let sceneKey = GameScene.getMapKeyByUrl(mapUrl); + const sceneKey = GameScene.getMapKeyByUrl(mapUrl); - let gameIndex = scene.getIndex(sceneKey); + const gameIndex = scene.getIndex(sceneKey); if(gameIndex === -1){ - let game : Phaser.Scene = GameScene.createFromUrl(mapUrl, instance); + const game : Phaser.Scene = GameScene.createFromUrl(mapUrl, instance); scene.add(sceneKey, game, false); } return sceneKey; } - - private oldSceneKey : string; - private oldMapUrlFile : string; - private oldInstance : string; - private scenePlugin: ScenePlugin; - private reconnectScene: Scene|null = null; - switchToDisconnectedScene(): void { - if (this.currentGameScene === null) { - return; - } - console.log('Switching to disconnected scene'); - this.oldSceneKey = this.currentGameScene.scene.key; - this.oldMapUrlFile = this.currentGameScene.MapUrlFile; - this.oldInstance = this.currentGameScene.instance; - this.currentGameScene.scene.start(ReconnectingSceneName); - this.reconnectScene = this.currentGameScene.scene.get(ReconnectingSceneName); - // Let's completely delete an purge the disconnected scene. We will start again from 0. - this.currentGameScene.scene.remove(this.oldSceneKey); - this.scenePlugin = this.currentGameScene.scene; - this.currentGameScene = null; - } - - private timeoutCallback: NodeJS.Timeout|null = null; - reconnectToGameScene(lastPositionShared: PointInterface): void { - if (this.timeoutCallback !== null) { - console.log('Reconnect called but setTimeout in progress for the reconnection'); - return; - } - if (this.reconnectScene === null) { - console.log('Reconnect called without switchToDisconnectedScene called first'); - - if (!this.currentGameScene) { - console.error('Reconnect called but we are not on a GameScene'); - return; - } - - // In case we are asked to reconnect even if switchToDisconnectedScene was not triggered (can happen when a laptop goes to sleep) - this.switchToDisconnectedScene(); - // Wait a bit for scene to load. Otherwise, starting ReconnectingSceneName and then starting GameScene one after the other fails for some reason. - this.timeoutCallback = setTimeout(() => { - console.log('Reconnecting to game scene from setTimeout'); - this.timeoutCallback = null; - this.reconnectToGameScene(lastPositionShared); - }, 500); - return; - } - console.log('Reconnecting to game scene'); - const game : Phaser.Scene = GameScene.createFromUrl(this.oldMapUrlFile, this.oldInstance); - this.reconnectScene.scene.add(this.oldSceneKey, game, true, { initPosition: lastPositionShared }); - this.reconnectScene = null; - } - - private getCurrentGameScene(): GameScene { - if (this.currentGameScene === null) { - throw new Error('No current game scene enabled'); - } - return this.currentGameScene; - } } export const gameManager = new GameManager(); diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 2b60fd62..8763f913 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -1,13 +1,19 @@ import {GameManager, gameManager, HasMovedEvent} from "./GameManager"; import { - GroupCreatedUpdatedMessageInterface, + Connection, + GroupCreatedUpdatedMessageInterface, MessageUserJoined, MessageUserMovedInterface, MessageUserPositionInterface, PointInterface, PositionInterface } from "../../Connection"; import {CurrentGamerInterface, hasMovedEventName, Player} from "../Player/Player"; import { DEBUG_MODE, ZOOM_LEVEL, POSITION_DELAY } from "../../Enum/EnvironmentVariable"; -import {ITiledMap, ITiledMapLayer, ITiledTileSet} from "../Map/ITiledMap"; -import {PLAYER_RESOURCES} from "../Entity/Character"; +import { + ITiledMap, + ITiledMapLayer, + ITiledMapLayerProperty, + ITiledTileSet +} from "../Map/ITiledMap"; +import {PLAYER_RESOURCES, PlayerResourceDescriptionInterface} from "../Entity/Character"; import Texture = Phaser.Textures.Texture; import Sprite = Phaser.GameObjects.Sprite; import CanvasTexture = Phaser.Textures.CanvasTexture; @@ -16,6 +22,11 @@ import {PlayerAnimationNames} from "../Player/Animation"; import {PlayerMovement} from "./PlayerMovement"; import {PlayersPositionInterpolator} from "./PlayersPositionInterpolator"; import {RemotePlayer} from "../Entity/RemotePlayer"; +import GameObject = Phaser.GameObjects.GameObject; +import { Queue } from 'queue-typescript'; +import {SimplePeer} from "../../WebRtc/SimplePeer"; +import {ReconnectingSceneName} from "../Reconnecting/ReconnectingScene"; + export enum Textures { Player = "male1" @@ -26,6 +37,36 @@ export interface GameSceneInitInterface { startLayerName: string|undefined } +interface InitUserPositionEventInterface { + type: 'InitUserPositionEvent' + event: MessageUserPositionInterface[] +} + +interface AddPlayerEventInterface { + type: 'AddPlayerEvent' + event: AddPlayerInterface +} + +interface RemovePlayerEventInterface { + type: 'RemovePlayerEvent' + userId: string +} + +interface UserMovedEventInterface { + type: 'UserMovedEvent' + event: MessageUserMovedInterface +} + +interface GroupCreatedUpdatedEventInterface { + type: 'GroupCreatedUpdatedEvent' + event: GroupCreatedUpdatedMessageInterface +} + +interface DeleteGroupEventInterface { + type: 'DeleteGroupEvent' + groupId: string +} + export class GameScene extends Phaser.Scene { GameManager : GameManager; Terrains : Array; @@ -40,8 +81,12 @@ export class GameScene extends Phaser.Scene { startX: number; startY: number; circleTexture: CanvasTexture; + pendingEvents: Queue = new Queue(); private initPosition: PositionInterface|null = null; private playersPositionInterpolator = new PlayersPositionInterpolator(); + private connection: Connection; + private simplePeer : SimplePeer; + private connectionPromise: Promise MapKey: string; MapUrlFile: string; @@ -57,17 +102,20 @@ export class GameScene extends Phaser.Scene { y: -1000 } - PositionNextScene: Array = new Array(); + private PositionNextScene: Array> = new Array>(); private startLayerName: string|undefined; - static createFromUrl(mapUrlFile: string, instance: string): GameScene { - let key = GameScene.getMapKeyByUrl(mapUrlFile); - return new GameScene(key, mapUrlFile, instance); + static createFromUrl(mapUrlFile: string, instance: string, key: string|null = null): GameScene { + const mapKey = GameScene.getMapKeyByUrl(mapUrlFile); + if (key === null) { + key = mapKey; + } + return new GameScene(mapKey, mapUrlFile, instance, key); } - constructor(MapKey : string, MapUrlFile: string, instance: string) { + constructor(MapKey : string, MapUrlFile: string, instance: string, key: string) { super({ - key: MapKey + key: key }); this.GameManager = gameManager; @@ -77,13 +125,12 @@ export class GameScene extends Phaser.Scene { this.MapKey = MapKey; this.MapUrlFile = MapUrlFile; - this.RoomId = this.instance + '__' + this.MapKey; + this.RoomId = this.instance + '__' + MapKey; } //hook preload scene preload(): void { - this.GameManager.setCurrentGameScene(this); - this.load.on('filecomplete-tilemapJSON-'+this.MapKey, (key: string, type: string, data: any) => { + this.load.on('filecomplete-tilemapJSON-'+this.MapKey, (key: string, type: string, data: unknown) => { this.onMapLoad(data); }); //TODO strategy to add access token @@ -91,12 +138,12 @@ export class GameScene extends Phaser.Scene { // If the map has already been loaded as part of another GameScene, the "on load" event will not be triggered. // In this case, we check in the cache to see if the map is here and trigger the event manually. if (this.cache.tilemap.exists(this.MapKey)) { - let data = this.cache.tilemap.get(this.MapKey); + const data = this.cache.tilemap.get(this.MapKey); this.onMapLoad(data); } //add player png - PLAYER_RESOURCES.forEach((playerResource: any) => { + PLAYER_RESOURCES.forEach((playerResource: PlayerResourceDescriptionInterface) => { this.load.spritesheet( playerResource.name, playerResource.img, @@ -105,13 +152,76 @@ export class GameScene extends Phaser.Scene { }); this.load.bitmapFont('main_font', 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml'); + + this.connectionPromise = Connection.createConnection(gameManager.getPlayerName(), gameManager.getCharacterSelected()).then((connection : Connection) => { + this.connection = connection; + + connection.onUserJoins((message: MessageUserJoined) => { + const userMessage: AddPlayerInterface = { + userId: message.userId, + character: message.character, + name: message.name, + position: message.position + } + this.addPlayer(userMessage); + }); + + connection.onUserMoved((message: MessageUserMovedInterface) => { + this.updatePlayerPosition(message); + }); + + connection.onUserLeft((userId: string) => { + this.removePlayer(userId); + }); + + connection.onGroupUpdatedOrCreated((groupPositionMessage: GroupCreatedUpdatedMessageInterface) => { + this.shareGroupPosition(groupPositionMessage); + }) + + connection.onGroupDeleted((groupId: string) => { + try { + this.deleteGroup(groupId); + } catch (e) { + console.error(e); + } + }) + + connection.onServerDisconnected(() => { + console.log('Player disconnected from server. Reloading scene.'); + + this.simplePeer.closeAllConnections(); + + const key = 'somekey'+Math.round(Math.random()*10000); + const game : Phaser.Scene = GameScene.createFromUrl(this.MapUrlFile, this.instance, key); + this.scene.add(key, game, true, + { + initPosition: { + x: this.CurrentPlayer.x, + y: this.CurrentPlayer.y + } + }); + + this.scene.stop(this.scene.key); + this.scene.remove(this.scene.key); + }) + + // When connection is performed, let's connect SimplePeer + this.simplePeer = new SimplePeer(this.connection); + + this.scene.wake(); + this.scene.sleep(ReconnectingSceneName); + + return connection; + }); } + // FIXME: we need to put a "unknown" instead of a "any" and validate the structure of the JSON we are receiving. + // eslint-disable-next-line @typescript-eslint/no-explicit-any private onMapLoad(data: any): void { // Triggered when the map is loaded // Load tiles attached to the map recursively this.mapFile = data.data; - let url = this.MapUrlFile.substr(0, this.MapUrlFile.lastIndexOf('/')); + const url = this.MapUrlFile.substr(0, this.MapUrlFile.lastIndexOf('/')); this.mapFile.tilesets.forEach((tileset) => { if (typeof tileset.name === 'undefined' || typeof tileset.image === 'undefined') { console.warn("Don't know how to handle tileset ", tileset) @@ -145,7 +255,7 @@ export class GameScene extends Phaser.Scene { //add layer on map this.Layers = new Array(); let depth = -2; - for (let layer of this.mapFile.layers) { + for (const layer of this.mapFile.layers) { if (layer.type === 'tilelayer') { this.addLayer(this.Map.createStaticLayer(layer.name, this.Terrains, 0, 0).setDepth(depth)); } @@ -167,9 +277,9 @@ export class GameScene extends Phaser.Scene { } else { // Now, let's find the start layer if (this.startLayerName) { - for (let layer of this.mapFile.layers) { + for (const layer of this.mapFile.layers) { if (this.startLayerName === layer.name && layer.type === 'tilelayer' && this.isStartLayer(layer)) { - let startPosition = this.startUser(layer); + const startPosition = this.startUser(layer); this.startX = startPosition.x; this.startY = startPosition.y; } @@ -177,9 +287,9 @@ export class GameScene extends Phaser.Scene { } if (this.startX === undefined) { // If we have no start layer specified or if the hash passed does not exist, let's go with the default start position. - for (let layer of this.mapFile.layers) { + for (const layer of this.mapFile.layers) { if (layer.type === 'tilelayer' && layer.name === "start") { - let startPosition = this.startUser(layer); + const startPosition = this.startUser(layer); this.startX = startPosition.x; this.startY = startPosition.y; } @@ -211,12 +321,12 @@ export class GameScene extends Phaser.Scene { // Let's generate the circle for the group delimiter - let circleElement = Object.values(this.textures.list).find((object: Texture) => object.key === 'circleSprite'); + const circleElement = Object.values(this.textures.list).find((object: Texture) => object.key === 'circleSprite'); if(circleElement) { this.textures.remove('circleSprite'); } this.circleTexture = this.textures.createCanvas('circleSprite', 96, 96); - let context = this.circleTexture.context; + const context = this.circleTexture.context; context.beginPath(); context.arc(48, 48, 48, 0, 2 * Math.PI, false); // context.lineWidth = 5; @@ -225,12 +335,23 @@ export class GameScene extends Phaser.Scene { this.circleTexture.refresh(); // Let's alter browser history - let url = new URL(this.MapUrlFile); + const url = new URL(this.MapUrlFile); let path = '/_/'+this.instance+'/'+url.host+url.pathname; if (this.startLayerName) { path += '#'+this.startLayerName; } window.history.pushState({}, 'WorkAdventure', path); + + // Let's pause the scene if the connection is not established yet + if (this.connection === undefined) { + // Let's wait 0.5 seconds before printing the "connecting" screen to avoid blinking + setTimeout(() => { + if (this.connection === undefined) { + this.scene.sleep(); + this.scene.launch(ReconnectingSceneName); + } + }, 500); + } } private getExitSceneUrl(layer: ITiledMapLayer): string|undefined { @@ -246,11 +367,11 @@ export class GameScene extends Phaser.Scene { } private getProperty(layer: ITiledMapLayer, name: string): string|boolean|number|undefined { - let properties : any = layer.properties; + const properties = layer.properties; if (!properties) { return undefined; } - let obj = properties.find((property:any) => property.name === name); + const obj = properties.find((property: ITiledMapLayerProperty) => property.name === name); if (obj === undefined) { return undefined; } @@ -265,7 +386,7 @@ export class GameScene extends Phaser.Scene { * @param tileHeight */ private loadNextGame(layer: ITiledMapLayer, mapWidth: number, tileWidth: number, tileHeight: number){ - let exitSceneUrl = this.getExitSceneUrl(layer); + const exitSceneUrl = this.getExitSceneUrl(layer); if (exitSceneUrl === undefined) { throw new Error('Layer is not an exit scene layer.'); } @@ -275,18 +396,18 @@ export class GameScene extends Phaser.Scene { } // TODO: eventually compute a relative URL - let absoluteExitSceneUrl = new URL(exitSceneUrl, this.MapUrlFile).href; - let exitSceneKey = gameManager.loadMap(absoluteExitSceneUrl, this.scene, instance); + const absoluteExitSceneUrl = new URL(exitSceneUrl, this.MapUrlFile).href; + const exitSceneKey = gameManager.loadMap(absoluteExitSceneUrl, this.scene, instance); - let tiles : number[] = layer.data as number[]; + const tiles : number[] = layer.data as number[]; for (let key=0; key < tiles.length; key++) { - let objectKey = tiles[key]; + const objectKey = tiles[key]; if(objectKey === 0){ continue; } //key + 1 because the start x = 0; - let y : number = parseInt(((key + 1) / mapWidth).toString()); - let x : number = key - (y * mapWidth); + const y : number = parseInt(((key + 1) / mapWidth).toString()); + const x : number = key - (y * mapWidth); let hash = new URL(exitSceneUrl, this.MapUrlFile).hash; if (hash) { @@ -294,15 +415,13 @@ export class GameScene extends Phaser.Scene { } //push and save switching case - // TODO: this is not efficient. We should refactor that to enable a search by key. For instance: this.PositionNextScene[y][x] = exitSceneKey - this.PositionNextScene.push({ - xStart: (x * tileWidth), - yStart: (y * tileWidth), - xEnd: ((x +1) * tileHeight), - yEnd: ((y + 1) * tileHeight), + if (this.PositionNextScene[y] === undefined) { + this.PositionNextScene[y] = new Array<{key: string, hash: string}>(); + } + this.PositionNextScene[y][x] = { key: exitSceneKey, hash - }) + } } } @@ -310,14 +429,17 @@ export class GameScene extends Phaser.Scene { * @param layer */ private startUser(layer: ITiledMapLayer): PositionInterface { - let tiles : any = layer.data; - let possibleStartPositions : PositionInterface[] = []; + const tiles = layer.data; + if (typeof(tiles) === 'string') { + throw new Error('The content of a JSON map must be filled as a JSON array, not as a string'); + } + const possibleStartPositions : PositionInterface[] = []; tiles.forEach((objectKey : number, key: number) => { if(objectKey === 0){ return; } - let y = Math.floor(key / layer.width); - let x = key % layer.width; + const y = Math.floor(key / layer.width); + const x = key % layer.width; possibleStartPositions.push({x: x*32, y: y*32}); }); @@ -347,7 +469,7 @@ export class GameScene extends Phaser.Scene { createCollisionWithPlayer() { //add collision layer this.Layers.forEach((Layer: Phaser.Tilemaps.StaticTilemapLayer) => { - this.physics.add.collider(this.CurrentPlayer, Layer, (object1: any, object2: any) => { + this.physics.add.collider(this.CurrentPlayer, Layer, (object1: GameObject, object2: GameObject) => { //this.CurrentPlayer.say("Collision with layer : "+ (object2 as Tile).layer.name) }); Layer.setCollisionByProperty({collides: true}); @@ -363,11 +485,11 @@ export class GameScene extends Phaser.Scene { } createCollisionObject(){ - this.Objects.forEach((Object : Phaser.Physics.Arcade.Sprite) => { - this.physics.add.collider(this.CurrentPlayer, Object, (object1: any, object2: any) => { - //this.CurrentPlayer.say("Collision with object : " + (object2 as Phaser.Physics.Arcade.Sprite).texture.key) + /*this.Objects.forEach((Object : Phaser.Physics.Arcade.Sprite) => { + this.physics.add.collider(this.CurrentPlayer, Object, (object1, object2) => { + this.CurrentPlayer.say("Collision with object : " + (object2 as Phaser.Physics.Arcade.Sprite).texture.key) }); - }) + })*/ } createCurrentPlayer(){ @@ -388,10 +510,14 @@ export class GameScene extends Phaser.Scene { this.createCollisionObject(); //join room - this.GameManager.joinRoom(this.RoomId, this.startX, this.startY, PlayerAnimationNames.WalkDown, false); + this.connectionPromise.then((connection: Connection) => { + connection.joinARoom(this.RoomId, this.startX, this.startY, PlayerAnimationNames.WalkDown, false).then((userPositions: MessageUserPositionInterface[]) => { + this.initUsersPosition(userPositions); + }); - //listen event to share position of user - this.CurrentPlayer.on(hasMovedEventName, this.pushPlayerPosition.bind(this)) + //listen event to share position of user + this.CurrentPlayer.on(hasMovedEventName, this.pushPlayerPosition.bind(this)) + }); } pushPlayerPosition(event: HasMovedEvent) { @@ -423,18 +549,18 @@ export class GameScene extends Phaser.Scene { private doPushPlayerPosition(event: HasMovedEvent): void { this.lastMoveEventSent = event; this.lastSentTick = this.currentTick; - this.GameManager.pushPlayerPosition(event); + this.connection.sharePosition(event.x, event.y, event.direction, event.moving); } EventToClickOnTile(){ // debug code to get a tile properties by clicking on it - this.input.on("pointerdown", (pointer: Phaser.Input.Pointer)=>{ + /*this.input.on("pointerdown", (pointer: Phaser.Input.Pointer)=>{ //pixel position toz tile position - let tile = this.Map.getTileAt(this.Map.worldToTileX(pointer.worldX), this.Map.worldToTileY(pointer.worldY)); + const tile = this.Map.getTileAt(this.Map.worldToTileX(pointer.worldX), this.Map.worldToTileY(pointer.worldY)); if(tile){ this.CurrentPlayer.say("Your touch " + tile.layer.name); } - }); + });*/ } /** @@ -445,19 +571,46 @@ export class GameScene extends Phaser.Scene { this.currentTick = time; this.CurrentPlayer.moveUser(delta); + // Let's handle all events + while (this.pendingEvents.length !== 0) { + const event = this.pendingEvents.dequeue(); + switch (event.type) { + case "InitUserPositionEvent": + this.doInitUsersPosition(event.event); + break; + case "AddPlayerEvent": + this.doAddPlayer(event.event); + break; + case "RemovePlayerEvent": + this.doRemovePlayer(event.userId); + break; + case "UserMovedEvent": + this.doUpdatePlayerPosition(event.event); + break; + case "GroupCreatedUpdatedEvent": + this.doShareGroupPosition(event.event); + break; + case "DeleteGroupEvent": + this.doDeleteGroup(event.groupId); + break; + } + } + // Let's move all users - let updatedPlayersPositions = this.playersPositionInterpolator.getUpdatedPositions(time); + const updatedPlayersPositions = this.playersPositionInterpolator.getUpdatedPositions(time); updatedPlayersPositions.forEach((moveEvent: HasMovedEvent, userId: string) => { - let player : RemotePlayer | undefined = this.MapPlayersByKey.get(userId); + const player : RemotePlayer | undefined = this.MapPlayersByKey.get(userId); if (player === undefined) { throw new Error('Cannot find player with ID "' + userId +'"'); } player.updatePosition(moveEvent); }); - let nextSceneKey = this.checkToExit(); + const nextSceneKey = this.checkToExit(); if(nextSceneKey){ // We are completely destroying the current scene to avoid using a half-backed instance when coming back to the same map. + this.connection.closeConnection(); + this.scene.stop(); this.scene.remove(this.scene.key); this.scene.start(nextSceneKey.key, { startLayerName: nextSceneKey.hash @@ -468,23 +621,33 @@ export class GameScene extends Phaser.Scene { /** * */ - checkToExit(){ - if(this.PositionNextScene.length === 0){ + checkToExit(): {key: string, hash: string} | null { + const x = Math.floor(this.CurrentPlayer.x / 32); + const y = Math.floor(this.CurrentPlayer.y / 32); + + if (this.PositionNextScene[y] !== undefined && this.PositionNextScene[y][x] !== undefined) { + return this.PositionNextScene[y][x]; + } else { return null; } - return this.PositionNextScene.find((position : any) => { - return position.xStart <= this.CurrentPlayer.x && this.CurrentPlayer.x <= position.xEnd - && position.yStart <= this.CurrentPlayer.y && this.CurrentPlayer.y <= position.yEnd - }) } - public initUsersPosition(usersPosition: MessageUserPositionInterface[]): void { - if(!this.CurrentPlayer){ - console.error('Cannot initiate users list because map is not loaded yet') - return; - } + /** + * Called by the connexion when the full list of user position is received. + */ + private initUsersPosition(usersPosition: MessageUserPositionInterface[]): void { + this.pendingEvents.enqueue({ + type: "InitUserPositionEvent", + event: usersPosition + }); - let currentPlayerId = this.GameManager.getPlayerId(); + } + + /** + * Put all the players on the map on map load. + */ + private doInitUsersPosition(usersPosition: MessageUserPositionInterface[]): void { + const currentPlayerId = this.connection.getUserId(); // clean map this.MapPlayersByKey.forEach((player: RemotePlayer) => { @@ -502,10 +665,20 @@ export class GameScene extends Phaser.Scene { }); } + /** + * Called by the connexion when a new player arrives on a map + */ + public addPlayer(addPlayerData : AddPlayerInterface) : void { + this.pendingEvents.enqueue({ + type: "AddPlayerEvent", + event: addPlayerData + }); + } + /** * Create new player */ - public addPlayer(addPlayerData : AddPlayerInterface) : void{ + private doAddPlayer(addPlayerData : AddPlayerInterface) : void { //check if exist player, if exist, move position if(this.MapPlayersByKey.has(addPlayerData.userId)){ this.updatePlayerPosition({ @@ -515,7 +688,7 @@ export class GameScene extends Phaser.Scene { return; } //initialise player - let player = new RemotePlayer( + const player = new RemotePlayer( addPlayerData.userId, this, addPlayerData.position.x, @@ -535,9 +708,18 @@ export class GameScene extends Phaser.Scene { });*/ } + /** + * Called by the connexion when a player is removed from the map + */ public removePlayer(userId: string) { - console.log('Removing player ', userId) - let player = this.MapPlayersByKey.get(userId); + this.pendingEvents.enqueue({ + type: "RemovePlayerEvent", + userId + }); + } + + private doRemovePlayer(userId: string) { + const player = this.MapPlayersByKey.get(userId); if (player === undefined) { console.error('Cannot find user with id ', userId); } else { @@ -548,27 +730,43 @@ export class GameScene extends Phaser.Scene { this.playersPositionInterpolator.removePlayer(userId); } - updatePlayerPosition(message: MessageUserMovedInterface): void { - let player : RemotePlayer | undefined = this.MapPlayersByKey.get(message.userId); + public updatePlayerPosition(message: MessageUserMovedInterface): void { + this.pendingEvents.enqueue({ + type: "UserMovedEvent", + event: message + }); + } + + private doUpdatePlayerPosition(message: MessageUserMovedInterface): void { + const player : RemotePlayer | undefined = this.MapPlayersByKey.get(message.userId); if (player === undefined) { - throw new Error('Cannot find player with ID "' + message.userId +'"'); + //throw new Error('Cannot find player with ID "' + message.userId +'"'); + console.error('Cannot update position of player with ID "' + message.userId +'": player not found'); + return; } // We do not update the player position directly (because it is sent only every 200ms). // Instead we use the PlayersPositionInterpolator that will do a smooth animation over the next 200ms. - let playerMovement = new PlayerMovement({ x: player.x, y: player.y }, this.currentTick, message.position, this.currentTick + POSITION_DELAY); + const playerMovement = new PlayerMovement({ x: player.x, y: player.y }, this.currentTick, message.position, this.currentTick + POSITION_DELAY); this.playersPositionInterpolator.updatePlayerPosition(player.userId, playerMovement); } - shareGroupPosition(groupPositionMessage: GroupCreatedUpdatedMessageInterface) { - let groupId = groupPositionMessage.groupId; + public shareGroupPosition(groupPositionMessage: GroupCreatedUpdatedMessageInterface) { + this.pendingEvents.enqueue({ + type: "GroupCreatedUpdatedEvent", + event: groupPositionMessage + }); + } - let group = this.groups.get(groupId); + private doShareGroupPosition(groupPositionMessage: GroupCreatedUpdatedMessageInterface) { + const groupId = groupPositionMessage.groupId; + + const group = this.groups.get(groupId); if (group !== undefined) { group.setPosition(Math.round(groupPositionMessage.position.x), Math.round(groupPositionMessage.position.y)); } else { // TODO: circle radius should not be hard stored - let sprite = new Sprite( + const sprite = new Sprite( this, Math.round(groupPositionMessage.position.x), Math.round(groupPositionMessage.position.y), @@ -580,7 +778,14 @@ export class GameScene extends Phaser.Scene { } deleteGroup(groupId: string): void { - let group = this.groups.get(groupId); + this.pendingEvents.enqueue({ + type: "DeleteGroupEvent", + groupId + }); + } + + doDeleteGroup(groupId: string): void { + const group = this.groups.get(groupId); if(!group){ return; } @@ -590,8 +795,8 @@ export class GameScene extends Phaser.Scene { public static getMapKeyByUrl(mapUrlStart: string) : string { // FIXME: the key should be computed from the full URL of the map. - let startPos = mapUrlStart.indexOf('://')+3; - let endPos = mapUrlStart.indexOf(".json"); + const startPos = mapUrlStart.indexOf('://')+3; + const endPos = mapUrlStart.indexOf(".json"); return mapUrlStart.substring(startPos, endPos); } } diff --git a/front/src/Phaser/Game/PlayerMovement.ts b/front/src/Phaser/Game/PlayerMovement.ts index 1ed2b745..1458335d 100644 --- a/front/src/Phaser/Game/PlayerMovement.ts +++ b/front/src/Phaser/Game/PlayerMovement.ts @@ -23,8 +23,8 @@ export class PlayerMovement { return this.endPosition; } - let x = (this.endPosition.x - this.startPosition.x) * ((tick - this.startTick) / (this.endTick - this.startTick)) + this.startPosition.x; - let y = (this.endPosition.y - this.startPosition.y) * ((tick - this.startTick) / (this.endTick - this.startTick)) + this.startPosition.y; + const x = (this.endPosition.x - this.startPosition.x) * ((tick - this.startTick) / (this.endTick - this.startTick)) + this.startPosition.x; + const y = (this.endPosition.y - this.startPosition.y) * ((tick - this.startTick) / (this.endTick - this.startTick)) + this.startPosition.y; return { x, diff --git a/front/src/Phaser/Game/PlayersPositionInterpolator.ts b/front/src/Phaser/Game/PlayersPositionInterpolator.ts index 19e0f7bc..080c8a17 100644 --- a/front/src/Phaser/Game/PlayersPositionInterpolator.ts +++ b/front/src/Phaser/Game/PlayersPositionInterpolator.ts @@ -17,7 +17,7 @@ export class PlayersPositionInterpolator { } getUpdatedPositions(tick: number) : Map { - let positions = new Map(); + const positions = new Map(); this.playerMovements.forEach((playerMovement: PlayerMovement, userId: string) => { if (playerMovement.isOutdated(tick)) { //console.log("outdated") diff --git a/front/src/Phaser/Login/LoginScene.ts b/front/src/Phaser/Login/LoginScene.ts index 1b7ef76f..5177659b 100644 --- a/front/src/Phaser/Login/LoginScene.ts +++ b/front/src/Phaser/Login/LoginScene.ts @@ -4,7 +4,7 @@ import {TextInput} from "../Components/TextInput"; import {ClickButton} from "../Components/ClickButton"; import Image = Phaser.GameObjects.Image; import Rectangle = Phaser.GameObjects.Rectangle; -import {PLAYER_RESOURCES} from "../Entity/Character"; +import {PLAYER_RESOURCES, PlayerResourceDescriptionInterface} from "../Entity/Character"; import {cypressAsserter} from "../../Cypress/CypressAsserter"; import {SelectCharacterSceneInitDataInterface, SelectCharacterSceneName} from "./SelectCharacterScene"; @@ -40,7 +40,7 @@ export class LoginScene extends Phaser.Scene { this.load.bitmapFont(LoginTextures.mainFont, 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml'); cypressAsserter.preloadFinished(); //add player png - PLAYER_RESOURCES.forEach((playerResource: any) => { + PLAYER_RESOURCES.forEach((playerResource: PlayerResourceDescriptionInterface) => { this.load.spritesheet( playerResource.name, playerResource.img, @@ -67,7 +67,7 @@ export class LoginScene extends Phaser.Scene { this.logo = new Image(this, this.game.renderer.width - 30, this.game.renderer.height - 20, LoginTextures.icon); this.add.existing(this.logo); - let infoText = "Commands: \n - Arrows or Z,Q,S,D to move\n - SHIFT to run"; + const infoText = "Commands: \n - Arrows or Z,Q,S,D to move\n - SHIFT to run"; this.infoTextField = new TextField(this, 10, this.game.renderer.height - 35, infoText); this.input.keyboard.on('keyup-ENTER', () => { diff --git a/front/src/Phaser/Login/SelectCharacterScene.ts b/front/src/Phaser/Login/SelectCharacterScene.ts index dab9d61f..5175a7b8 100644 --- a/front/src/Phaser/Login/SelectCharacterScene.ts +++ b/front/src/Phaser/Login/SelectCharacterScene.ts @@ -3,8 +3,9 @@ import {TextField} from "../Components/TextField"; import {ClickButton} from "../Components/ClickButton"; import Image = Phaser.GameObjects.Image; import Rectangle = Phaser.GameObjects.Rectangle; -import {PLAYER_RESOURCES} from "../Entity/Character"; +import {PLAYER_RESOURCES, PlayerResourceDescriptionInterface} from "../Entity/Character"; import {GameSceneInitInterface} from "../Game/GameScene"; +import {StartMapInterface} from "../../Connection"; //todo: put this constants in a dedicated file export const SelectCharacterSceneName = "SelectCharacterScene"; @@ -47,7 +48,7 @@ export class SelectCharacterScene extends Phaser.Scene { // Note: arcade.png from the Phaser 3 examples at: https://github.com/photonstorm/phaser3-examples/tree/master/public/assets/fonts/bitmap this.load.bitmapFont(LoginTextures.mainFont, 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml'); //add player png - PLAYER_RESOURCES.forEach((playerResource: any) => { + PLAYER_RESOURCES.forEach((playerResource: PlayerResourceDescriptionInterface) => { this.load.spritesheet( playerResource.name, playerResource.img, @@ -63,7 +64,7 @@ export class SelectCharacterScene extends Phaser.Scene { this.pressReturnField = new TextField(this, this.game.renderer.width / 2, 230, 'Press enter to start'); this.pressReturnField.setOrigin(0.5).setCenterAlign() - let rectangleXStart = this.game.renderer.width / 2 - (this.nbCharactersPerRow / 2) * 32 + 16; + const rectangleXStart = this.game.renderer.width / 2 - (this.nbCharactersPerRow / 2) * 32 + 16; this.selectedRectangle = this.add.rectangle(rectangleXStart, 90, 32, 32).setStrokeStyle(2, 0xFFFFFF); @@ -103,8 +104,8 @@ export class SelectCharacterScene extends Phaser.Scene { this.createCurrentPlayer(); if (window.localStorage) { - let playerNumberStr: string = window.localStorage.getItem('selectedPlayer') ?? '0'; - let playerNumber: number = Number(playerNumberStr); + const playerNumberStr: string = window.localStorage.getItem('selectedPlayer') ?? '0'; + const playerNumber: number = Number(playerNumberStr); this.selectedRectangleXPos = playerNumber % this.nbCharactersPerRow; this.selectedRectangleYPos = Math.floor(playerNumber / this.nbCharactersPerRow); this.updateSelectedPlayer(); @@ -115,63 +116,60 @@ export class SelectCharacterScene extends Phaser.Scene { this.pressReturnField.setVisible(!!(Math.floor(time / 500) % 2)); } - private async login(name: string) { - return gameManager.connect(name, this.selectedPlayer.texture.key).then(() => { - // Do we have a start URL in the address bar? If so, let's redirect to this address - let instanceAndMapUrl = this.findMapUrl(); - if (instanceAndMapUrl !== null) { - let [mapUrl, instance] = instanceAndMapUrl; - let key = gameManager.loadMap(mapUrl, this.scene, instance); - this.scene.start(key, { - startLayerName: window.location.hash ? window.location.hash.substr(1) : undefined - } as GameSceneInitInterface); - return mapUrl; - } else { - // If we do not have a map address in the URL, let's ask the server for a start map. - return gameManager.loadStartMap().then((scene : any) => { - if (!scene) { - return; - } - let key = gameManager.loadMap(window.location.protocol + "//" + scene.mapUrlStart, this.scene, scene.startInstance); - this.scene.start(key); - return scene; - }).catch((err) => { - console.error(err); - throw err; - }); - } - }).catch((err) => { - console.error(err); - throw err; - }); + private async login(name: string): Promise { + gameManager.storePlayerDetails(name, this.selectedPlayer.texture.key); + + // Do we have a start URL in the address bar? If so, let's redirect to this address + const instanceAndMapUrl = this.findMapUrl(); + if (instanceAndMapUrl !== null) { + const [mapUrl, instance] = instanceAndMapUrl; + const key = gameManager.loadMap(mapUrl, this.scene, instance); + this.scene.start(key, { + startLayerName: window.location.hash ? window.location.hash.substr(1) : undefined + } as GameSceneInitInterface); + return { + mapUrlStart: mapUrl, + startInstance: instance + }; + } else { + // If we do not have a map address in the URL, let's ask the server for a start map. + return gameManager.loadStartMap().then((startMap: StartMapInterface) => { + const key = gameManager.loadMap(window.location.protocol + "//" + startMap.mapUrlStart, this.scene, startMap.startInstance); + this.scene.start(key); + return startMap; + }).catch((err) => { + console.error(err); + throw err; + }); + } } /** * Returns the map URL and the instance from the current URL */ private findMapUrl(): [string, string]|null { - let path = window.location.pathname; + const path = window.location.pathname; if (!path.startsWith('/_/')) { return null; } - let instanceAndMap = path.substr(3); - let firstSlash = instanceAndMap.indexOf('/'); + const instanceAndMap = path.substr(3); + const firstSlash = instanceAndMap.indexOf('/'); if (firstSlash === -1) { return null; } - let instance = instanceAndMap.substr(0, firstSlash); + const instance = instanceAndMap.substr(0, firstSlash); return [window.location.protocol+'//'+instanceAndMap.substr(firstSlash+1), instance]; } createCurrentPlayer(): void { for (let i = 0; i = new Map(); + get(event: UserInputEvent): boolean { - return this.KeysCode[event] || false; + return this.KeysCode.get(event) || false; } - set(event: UserInputEvent, value: boolean): boolean { - return this.KeysCode[event] = true; + set(event: UserInputEvent, value: boolean): void { + this.KeysCode.set(event, value); } } @@ -55,7 +52,7 @@ export class UserInputManager { } getEventListForGameTick(): ActiveEventList { - let eventsMap = new ActiveEventList(); + const eventsMap = new ActiveEventList(); this.KeysCode.forEach(d => { if (d. keyInstance.isDown) { eventsMap.set(d.event, true); diff --git a/front/src/WebRtc/MediaManager.ts b/front/src/WebRtc/MediaManager.ts index 359eac67..03736e6e 100644 --- a/front/src/WebRtc/MediaManager.ts +++ b/front/src/WebRtc/MediaManager.ts @@ -1,53 +1,53 @@ -const videoConstraint: {width : any, height: any, facingMode : string} = { +const videoConstraint: boolean|MediaTrackConstraints = { width: { ideal: 1280 }, height: { ideal: 720 }, facingMode: "user" }; export class MediaManager { localStream: MediaStream|null = null; - remoteVideo: Array = new Array(); + private remoteVideo: Map = new Map(); myCamVideo: HTMLVideoElement; - cinemaClose: any = null; - cinema: any = null; - microphoneClose: any = null; - microphone: any = null; + cinemaClose: HTMLImageElement; + cinema: HTMLImageElement; + microphoneClose: HTMLImageElement; + microphone: HTMLImageElement; webrtcInAudio: HTMLAudioElement; - constraintsMedia : {audio : any, video : any} = { + constraintsMedia : MediaStreamConstraints = { audio: true, video: videoConstraint }; - updatedLocalStreamCallBack : Function; + updatedLocalStreamCallBack : (media: MediaStream) => void; - constructor(updatedLocalStreamCallBack : Function) { + constructor(updatedLocalStreamCallBack : (media: MediaStream) => void) { this.updatedLocalStreamCallBack = updatedLocalStreamCallBack; this.myCamVideo = this.getElementByIdOrFail('myCamVideo'); this.webrtcInAudio = this.getElementByIdOrFail('audio-webrtc-in'); this.webrtcInAudio.volume = 0.2; - this.microphoneClose = document.getElementById('microphone-close'); + this.microphoneClose = this.getElementByIdOrFail('microphone-close'); this.microphoneClose.style.display = "none"; - this.microphoneClose.addEventListener('click', (e: any) => { + this.microphoneClose.addEventListener('click', (e: MouseEvent) => { e.preventDefault(); this.enabledMicrophone(); //update tracking }); - this.microphone = document.getElementById('microphone'); - this.microphone.addEventListener('click', (e: any) => { + this.microphone = this.getElementByIdOrFail('microphone'); + this.microphone.addEventListener('click', (e: MouseEvent) => { e.preventDefault(); this.disabledMicrophone(); //update tracking }); - this.cinemaClose = document.getElementById('cinema-close'); + this.cinemaClose = this.getElementByIdOrFail('cinema-close'); this.cinemaClose.style.display = "none"; - this.cinemaClose.addEventListener('click', (e: any) => { + this.cinemaClose.addEventListener('click', (e: MouseEvent) => { e.preventDefault(); this.enabledCamera(); //update tracking }); - this.cinema = document.getElementById('cinema'); - this.cinema.addEventListener('click', (e: any) => { + this.cinema = this.getElementByIdOrFail('cinema'); + this.cinema.addEventListener('click', (e: MouseEvent) => { e.preventDefault(); this.disabledCamera(); //update tracking @@ -55,7 +55,7 @@ export class MediaManager { } activeVisio(){ - let webRtc = this.getElementByIdOrFail('webRtc'); + const webRtc = this.getElementByIdOrFail('webRtc'); webRtc.classList.add('active'); } @@ -63,7 +63,7 @@ export class MediaManager { this.cinemaClose.style.display = "none"; this.cinema.style.display = "block"; this.constraintsMedia.video = videoConstraint; - this.getCamera().then((stream) => { + this.getCamera().then((stream: MediaStream) => { this.updatedLocalStreamCallBack(stream); }); } @@ -107,8 +107,13 @@ export class MediaManager { } //get camera - getCamera() { + getCamera(): Promise { let promise = null; + + if (navigator.mediaDevices === undefined) { + return Promise.reject(new Error('Unable to access your camera or microphone. Your browser is too old (or you are running a development version of WorkAdventure on Firefox)')); + } + try { promise = navigator.mediaDevices.getUserMedia(this.constraintsMedia) .then((stream: MediaStream) => { @@ -123,11 +128,12 @@ export class MediaManager { return stream; }).catch((err) => { - console.info(`error get media {video: ${this.constraintsMedia.video}},{audio: ${this.constraintsMedia.audio}}`,err); + console.info("error get media ", this.constraintsMedia.video, this.constraintsMedia.audio, err); this.localStream = null; + throw err; }); } catch (e) { - promise = Promise.reject(false); + promise = Promise.reject(e); } return promise; } @@ -138,9 +144,9 @@ export class MediaManager { */ addActiveVideo(userId : string, userName: string = ""){ this.webrtcInAudio.play(); - let elementRemoteVideo = this.getElementByIdOrFail("activeCam"); + const elementRemoteVideo = this.getElementByIdOrFail("activeCam"); userName = userName.toUpperCase(); - let color = this.getColorByString(userName); + const color = this.getColorByString(userName); elementRemoteVideo.insertAdjacentHTML('beforeend', `
@@ -150,7 +156,7 @@ export class MediaManager {
`); - this.remoteVideo[(userId as any)] = document.getElementById(userId); + this.remoteVideo.set(userId, this.getElementByIdOrFail(userId)); } /** @@ -158,7 +164,7 @@ export class MediaManager { * @param userId */ disabledMicrophoneByUserId(userId: string){ - let element = document.getElementById(`microphone-${userId}`); + const element = document.getElementById(`microphone-${userId}`); if(!element){ return; } @@ -170,7 +176,7 @@ export class MediaManager { * @param userId */ enabledMicrophoneByUserId(userId: string){ - let element = document.getElementById(`microphone-${userId}`); + const element = document.getElementById(`microphone-${userId}`); if(!element){ return; } @@ -215,7 +221,12 @@ export class MediaManager { * @param stream */ addStreamRemoteVideo(userId : string, stream : MediaStream){ - this.remoteVideo[(userId as any)].srcObject = stream; + const remoteVideo = this.remoteVideo.get(userId); + if (remoteVideo === undefined) { + console.error('Unable to find video for ', userId); + return; + } + remoteVideo.srcObject = stream; } /** @@ -223,15 +234,16 @@ export class MediaManager { * @param userId */ removeActiveVideo(userId : string){ - let element = document.getElementById(`div-${userId}`); + const element = document.getElementById(`div-${userId}`); if(!element){ return; } element.remove(); + this.remoteVideo.delete(userId); } isConnecting(userId : string): void { - let connectingSpinnerDiv = this.getSpinner(userId); + const connectingSpinnerDiv = this.getSpinner(userId); if (connectingSpinnerDiv === null) { return; } @@ -239,7 +251,7 @@ export class MediaManager { } isConnected(userId : string): void { - let connectingSpinnerDiv = this.getSpinner(userId); + const connectingSpinnerDiv = this.getSpinner(userId); if (connectingSpinnerDiv === null) { return; } @@ -247,11 +259,11 @@ export class MediaManager { } isError(userId : string): void { - let element = document.getElementById(`div-${userId}`); + const element = document.getElementById(`div-${userId}`); if(!element){ return; } - let errorDiv = element.getElementsByClassName('rtc-error').item(0) as HTMLDivElement|null; + const errorDiv = element.getElementsByClassName('rtc-error').item(0) as HTMLDivElement|null; if (errorDiv === null) { return; } @@ -259,11 +271,11 @@ export class MediaManager { } private getSpinner(userId : string): HTMLDivElement|null { - let element = document.getElementById(`div-${userId}`); + const element = document.getElementById(`div-${userId}`); if(!element){ return null; } - let connnectingSpinnerDiv = element.getElementsByClassName('connecting-spinner').item(0) as HTMLDivElement|null; + const connnectingSpinnerDiv = element.getElementsByClassName('connecting-spinner').item(0) as HTMLDivElement|null; return connnectingSpinnerDiv; } @@ -280,14 +292,14 @@ export class MediaManager { } let color = '#'; for (let i = 0; i < 3; i++) { - let value = (hash >> (i * 8)) & 255; + const value = (hash >> (i * 8)) & 255; color += ('00' + value.toString(16)).substr(-2); } return color; } private getElementByIdOrFail(id: string): T { - let elem = document.getElementById(id); + const elem = document.getElementById(id); if (elem === null) { throw new Error("Cannot find HTML element with id '"+id+"'"); } diff --git a/front/src/WebRtc/SimplePeer.ts b/front/src/WebRtc/SimplePeer.ts index da1ae3db..381b3ac2 100644 --- a/front/src/WebRtc/SimplePeer.ts +++ b/front/src/WebRtc/SimplePeer.ts @@ -1,7 +1,12 @@ -import {ConnectionInterface, WebRtcDisconnectMessageInterface, WebRtcStartMessageInterface} from "../Connection"; +import { + Connection, + WebRtcDisconnectMessageInterface, + WebRtcSignalMessageInterface, + WebRtcStartMessageInterface +} from "../Connection"; import {MediaManager} from "./MediaManager"; import * as SimplePeerNamespace from "simple-peer"; -let Peer: SimplePeerNamespace.SimplePeer = require('simple-peer'); +const Peer: SimplePeerNamespace.SimplePeer = require('simple-peer'); export interface UserSimplePeer{ userId: string; @@ -13,7 +18,7 @@ export interface UserSimplePeer{ * This class manages connections to all the peers in the same group as me. */ export class SimplePeer { - private Connection: ConnectionInterface; + private Connection: Connection; private WebRtcRoomId: string; private Users: Array = new Array(); @@ -21,7 +26,7 @@ export class SimplePeer { private PeerConnectionArray: Map = new Map(); - constructor(Connection: ConnectionInterface, WebRtcRoomId: string = "test-webrtc") { + constructor(Connection: Connection, WebRtcRoomId: string = "test-webrtc") { this.Connection = Connection; this.WebRtcRoomId = WebRtcRoomId; this.MediaManager = new MediaManager((stream : MediaStream) => { @@ -36,7 +41,7 @@ export class SimplePeer { private initialise() { //receive signal by gemer - this.Connection.receiveWebrtcSignal((message: any) => { + this.Connection.receiveWebrtcSignal((message: WebRtcSignalMessageInterface) => { this.receiveWebrtcSignal(message); }); @@ -95,7 +100,7 @@ export class SimplePeer { let name = user.name; if(!name){ - let userSearch = this.Users.find((userSearch: UserSimplePeer) => userSearch.userId === user.userId); + const userSearch = this.Users.find((userSearch: UserSimplePeer) => userSearch.userId === user.userId); if(userSearch) { name = userSearch.name; } @@ -103,7 +108,7 @@ export class SimplePeer { this.MediaManager.removeActiveVideo(user.userId); this.MediaManager.addActiveVideo(user.userId, name); - let peer : SimplePeerNamespace.Instance = new Peer({ + const peer : SimplePeerNamespace.Instance = new Peer({ initiator: user.initiator ? user.initiator : false, reconnectTimer: 10000, config: { @@ -122,7 +127,7 @@ export class SimplePeer { this.PeerConnectionArray.set(user.userId, peer); //start listen signal for the peer connection - peer.on('signal', (data: any) => { + peer.on('signal', (data: unknown) => { this.sendWebrtcSignal(data, user.userId); }); @@ -159,6 +164,7 @@ export class SimplePeer { this.closeConnection(user.userId); }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any peer.on('error', (err: any) => { console.error(`error => ${user.userId} => ${err.code}`, err); this.MediaManager.isError(user.userId); @@ -170,7 +176,7 @@ export class SimplePeer { }); peer.on('data', (chunk: Buffer) => { - let data = JSON.parse(chunk.toString('utf8')); + const data = JSON.parse(chunk.toString('utf8')); if(data.type === "stream"){ this.stream(user.userId, data.stream); } @@ -187,7 +193,7 @@ export class SimplePeer { private closeConnection(userId : string) { try { this.MediaManager.removeActiveVideo(userId); - let peer = this.PeerConnectionArray.get(userId); + const peer = this.PeerConnectionArray.get(userId); if (peer === undefined) { console.warn("Tried to close connection for user "+userId+" but could not find user") return; @@ -203,12 +209,18 @@ export class SimplePeer { } } + public closeAllConnections() { + for (const userId of this.PeerConnectionArray.keys()) { + this.closeConnection(userId); + } + } + /** * * @param userId * @param data */ - private sendWebrtcSignal(data: any, userId : string) { + private sendWebrtcSignal(data: unknown, userId : string) { try { this.Connection.sendWebrtcSignal(data, this.WebRtcRoomId, null, userId); }catch (e) { @@ -216,13 +228,14 @@ export class SimplePeer { } } - private receiveWebrtcSignal(data: any) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private receiveWebrtcSignal(data: WebRtcSignalMessageInterface) { try { //if offer type, create peer connection if(data.signal.type === "offer"){ this.createPeerConnection(data); } - let peer = this.PeerConnectionArray.get(data.userId); + const peer = this.PeerConnectionArray.get(data.userId); if (peer !== undefined) { peer.signal(data.signal); } else { @@ -251,10 +264,10 @@ export class SimplePeer { * * @param userId */ - private addMedia (userId : any = null) { + private addMedia (userId : string) { try { - let localStream: MediaStream|null = this.MediaManager.localStream; - let peer = this.PeerConnectionArray.get(userId); + const localStream: MediaStream|null = this.MediaManager.localStream; + const peer = this.PeerConnectionArray.get(userId); if(localStream === null) { //send fake signal if(peer === undefined){ diff --git a/front/src/index.ts b/front/src/index.ts index 843925ac..6221d3ad 100644 --- a/front/src/index.ts +++ b/front/src/index.ts @@ -24,7 +24,7 @@ const config: GameConfig = { cypressAsserter.gameStarted(); -let game = new Phaser.Game(config); +const game = new Phaser.Game(config); window.addEventListener('resize', function (event) { game.scale.resize(window.innerWidth / RESOLUTION, window.innerHeight / RESOLUTION); diff --git a/front/tests/Phaser/Game/PlayerMovementTest.ts b/front/tests/Phaser/Game/PlayerMovementTest.ts index e65dbec8..ce2e2767 100644 --- a/front/tests/Phaser/Game/PlayerMovementTest.ts +++ b/front/tests/Phaser/Game/PlayerMovementTest.ts @@ -3,7 +3,7 @@ import {PlayerMovement} from "../../../src/Phaser/Game/PlayerMovement"; describe("Interpolation / Extrapolation", () => { it("should interpolate", () => { - let playerMovement = new PlayerMovement({ + const playerMovement = new PlayerMovement({ x: 100, y: 200 }, 42000, { @@ -39,7 +39,7 @@ describe("Interpolation / Extrapolation", () => { }); it("should not extrapolate if we stop", () => { - let playerMovement = new PlayerMovement({ + const playerMovement = new PlayerMovement({ x: 100, y: 200 }, 42000, { @@ -57,7 +57,7 @@ describe("Interpolation / Extrapolation", () => { }); it("should should keep moving until it stops", () => { - let playerMovement = new PlayerMovement({ + const playerMovement = new PlayerMovement({ x: 100, y: 200 }, 42000, { diff --git a/front/tsconfig.json b/front/tsconfig.json index 84882e74..1661efa2 100644 --- a/front/tsconfig.json +++ b/front/tsconfig.json @@ -6,6 +6,7 @@ "noImplicitAny": true, "module": "CommonJS", "target": "es5", + "downlevelIteration": true, "jsx": "react", "allowJs": true, diff --git a/front/yarn.lock b/front/yarn.lock index 7ed8c19a..126536e0 100644 --- a/front/yarn.lock +++ b/front/yarn.lock @@ -2602,6 +2602,11 @@ levn@^0.3.0, levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" +linked-list-typescript@^1.0.11: + version "1.0.15" + resolved "https://registry.yarnpkg.com/linked-list-typescript/-/linked-list-typescript-1.0.15.tgz#faeed93cf9203f102e2158c29edcddda320abe82" + integrity sha512-RIyUu9lnJIyIaMe63O7/aFv/T2v3KsMFuXMBbUQCHX+cgtGro86ETDj5ed0a8gQL2+DFjzYYsgVG4I36/cUwgw== + loader-runner@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357" @@ -3400,6 +3405,13 @@ queue-microtask@^1.1.0: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.1.2.tgz#139bf8186db0c545017ec66c2664ac646d5c571e" integrity sha512-F9wwNePtXrzZenAB3ax0Y8TSKGvuB7Qw16J30hspEUTbfUM+H827XyN3rlpwhVmtm5wuZtbKIHjOnwDn7MUxWQ== +queue-typescript@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/queue-typescript/-/queue-typescript-1.0.1.tgz#2d7842fc3b3e0e3f33d077887a8f2a5bb0baf460" + integrity sha512-tkK08uPfmpPl0cX1WRSU3EoNb/T5zSoZPGkkpfGX4E8QayWvEmLS2cI3pFngNPkNTCU5pCDQ1IwlzN0L5gdFPg== + dependencies: + linked-list-typescript "^1.0.11" + randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.3, randombytes@^2.0.5: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"