diff --git a/front/src/Connection.ts b/front/src/Connection.ts index fc8fa1ed..cf7a3856 100644 --- a/front/src/Connection.ts +++ b/front/src/Connection.ts @@ -98,10 +98,10 @@ export interface GroupCreatedUpdatedMessageInterface { } export interface ConnectionInterface { - socket: any; - token: string; - name: string; - userId: string; + socket: Socket|null; + token: string|null; + name: string|null; + userId: string|null; createConnection(name: string, characterSelected: string): Promise; @@ -112,7 +112,7 @@ export interface ConnectionInterface { sharePosition(x: number, y: number, direction: string, moving: boolean): void; /*webrtc*/ - sendWebrtcSignal(signal: any, roomId: string, userId?: string, receiverId?: string): void; + sendWebrtcSignal(signal: any, roomId: string, userId?: string|null, receiverId?: string): void; receiveWebrtcSignal(callBack: Function): void; @@ -122,15 +122,15 @@ export interface ConnectionInterface { } export class Connection implements ConnectionInterface { - socket: Socket; - token: string; - name: string; // TODO: drop "name" storage here - character: string; - userId: string; + 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; GameManager: GameManager; - lastPositionShared: PointInterface = null; + lastPositionShared: PointInterface|null = null; lastRoom: string|null = null; constructor(GameManager: GameManager) { @@ -156,6 +156,13 @@ export class Connection implements ConnectionInterface { }); } + private getSocket(): Socket { + if (this.socket === null) { + throw new Error('Socket not initialized while using Connection') + } + return this.socket; + } + /** * * @param character @@ -171,7 +178,7 @@ export class Connection implements ConnectionInterface { this.onUserLeft(); return new Promise((resolve, reject) => { - this.socket.emit(EventMessage.SET_PLAYER_DETAILS, { + this.getSocket().emit(EventMessage.SET_PLAYER_DETAILS, { name: this.name, character: this.character } as SetPlayerDetailsMessage, (id: string) => { @@ -215,7 +222,7 @@ export class Connection implements ConnectionInterface { } joinARoom(roomId: string, startX: number, startY: number, direction: string, moving: boolean): void { - this.socket.emit(EventMessage.JOIN_ROOM, { roomId, position: {x: startX, y: startY, direction, moving }}, (userPositions: MessageUserPositionInterface[]) => { + this.getSocket().emit(EventMessage.JOIN_ROOM, { roomId, position: {x: startX, y: startY, direction, moving }}, (userPositions: MessageUserPositionInterface[]) => { this.GameManager.initUsersPosition(userPositions); }); this.lastRoom = roomId; @@ -227,42 +234,42 @@ export class Connection implements ConnectionInterface { } let point = new Point(x, y, direction, moving); this.lastPositionShared = point; - this.socket.emit(EventMessage.USER_POSITION, point); + this.getSocket().emit(EventMessage.USER_POSITION, point); } private onUserJoins(): void { - this.socket.on(EventMessage.JOIN_ROOM, (message: MessageUserJoined) => { + this.getSocket().on(EventMessage.JOIN_ROOM, (message: MessageUserJoined) => { this.GameManager.onUserJoins(message); }); } private onUserMoved(): void { - this.socket.on(EventMessage.USER_MOVED, (message: MessageUserMovedInterface) => { + this.getSocket().on(EventMessage.USER_MOVED, (message: MessageUserMovedInterface) => { this.GameManager.onUserMoved(message); }); } private onUserLeft(): void { - this.socket.on(EventMessage.USER_LEFT, (userId: string) => { + this.getSocket().on(EventMessage.USER_LEFT, (userId: string) => { this.GameManager.onUserLeft(userId); }); } private groupUpdatedOrCreated(): void { - this.socket.on(EventMessage.GROUP_CREATE_UPDATE, (groupCreateUpdateMessage: GroupCreatedUpdatedMessageInterface) => { + 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); }) } private groupDeleted(): void { - this.socket.on(EventMessage.GROUP_DELETE, (groupId: string) => { + this.getSocket().on(EventMessage.GROUP_DELETE, (groupId: string) => { this.GameManager.deleteGroup(groupId); }) } - sendWebrtcSignal(signal: any, roomId: string, userId? : string, receiverId? : string) { - return this.socket.emit(EventMessage.WEBRTC_SIGNAL, { + sendWebrtcSignal(signal: any, roomId: string, userId? : string|null, receiverId? : string) { + return this.getSocket().emit(EventMessage.WEBRTC_SIGNAL, { userId: userId ? userId : this.userId, receiverId: receiverId ? receiverId : this.userId, roomId: roomId, @@ -271,31 +278,34 @@ export class Connection implements ConnectionInterface { } receiveWebrtcStart(callback: Function) { - this.socket.on(EventMessage.WEBRTC_START, callback); + this.getSocket().on(EventMessage.WEBRTC_START, callback); } receiveWebrtcSignal(callback: Function) { - return this.socket.on(EventMessage.WEBRTC_SIGNAL, callback); + return this.getSocket().on(EventMessage.WEBRTC_SIGNAL, callback); } private errorMessage(): void { - this.socket.on(EventMessage.MESSAGE_ERROR, (message: string) => { + this.getSocket().on(EventMessage.MESSAGE_ERROR, (message: string) => { console.error(EventMessage.MESSAGE_ERROR, message); }) } private disconnectServer(): void { - this.socket.on(EventMessage.CONNECT_ERROR, () => { + this.getSocket().on(EventMessage.CONNECT_ERROR, () => { this.GameManager.switchToDisconnectedScene(); }); - this.socket.on(EventMessage.RECONNECT, () => { + this.getSocket().on(EventMessage.RECONNECT, () => { this.connectSocketServer(); + if (this.lastPositionShared === null) { + throw new Error('No last position shared found while reconnecting'); + } this.GameManager.reconnectToGameScene(this.lastPositionShared); }); } disconnectMessage(callback: Function): void { - this.socket.on(EventMessage.WEBRTC_DISCONNECT, callback); + this.getSocket().on(EventMessage.WEBRTC_DISCONNECT, callback); } } diff --git a/front/src/Logger/MessageUI.ts b/front/src/Logger/MessageUI.ts index a73c2418..6011fb73 100644 --- a/front/src/Logger/MessageUI.ts +++ b/front/src/Logger/MessageUI.ts @@ -3,18 +3,18 @@ export class MessageUI { static warningMessage(text: string){ this.removeMessage(); let body = document.getElementById("body"); - body.insertAdjacentHTML('afterbegin', ` + body?.insertAdjacentHTML('afterbegin', `
${text}
`); } - static removeMessage(id : string = null) { + static removeMessage(id : string|null = null) { if(!id){ let messages = document.getElementsByClassName("message-info"); for (let i = 0; i < messages.length; i++){ - messages.item(i).remove(); + messages.item(i)?.remove(); } return; } @@ -24,4 +24,4 @@ export class MessageUI { } previousElement.remove(); } -} \ No newline at end of file +} diff --git a/front/src/Phaser/Entity/PlayableCaracter.ts b/front/src/Phaser/Entity/Character.ts similarity index 62% rename from front/src/Phaser/Entity/PlayableCaracter.ts rename to front/src/Phaser/Entity/Character.ts index e1b774ef..ec0167eb 100644 --- a/front/src/Phaser/Entity/PlayableCaracter.ts +++ b/front/src/Phaser/Entity/Character.ts @@ -24,14 +24,31 @@ export const PLAYER_RESOURCES: Array = [ {name: "Female8", img: "resources/characters/pipoya/Female 16-4.png"/*, x: 128, y: 128*/} ]; -export class PlayableCaracter extends Phaser.Physics.Arcade.Sprite { - private bubble: SpeechBubble; +interface AnimationData { + key: string; + frameRate: number; + repeat: number; + frameModel: string; //todo use an enum + frameStart: number; + frameEnd: number; +} + +export abstract class Character extends Phaser.Physics.Arcade.Sprite { + private bubble: SpeechBubble|null = null; private readonly playerName: BitmapText; public PlayerValue: string; public PlayerTexture: string; - constructor(scene: Phaser.Scene, x: number, y: number, texture: string, name: string, frame?: string | number) { + constructor(scene: Phaser.Scene, + x: number, + y: number, + texture: string, + name: string, + direction: string, + moving: boolean, + frame?: string | number + ) { super(scene, x, y, texture, frame); this.PlayerValue = name; @@ -51,6 +68,64 @@ export class PlayableCaracter extends Phaser.Physics.Arcade.Sprite { this.setDepth(-1); this.scene.events.on('postupdate', this.postupdate.bind(this)); + + this.initAnimation(); + this.playAnimation(direction, moving); + } + + private initAnimation(): void { + this.getPlayerAnimations(this.PlayerTexture).forEach(d => { + this.scene.anims.create({ + key: d.key, + frames: this.scene.anims.generateFrameNumbers(d.frameModel, {start: d.frameStart, end: d.frameEnd}), + frameRate: d.frameRate, + repeat: d.repeat + }); + }) + } + + private getPlayerAnimations(name: string): AnimationData[] { + return [{ + key: `${name}-${PlayerAnimationNames.WalkDown}`, + frameModel: name, + frameStart: 0, + frameEnd: 2, + frameRate: 10, + repeat: -1 + }, { + key: `${name}-${PlayerAnimationNames.WalkLeft}`, + frameModel: name, + frameStart: 3, + frameEnd: 5, + frameRate: 10, + repeat: -1 + }, { + key: `${name}-${PlayerAnimationNames.WalkRight}`, + frameModel: name, + frameStart: 6, + frameEnd: 8, + frameRate: 10, + repeat: -1 + }, { + key: `${name}-${PlayerAnimationNames.WalkUp}`, + frameModel: name, + frameStart: 9, + frameEnd: 11, + frameRate: 10, + repeat: -1 + }]; + } + + protected playAnimation(direction : string, moving: boolean): void { + if (moving && (!this.anims.currentAnim || this.anims.currentAnim.key !== direction)) { + this.play(this.PlayerTexture+'-'+direction, true); + } else if (!moving) { + /*if (this.anims.currentAnim) { + this.anims.stop(); + }*/ + this.play(this.PlayerTexture+'-'+direction, true); + this.stop(); + } } move(x: number, y: number) { @@ -91,8 +166,10 @@ export class PlayableCaracter extends Phaser.Physics.Arcade.Sprite { this.bubble = new SpeechBubble(this.scene, this, text) //todo make the bubble destroy on player movement? setTimeout(() => { - this.bubble.destroy(); - this.bubble = null; + if (this.bubble !== null) { + this.bubble.destroy(); + this.bubble = null; + } }, 3000) } diff --git a/front/src/Phaser/Entity/RemotePlayer.ts b/front/src/Phaser/Entity/RemotePlayer.ts new file mode 100644 index 00000000..36911bb6 --- /dev/null +++ b/front/src/Phaser/Entity/RemotePlayer.ts @@ -0,0 +1,38 @@ +import {GameScene} from "../Game/GameScene"; +import {PointInterface} from "../../Connection"; +import {Character} from "../Entity/Character"; + +/** + * Class representing the sprite of a remote player (a player that plays on another computer) + */ +export class RemotePlayer extends Character { + userId: string; + previousDirection: string; + wasMoving: boolean; + + constructor( + userId: string, + Scene: GameScene, + x: number, + y: number, + name: string, + PlayerTexture: string, + direction: string, + moving: boolean + ) { + super(Scene, x, y, PlayerTexture, name, direction, moving, 1); + + //set data + this.userId = userId; + + //the current player model should be push away by other players to prevent conflict + //this.setImmovable(false); + } + + updatePosition(position: PointInterface): void { + this.playAnimation(position.direction, position.moving); + this.setX(position.x); + this.setY(position.y); + this.setDepth(position.y); + } +} diff --git a/front/src/Phaser/Entity/SpeechBubble.ts b/front/src/Phaser/Entity/SpeechBubble.ts index 51aaa169..f2385290 100644 --- a/front/src/Phaser/Entity/SpeechBubble.ts +++ b/front/src/Phaser/Entity/SpeechBubble.ts @@ -1,5 +1,5 @@ import Scene = Phaser.Scene; -import {PlayableCaracter} from "./PlayableCaracter"; +import {Character} from "./Character"; export class SpeechBubble { private bubble: Phaser.GameObjects.Graphics; @@ -11,7 +11,7 @@ export class SpeechBubble { * @param player * @param text */ - constructor(scene: Scene, player: PlayableCaracter, text: string = "") { + constructor(scene: Scene, player: Character, text: string = "") { let bubbleHeight = 50; let bubblePadding = 10; @@ -76,13 +76,10 @@ export class SpeechBubble { this.content.setPosition(this.bubble.x + (bubbleWidth / 2) - (bounds.width / 2), this.bubble.y + (bubbleHeight / 2) - (bounds.height / 2)); } } - + destroy(): void { this.bubble.setVisible(false) //todo find a better way this.bubble.destroy(); - this.bubble = null; this.content.destroy(); - this.content = null; } - -} \ No newline at end of file +} diff --git a/front/src/Phaser/Game/GameManager.ts b/front/src/Phaser/Game/GameManager.ts index 7f895048..7eef49b4 100644 --- a/front/src/Phaser/Game/GameManager.ts +++ b/front/src/Phaser/Game/GameManager.ts @@ -9,7 +9,7 @@ import { Point, PointInterface } from "../../Connection"; -import {SimplePeerInterface, SimplePeer} from "../../WebRtc/SimplePeer"; +import {SimplePeer} from "../../WebRtc/SimplePeer"; import {AddPlayerInterface} from "./AddPlayerInterface"; import {ReconnectingSceneName} from "../Reconnecting/ReconnectingScene"; @@ -35,7 +35,7 @@ export class GameManager { private ConnectionInstance: Connection; private currentGameScene: GameScene; private playerName: string; - SimplePeer : SimplePeerInterface; + SimplePeer : SimplePeer; private characterUserSelected: string; constructor() { @@ -139,7 +139,7 @@ export class GameManager { return this.playerName; } - getPlayerId(): string { + getPlayerId(): string|null { return this.ConnectionInstance.userId; } @@ -155,9 +155,8 @@ export class GameManager { let sceneKey = GameScene.getMapKeyByUrl(mapUrl); let gameIndex = scene.getIndex(sceneKey); - let game : Phaser.Scene = null; if(gameIndex === -1){ - game = GameScene.createFromUrl(mapUrl, instance); + let game : Phaser.Scene = GameScene.createFromUrl(mapUrl, instance); scene.add(sceneKey, game, false); } return sceneKey; diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 5cf89c16..934831e1 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -4,10 +4,10 @@ import { MessageUserMovedInterface, MessageUserPositionInterface, PointInterface, PositionInterface } from "../../Connection"; -import {CurrentGamerInterface, GamerInterface, hasMovedEventName, Player} from "../Player/Player"; +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/PlayableCaracter"; +import {PLAYER_RESOURCES} from "../Entity/Character"; import Texture = Phaser.Textures.Texture; import Sprite = Phaser.GameObjects.Sprite; import CanvasTexture = Phaser.Textures.CanvasTexture; @@ -15,6 +15,7 @@ import {AddPlayerInterface} from "./AddPlayerInterface"; import {PlayerAnimationNames} from "../Player/Animation"; import {PlayerMovement} from "./PlayerMovement"; import {PlayersPositionInterpolator} from "./PlayersPositionInterpolator"; +import {RemotePlayer} from "../Entity/RemotePlayer"; export enum Textures { Player = "male1" @@ -29,16 +30,16 @@ export class GameScene extends Phaser.Scene { Terrains : Array; CurrentPlayer: CurrentGamerInterface; MapPlayers : Phaser.Physics.Arcade.Group; - MapPlayersByKey : Map = new Map(); + MapPlayersByKey : Map = new Map(); Map: Phaser.Tilemaps.Tilemap; Layers : Array; Objects : Array; - map: ITiledMap; + mapFile: ITiledMap; groups: Map; startX = 704;// 22 case startY = 32; // 1 case circleTexture: CanvasTexture; - initPosition: PositionInterface; + private initPosition: PositionInterface|null = null; private playersPositionInterpolator = new PlayersPositionInterpolator(); MapKey: string; @@ -107,9 +108,9 @@ export class GameScene extends Phaser.Scene { private onMapLoad(data: any): void { // Triggered when the map is loaded // Load tiles attached to the map recursively - this.map = data.data; + this.mapFile = data.data; let url = this.MapUrlFile.substr(0, this.MapUrlFile.lastIndexOf('/')); - this.map.tilesets.forEach((tileset) => { + this.mapFile.tilesets.forEach((tileset) => { if (typeof tileset.name === 'undefined' || typeof tileset.image === 'undefined') { console.warn("Don't know how to handle tileset ", tileset) return; @@ -121,14 +122,16 @@ export class GameScene extends Phaser.Scene { //hook initialisation init(initData : GameSceneInitInterface) { - this.initPosition = initData.initPosition; + if (initData.initPosition !== undefined) { + this.initPosition = initData.initPosition; + } } //hook create scene create(): void { //initalise map this.Map = this.add.tilemap(this.MapKey); - this.map.tilesets.forEach((tileset: ITiledTileSet) => { + this.mapFile.tilesets.forEach((tileset: ITiledTileSet) => { this.Terrains.push(this.Map.addTilesetImage(tileset.name, tileset.name)); }); @@ -138,12 +141,12 @@ export class GameScene extends Phaser.Scene { //add layer on map this.Layers = new Array(); let depth = -2; - this.map.layers.forEach((layer : ITiledMapLayer) => { + this.mapFile.layers.forEach((layer : ITiledMapLayer) => { if (layer.type === 'tilelayer') { this.addLayer(this.Map.createStaticLayer(layer.name, this.Terrains, 0, 0).setDepth(depth)); } if (layer.type === 'tilelayer' && this.getExitSceneUrl(layer) !== undefined) { - this.loadNextGame(layer, this.map.width, this.map.tilewidth, this.map.tileheight); + this.loadNextGame(layer, this.mapFile.width, this.mapFile.tilewidth, this.mapFile.tileheight); } if (layer.type === 'tilelayer' && layer.name === "start") { let startPosition = this.startUser(layer); @@ -196,7 +199,7 @@ export class GameScene extends Phaser.Scene { // FIXME: entry should be dictated by a property passed to init() path += '#'+url.hash; } - window.history.pushState({}, null, path); + window.history.pushState({}, 'WorkAdventure', path); } private getExitSceneUrl(layer: ITiledMapLayer): string|undefined { @@ -232,6 +235,9 @@ export class GameScene extends Phaser.Scene { */ private loadNextGame(layer: ITiledMapLayer, mapWidth: number, tileWidth: number, tileHeight: number){ let exitSceneUrl = this.getExitSceneUrl(layer); + if (exitSceneUrl === undefined) { + throw new Error('Layer is not an exit scene layer.'); + } let instance = this.getExitSceneInstance(layer); if (instance === undefined) { instance = this.instance; @@ -265,7 +271,7 @@ export class GameScene extends Phaser.Scene { * @param layer */ private startUser(layer: ITiledMapLayer): PositionInterface { - if (this.initPosition !== undefined) { + if (this.initPosition !== null) { this.startX = this.initPosition.x; this.startY = this.initPosition.y; return { @@ -338,7 +344,6 @@ export class GameScene extends Phaser.Scene { //initialise player //TODO create animation moving between exit and start this.CurrentPlayer = new Player( - null, // The current player is not has no id (because the id can change if connection is lost and we should check that id using the GameManager. this, this.startX, this.startY, @@ -413,7 +418,7 @@ export class GameScene extends Phaser.Scene { // Let's move all users let updatedPlayersPositions = this.playersPositionInterpolator.getUpdatedPositions(time); updatedPlayersPositions.forEach((moveEvent: HasMovedEvent, userId: string) => { - let player : GamerInterface | undefined = this.MapPlayersByKey.get(userId); + let player : RemotePlayer | undefined = this.MapPlayersByKey.get(userId); if (player === undefined) { throw new Error('Cannot find player with ID "' + userId +'"'); } @@ -450,11 +455,11 @@ export class GameScene extends Phaser.Scene { let currentPlayerId = this.GameManager.getPlayerId(); // clean map - this.MapPlayersByKey.forEach((player: GamerInterface) => { + this.MapPlayersByKey.forEach((player: RemotePlayer) => { player.destroy(); this.MapPlayers.remove(player); }); - this.MapPlayersByKey = new Map(); + this.MapPlayersByKey = new Map(); // load map usersPosition.forEach((userPosition : MessageUserPositionInterface) => { @@ -478,7 +483,7 @@ export class GameScene extends Phaser.Scene { return; } //initialise player - let player = new Player( + let player = new RemotePlayer( addPlayerData.userId, this, addPlayerData.position.x, @@ -503,15 +508,16 @@ export class GameScene extends Phaser.Scene { let player = this.MapPlayersByKey.get(userId); if (player === undefined) { console.error('Cannot find user with id ', userId); + } else { + player.destroy(); + this.MapPlayers.remove(player); } - player.destroy(); - this.MapPlayers.remove(player); this.MapPlayersByKey.delete(userId); this.playersPositionInterpolator.removePlayer(userId); } updatePlayerPosition(message: MessageUserMovedInterface): void { - let player : GamerInterface | undefined = this.MapPlayersByKey.get(message.userId); + let player : RemotePlayer | undefined = this.MapPlayersByKey.get(message.userId); if (player === undefined) { throw new Error('Cannot find player with ID "' + message.userId +'"'); } @@ -525,8 +531,9 @@ export class GameScene extends Phaser.Scene { shareGroupPosition(groupPositionMessage: GroupCreatedUpdatedMessageInterface) { let groupId = groupPositionMessage.groupId; - if (this.groups.has(groupId)) { - this.groups.get(groupId).setPosition(Math.round(groupPositionMessage.position.x), Math.round(groupPositionMessage.position.y)); + let 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( @@ -541,10 +548,11 @@ export class GameScene extends Phaser.Scene { } deleteGroup(groupId: string): void { - if(!this.groups.get(groupId)){ + let group = this.groups.get(groupId); + if(!group){ return; } - this.groups.get(groupId).destroy(); + group.destroy(); this.groups.delete(groupId); } diff --git a/front/src/Phaser/Login/LoginScene.ts b/front/src/Phaser/Login/LoginScene.ts index da684da2..1b7ef76f 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/PlayableCaracter"; +import {PLAYER_RESOURCES} from "../Entity/Character"; import {cypressAsserter} from "../../Cypress/CypressAsserter"; import {SelectCharacterSceneInitDataInterface, SelectCharacterSceneName} from "./SelectCharacterScene"; @@ -16,12 +16,12 @@ enum LoginTextures { } export class LoginScene extends Phaser.Scene { - private nameInput: TextInput; - private textField: TextField; - private infoTextField: TextField; - private pressReturnField: TextField; - private logo: Image; - private name: string; + private nameInput: TextInput|null = null; + private textField: TextField|null = null; + private infoTextField: TextField|null = null; + private pressReturnField: TextField|null = null; + private logo: Image|null = null; + private name: string = ''; constructor() { super({ @@ -82,9 +82,9 @@ export class LoginScene extends Phaser.Scene { update(time: number, delta: number): void { if (this.name == '') { - this.pressReturnField.setVisible(false); + this.pressReturnField?.setVisible(false); } else { - this.pressReturnField.setVisible(!!(Math.floor(time / 500) % 2)); + this.pressReturnField?.setVisible(!!(Math.floor(time / 500) % 2)); } } diff --git a/front/src/Phaser/Login/SelectCharacterScene.ts b/front/src/Phaser/Login/SelectCharacterScene.ts index 8a52afcd..eb5f25e9 100644 --- a/front/src/Phaser/Login/SelectCharacterScene.ts +++ b/front/src/Phaser/Login/SelectCharacterScene.ts @@ -3,7 +3,7 @@ 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/PlayableCaracter"; +import {PLAYER_RESOURCES} from "../Entity/Character"; //todo: put this constants in a dedicated file export const SelectCharacterSceneName = "SelectCharacterScene"; diff --git a/front/src/Phaser/Player/Player.ts b/front/src/Phaser/Player/Player.ts index ae40efa5..6794a472 100644 --- a/front/src/Phaser/Player/Player.ts +++ b/front/src/Phaser/Player/Player.ts @@ -2,39 +2,21 @@ import {PlayerAnimationNames} from "./Animation"; import {GameScene, Textures} from "../Game/GameScene"; import {MessageUserPositionInterface, PointInterface} from "../../Connection"; import {ActiveEventList, UserInputEvent, UserInputManager} from "../UserInput/UserInputManager"; -import {PlayableCaracter} from "../Entity/PlayableCaracter"; +import {Character} from "../Entity/Character"; export const hasMovedEventName = "hasMoved"; -export interface CurrentGamerInterface extends PlayableCaracter{ +export interface CurrentGamerInterface extends Character{ moveUser(delta: number) : void; say(text : string) : void; } -export interface GamerInterface extends PlayableCaracter{ - userId : string; - updatePosition(position: PointInterface): void; - say(text : string) : void; -} - -interface AnimationData { - key: string; - frameRate: number; - repeat: number; - frameModel: string; //todo use an enum - frameStart: number; - frameEnd: number; -} - - -export class Player extends PlayableCaracter implements CurrentGamerInterface, GamerInterface { - userId: string; +export class Player extends Character implements CurrentGamerInterface { userInputManager: UserInputManager; previousDirection: string; wasMoving: boolean; constructor( - userId: string, Scene: GameScene, x: number, y: number, @@ -43,62 +25,13 @@ export class Player extends PlayableCaracter implements CurrentGamerInterface, G direction: string, moving: boolean ) { - super(Scene, x, y, PlayerTexture, name, 1); + super(Scene, x, y, PlayerTexture, name, direction, moving, 1); //create input to move this.userInputManager = new UserInputManager(Scene); - //set data - this.userId = userId; - //the current player model should be push away by other players to prevent conflict this.setImmovable(false); - this.initAnimation(); - - this.playAnimation(direction, moving); - } - - private initAnimation(): void { - this.getPlayerAnimations(this.PlayerTexture).forEach(d => { - this.scene.anims.create({ - key: d.key, - frames: this.scene.anims.generateFrameNumbers(d.frameModel, {start: d.frameStart, end: d.frameEnd}), - frameRate: d.frameRate, - repeat: d.repeat - }); - }) - } - - private getPlayerAnimations(name: string): AnimationData[] { - return [{ - key: `${name}-${PlayerAnimationNames.WalkDown}`, - frameModel: name, - frameStart: 0, - frameEnd: 2, - frameRate: 10, - repeat: -1 - }, { - key: `${name}-${PlayerAnimationNames.WalkLeft}`, - frameModel: name, - frameStart: 3, - frameEnd: 5, - frameRate: 10, - repeat: -1 - }, { - key: `${name}-${PlayerAnimationNames.WalkRight}`, - frameModel: name, - frameStart: 6, - frameEnd: 8, - frameRate: 10, - repeat: -1 - }, { - key: `${name}-${PlayerAnimationNames.WalkUp}`, - frameModel: name, - frameStart: 9, - frameEnd: 11, - frameRate: 10, - repeat: -1 - }]; } moveUser(delta: number): void { @@ -146,24 +79,4 @@ export class Player extends PlayableCaracter implements CurrentGamerInterface, G } this.wasMoving = moving; } - - //todo: put this method into the NonPlayer class instead - updatePosition(position: PointInterface): void { - this.playAnimation(position.direction, position.moving); - this.setX(position.x); - this.setY(position.y); - this.setDepth(position.y); - } - - private playAnimation(direction : string, moving: boolean): void { - if (moving && (!this.anims.currentAnim || this.anims.currentAnim.key !== direction)) { - this.play(this.PlayerTexture+'-'+direction, true); - } else if (!moving) { - /*if (this.anims.currentAnim) { - this.anims.stop(); - }*/ - this.play(this.PlayerTexture+'-'+direction, true); - this.stop(); - } - } } diff --git a/front/src/Phaser/Reconnecting/ReconnectingScene.ts b/front/src/Phaser/Reconnecting/ReconnectingScene.ts index 273820b7..7188b223 100644 --- a/front/src/Phaser/Reconnecting/ReconnectingScene.ts +++ b/front/src/Phaser/Reconnecting/ReconnectingScene.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/PlayableCaracter"; +import {PLAYER_RESOURCES} from "../Entity/Character"; import {cypressAsserter} from "../../Cypress/CypressAsserter"; import Sprite = Phaser.GameObjects.Sprite; diff --git a/front/src/Phaser/UserInput/UserInputManager.ts b/front/src/Phaser/UserInput/UserInputManager.ts index 9c4ca660..fec4f58d 100644 --- a/front/src/Phaser/UserInput/UserInputManager.ts +++ b/front/src/Phaser/UserInput/UserInputManager.ts @@ -2,7 +2,6 @@ import Map = Phaser.Structs.Map; import {GameScene} from "../Game/GameScene"; interface UserInputManagerDatum { - keyCode: number; keyInstance: Phaser.Input.Keyboard.Key; event: UserInputEvent } @@ -33,27 +32,26 @@ export class ActiveEventList { //this class is responsible for catching user inputs and listing all active user actions at every game tick events. export class UserInputManager { - private KeysCode: UserInputManagerDatum[] = [ - {keyCode: Phaser.Input.Keyboard.KeyCodes.Z, event: UserInputEvent.MoveUp, keyInstance: null}, - {keyCode: Phaser.Input.Keyboard.KeyCodes.Q, event: UserInputEvent.MoveLeft, keyInstance: null}, - {keyCode: Phaser.Input.Keyboard.KeyCodes.S, event: UserInputEvent.MoveDown, keyInstance: null}, - {keyCode: Phaser.Input.Keyboard.KeyCodes.D, event: UserInputEvent.MoveRight, keyInstance: null}, - - {keyCode: Phaser.Input.Keyboard.KeyCodes.UP, event: UserInputEvent.MoveUp, keyInstance: null}, - {keyCode: Phaser.Input.Keyboard.KeyCodes.LEFT, event: UserInputEvent.MoveLeft, keyInstance: null}, - {keyCode: Phaser.Input.Keyboard.KeyCodes.DOWN, event: UserInputEvent.MoveDown, keyInstance: null}, - {keyCode: Phaser.Input.Keyboard.KeyCodes.RIGHT, event: UserInputEvent.MoveRight, keyInstance: null}, - - {keyCode: Phaser.Input.Keyboard.KeyCodes.SHIFT, event: UserInputEvent.SpeedUp, keyInstance: null}, - - {keyCode: Phaser.Input.Keyboard.KeyCodes.E, event: UserInputEvent.Interact, keyInstance: null}, - {keyCode: Phaser.Input.Keyboard.KeyCodes.F, event: UserInputEvent.Shout, keyInstance: null}, - ]; + private KeysCode: UserInputManagerDatum[]; constructor(Scene : GameScene) { - this.KeysCode.forEach(d => { - d.keyInstance = Scene.input.keyboard.addKey(d.keyCode); - }); + + this.KeysCode = [ + {event: UserInputEvent.MoveUp, keyInstance: Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.Z) }, + {event: UserInputEvent.MoveLeft, keyInstance: Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.Q) }, + {event: UserInputEvent.MoveDown, keyInstance: Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.S) }, + {event: UserInputEvent.MoveRight, keyInstance: Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.D) }, + + {event: UserInputEvent.MoveUp, keyInstance: Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.UP) }, + {event: UserInputEvent.MoveLeft, keyInstance: Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.LEFT) }, + {event: UserInputEvent.MoveDown, keyInstance: Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.DOWN) }, + {event: UserInputEvent.MoveRight, keyInstance: Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.RIGHT) }, + + {event: UserInputEvent.SpeedUp, keyInstance: Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SHIFT) }, + + {event: UserInputEvent.Interact, keyInstance: Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.E) }, + {event: UserInputEvent.Shout, keyInstance: Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.F) }, + ]; } getEventListForGameTick(): ActiveEventList { diff --git a/front/src/WebRtc/MediaManager.ts b/front/src/WebRtc/MediaManager.ts index 1e183559..bb3f0cb9 100644 --- a/front/src/WebRtc/MediaManager.ts +++ b/front/src/WebRtc/MediaManager.ts @@ -4,26 +4,25 @@ const videoConstraint: {width : any, height: any, facingMode : string} = { facingMode: "user" }; export class MediaManager { - localStream: MediaStream; + localStream: MediaStream|null = null; remoteVideo: Array = new Array(); - myCamVideo: any; + myCamVideo: HTMLVideoElement; cinemaClose: any = null; cinema: any = null; microphoneClose: any = null; microphone: any = null; - webrtcInAudio: any; + webrtcInAudio: HTMLAudioElement; constraintsMedia : {audio : any, video : any} = { audio: true, video: videoConstraint }; - getCameraPromise : Promise = null; updatedLocalStreamCallBack : Function; constructor(updatedLocalStreamCallBack : Function) { this.updatedLocalStreamCallBack = updatedLocalStreamCallBack; - this.myCamVideo = document.getElementById('myCamVideo'); - this.webrtcInAudio = document.getElementById('audio-webrtc-in'); + this.myCamVideo = this.getElementByIdOrFail('myCamVideo'); + this.webrtcInAudio = this.getElementByIdOrFail('audio-webrtc-in'); this.webrtcInAudio.volume = 0.2; this.microphoneClose = document.getElementById('microphone-close'); @@ -56,7 +55,7 @@ export class MediaManager { } activeVisio(){ - let webRtc = document.getElementById('webRtc'); + let webRtc = this.getElementByIdOrFail('webRtc'); webRtc.classList.add('active'); } @@ -130,7 +129,7 @@ export class MediaManager { } catch (e) { promise = Promise.reject(false); } - return this.getCameraPromise = promise; + return promise; } /** @@ -139,7 +138,7 @@ export class MediaManager { */ addActiveVideo(userId : string, userName: string = ""){ this.webrtcInAudio.play(); - let elementRemoteVideo = document.getElementById("activeCam"); + let elementRemoteVideo = this.getElementByIdOrFail("activeCam"); userName = userName.toUpperCase(); let color = this.getColorByString(userName); elementRemoteVideo.insertAdjacentHTML('beforeend', ` @@ -247,4 +246,14 @@ export class MediaManager { } return color; } -} \ No newline at end of file + + private getElementByIdOrFail(id: string): T { + let elem = document.getElementById(id); + if (elem === null) { + throw new Error("Cannot find HTML element with id '"+id+"'"); + } + // FIXME: does not check the type of the returned type + return elem as T; + } + +} diff --git a/front/src/WebRtc/SimplePeer.ts b/front/src/WebRtc/SimplePeer.ts index 3693924d..2af0be27 100644 --- a/front/src/WebRtc/SimplePeer.ts +++ b/front/src/WebRtc/SimplePeer.ts @@ -1,21 +1,21 @@ import {ConnectionInterface} from "../Connection"; import {MediaManager} from "./MediaManager"; -let Peer = require('simple-peer'); +import * as SimplePeerNamespace from "simple-peer"; +let Peer: SimplePeerNamespace.SimplePeer = require('simple-peer'); -class UserSimplePear{ +class UserSimplePeer{ userId: string; name?: string; initiator?: boolean; } -export class SimplePeerInterface {} -export class SimplePeer implements SimplePeerInterface{ +export class SimplePeer { private Connection: ConnectionInterface; private WebRtcRoomId: string; - private Users: Array = new Array(); + private Users: Array = new Array(); private MediaManager: MediaManager; - private PeerConnectionArray: Map = new Map(); + private PeerConnectionArray: Map = new Map(); constructor(Connection: ConnectionInterface, WebRtcRoomId: string = "test-webrtc") { this.Connection = Connection; @@ -66,7 +66,7 @@ export class SimplePeer implements SimplePeerInterface{ * server has two person connected, start the meet */ private startWebRtc() { - this.Users.forEach((user: UserSimplePear) => { + this.Users.forEach((user: UserSimplePeer) => { //if it's not an initiator, peer connection will be created when gamer will receive offer signal if(!user.initiator){ return; @@ -78,14 +78,14 @@ export class SimplePeer implements SimplePeerInterface{ /** * create peer connection to bind users */ - private createPeerConnection(user : UserSimplePear) { + private createPeerConnection(user : UserSimplePeer) { if(this.PeerConnectionArray.has(user.userId)) { return; } let name = user.name; if(!name){ - let userSearch = this.Users.find((userSearch: UserSimplePear) => userSearch.userId === user.userId); + let userSearch = this.Users.find((userSearch: UserSimplePeer) => userSearch.userId === user.userId); if(userSearch) { name = userSearch.name; } @@ -112,11 +112,11 @@ export class SimplePeer implements SimplePeerInterface{ this.PeerConnectionArray.set(user.userId, peer); //start listen signal for the peer connection - this.PeerConnectionArray.get(user.userId).on('signal', (data: any) => { + peer.on('signal', (data: any) => { this.sendWebrtcSignal(data, user.userId); }); - this.PeerConnectionArray.get(user.userId).on('stream', (stream: MediaStream) => { + peer.on('stream', (stream: MediaStream) => { let videoActive = false; let microphoneActive = false; stream.getTracks().forEach((track : MediaStreamTrack) => { @@ -141,23 +141,23 @@ export class SimplePeer implements SimplePeerInterface{ this.stream(user.userId, stream); }); - /*this.PeerConnectionArray.get(user.userId).on('track', (track: MediaStreamTrack, stream: MediaStream) => { + /*peer.on('track', (track: MediaStreamTrack, stream: MediaStream) => { this.stream(user.userId, stream); });*/ - this.PeerConnectionArray.get(user.userId).on('close', () => { + peer.on('close', () => { this.closeConnection(user.userId); }); - this.PeerConnectionArray.get(user.userId).on('error', (err: any) => { + peer.on('error', (err: any) => { console.error(`error => ${user.userId} => ${err.code}`, err); }); - this.PeerConnectionArray.get(user.userId).on('connect', () => { + peer.on('connect', () => { console.info(`connect => ${user.userId}`); }); - this.PeerConnectionArray.get(user.userId).on('data', (chunk: Buffer) => { + peer.on('data', (chunk: Buffer) => { let data = JSON.parse(chunk.toString('utf8')); if(data.type === "stream"){ this.stream(user.userId, data.stream); @@ -174,7 +174,7 @@ export class SimplePeer implements SimplePeerInterface{ return; } // @ts-ignore - this.PeerConnectionArray.get(userId).destroy(); + this.PeerConnectionArray.get(userId)?.destroy(); this.PeerConnectionArray.delete(userId) } catch (err) { console.error("closeConnection", err) @@ -200,7 +200,12 @@ export class SimplePeer implements SimplePeerInterface{ if(data.signal.type === "offer"){ this.createPeerConnection(data); } - this.PeerConnectionArray.get(data.userId).signal(data.signal); + let peer = this.PeerConnectionArray.get(data.userId); + if (peer !== undefined) { + peer.signal(data.signal); + } else { + console.error('Could not find peer whose ID is "'+data.userId+'" in PeerConnectionArray'); + } } catch (e) { console.error(`receiveWebrtcSignal => ${data.userId}`, e); } @@ -227,27 +232,32 @@ export class SimplePeer implements SimplePeerInterface{ private addMedia (userId : any = null) { try { let transceiver : any = null; - if(!this.MediaManager.localStream){ + let localStream: MediaStream|null = this.MediaManager.localStream; + let peer = this.PeerConnectionArray.get(userId); + if(localStream === null) { //send fake signal - if(!this.PeerConnectionArray.has(userId)){ + if(peer === undefined){ return; } - this.PeerConnectionArray.get(userId).write(new Buffer(JSON.stringify({ + peer.write(new Buffer(JSON.stringify({ type: "stream", stream: null }))); return; } - this.MediaManager.localStream.getTracks().forEach( - transceiver = (track: MediaStreamTrack) => this.PeerConnectionArray.get(userId).addTrack(track, this.MediaManager.localStream) - ) + if (peer === undefined) { + throw new Error('While adding media, cannot find user with ID '+userId); + } + for (const track of localStream.getTracks()) { + peer.addTrack(track, localStream); + } }catch (e) { console.error(`addMedia => addMedia => ${userId}`, e); } } updatedLocalStream(){ - this.Users.forEach((user: UserSimplePear) => { + this.Users.forEach((user: UserSimplePeer) => { this.addMedia(user.userId); }) } diff --git a/front/tsconfig.json b/front/tsconfig.json index c34c1dd2..84882e74 100644 --- a/front/tsconfig.json +++ b/front/tsconfig.json @@ -7,6 +7,18 @@ "module": "CommonJS", "target": "es5", "jsx": "react", - "allowJs": true + "allowJs": true, + + "strict": false, /* Enable all strict type-checking options. */ + "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + "strictNullChecks": true, /* Enable strict null checks. */ + "strictFunctionTypes": true, /* Enable strict checking of function types. */ + "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + "strictPropertyInitialization": false, /* Enable strict checking of property initialization in classes. */ + "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */ } }