diff --git a/back/src/Controller/AuthenticateController.ts b/back/src/Controller/AuthenticateController.ts index db5fb1ce..4e617509 100644 --- a/back/src/Controller/AuthenticateController.ts +++ b/back/src/Controller/AuthenticateController.ts @@ -15,21 +15,21 @@ export class AuthenticateController { //permit to login on application. Return token to connect on Websocket IO. login(){ // For now, let's completely forget the /login route. - /*this.App.post("/login", (req: Request, res: Response) => { + this.App.post("/login", (req: Request, res: Response) => { let param = req.body; - if(!param.email){ + /*if(!param.name){ return res.status(BAD_REQUEST).send({ message: "email parameter is empty" }); - } + }*/ //TODO check user email for The Coding Machine game let userId = uuid(); - let token = Jwt.sign({email: param.email, userId: userId}, SECRET_KEY, {expiresIn: '24h'}); + let token = Jwt.sign({name: param.name, userId: userId}, SECRET_KEY, {expiresIn: '24h'}); return res.status(OK).send({ token: token, mapUrlStart: URL_ROOM_STARTED, userId: userId, }); - });*/ + }); } } diff --git a/back/src/Controller/IoSocketController.ts b/back/src/Controller/IoSocketController.ts index 2c798797..f6f9f41f 100644 --- a/back/src/Controller/IoSocketController.ts +++ b/back/src/Controller/IoSocketController.ts @@ -5,18 +5,20 @@ import {MessageUserPosition, Point} from "../Model/Websocket/MessageUserPosition import {ExSocketInterface} from "../Model/Websocket/ExSocketInterface"; //TODO fix import by "_Model/.." import Jwt, {JsonWebTokenError} from "jsonwebtoken"; import {SECRET_KEY, MINIMUM_DISTANCE, GROUP_RADIUS} from "../Enum/EnvironmentVariable"; //TODO fix import by "_Enum/..." -import {ExtRooms, RefreshUserPositionFunction} from "../Model/Websocket/ExtRooms"; -import {ExtRoomsInterface} from "../Model/Websocket/ExtRoomsInterface"; import {World} from "../Model/World"; import {Group} from "_Model/Group"; import {UserInterface} from "_Model/UserInterface"; import {SetPlayerDetailsMessage} from "_Model/Websocket/SetPlayerDetailsMessage"; +import {MessageUserJoined} from "../Model/Websocket/MessageUserJoined"; +import {MessageUserMoved} from "../Model/Websocket/MessageUserMoved"; enum SockerIoEvent { CONNECTION = "connection", DISCONNECT = "disconnect", - JOIN_ROOM = "join-room", - USER_POSITION = "user-position", + JOIN_ROOM = "join-room", // bi-directional + USER_POSITION = "user-position", // bi-directional + USER_MOVED = "user-moved", // From server to client + USER_LEFT = "user-left", // From server to client WEBRTC_SIGNAL = "webrtc-signal", WEBRTC_OFFER = "webrtc-offer", WEBRTC_START = "webrtc-start", @@ -30,40 +32,54 @@ enum SockerIoEvent { export class IoSocketController { Io: socketIO.Server; Worlds: Map = new Map(); + sockets: Map = new Map(); constructor(server: http.Server) { this.Io = socketIO(server); // Authentication with token. it will be decoded and stored in the socket. // Completely commented for now, as we do not use the "/login" route at all. - /*this.Io.use((socket: Socket, next) => { + this.Io.use((socket: Socket, next) => { if (!socket.handshake.query || !socket.handshake.query.token) { return next(new Error('Authentication error')); } if(this.searchClientByToken(socket.handshake.query.token)){ return next(new Error('Authentication error')); } - Jwt.verify(socket.handshake.query.token, SECRET_KEY, (err: JsonWebTokenError, tokenDecoded: object) => { + Jwt.verify(socket.handshake.query.token, SECRET_KEY, (err: JsonWebTokenError, tokenDecoded: any) => { if (err) { return next(new Error('Authentication error')); } (socket as ExSocketInterface).token = tokenDecoded; + (socket as ExSocketInterface).userId = tokenDecoded.userId; next(); }); - });*/ + }); this.ioConnection(); - this.shareUsersPosition(); + } + + /** + * + * @param token + */ + searchClientByToken(token: string): ExSocketInterface | null { + let clients: Array = Object.values(this.Io.sockets.sockets); + for (let i = 0; i < clients.length; i++) { + let client: ExSocketInterface = clients[i]; + if (client.token !== token) { + continue + } + return client; + } + return null; } private sendUpdateGroupEvent(group: Group): void { // Let's get the room of the group. To do this, let's get anyone in the group and find its room. // Note: this is suboptimal let userId = group.getUsers()[0].id; - let client: ExSocketInterface|null = this.searchClientById(userId); - if (client === null) { - return; - } + let client: ExSocketInterface = this.searchClientByIdOrFail(userId); let roomId = client.roomId; this.Io.in(roomId).emit(SockerIoEvent.GROUP_CREATE_UPDATE, { position: group.getPosition(), @@ -73,19 +89,16 @@ export class IoSocketController { private sendDeleteGroupEvent(uuid: string, lastUser: UserInterface): void { // Let's get the room of the group. To do this, let's get anyone in the group and find its room. - // Note: this is suboptimal let userId = lastUser.id; - let client: ExSocketInterface|null = this.searchClientById(userId); - if (client === null) { - console.warn('Could not find client ', userId, ' in group') - return; - } + let client: ExSocketInterface = this.searchClientByIdOrFail(userId); let roomId = client.roomId; this.Io.in(roomId).emit(SockerIoEvent.GROUP_DELETE, uuid); } ioConnection() { this.Io.on(SockerIoEvent.CONNECTION, (socket: Socket) => { + let client : ExSocketInterface = socket as ExSocketInterface; + this.sockets.set(client.userId, client); /*join-rom event permit to join one room. message : userId : user identification @@ -94,12 +107,19 @@ export class IoSocketController { x: user x position on map y: user y position on map */ - socket.on(SockerIoEvent.JOIN_ROOM, (roomId: any): void => { + socket.on(SockerIoEvent.JOIN_ROOM, (message: any, answerFn): void => { try { + let roomId = message.roomId; + if (typeof(roomId) !== 'string') { socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: 'Expected roomId as a string.'}); return; } + let position = this.hydratePositionReceive(message.position); + if (position instanceof Error) { + socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: position.message}); + return; + } let Client = (socket as ExSocketInterface); @@ -111,14 +131,21 @@ export class IoSocketController { this.leaveRoom(Client); //join new previous room - this.joinRoom(Client, roomId); + let world = this.joinRoom(Client, roomId, position); //add function to refresh position user in real time. - this.refreshUserPosition(Client); + //this.refreshUserPosition(Client); - let messageUserPosition = new MessageUserPosition(Client.id, Client.name, Client.character,new Point(0, 0, 'none')); + let messageUserJoined = new MessageUserJoined(Client.userId, Client.name, Client.character, Client.position); - socket.to(roomId).emit(SockerIoEvent.JOIN_ROOM, messageUserPosition); + socket.to(roomId).emit(SockerIoEvent.JOIN_ROOM, messageUserJoined); + + // The answer shall contain the list of all users of the room with their positions: + let listOfUsers = Array.from(world.getUsers(), ([key, user]) => { + let player = this.searchClientByIdOrFail(user.id); + return new MessageUserPosition(user.id, player.name, player.character, player.position); + }); + answerFn(listOfUsers); } catch (e) { console.error('An error occurred on "join_room" event'); console.error(e); @@ -138,8 +165,15 @@ export class IoSocketController { // sending to all clients in room except sender Client.position = position; - //refresh position of all user in all rooms in real time - this.refreshUserPosition(Client); + // update position in the world + let world = this.Worlds.get(Client.roomId); + if (!world) { + console.error("Could not find world with id '", Client.roomId, "'"); + return; + } + world.updatePosition(Client, position); + + socket.to(Client.roomId).emit(SockerIoEvent.USER_MOVED, new MessageUserMoved(Client.userId, Client.position)); } catch (e) { console.error('An error occurred on "user_position" event'); console.error(e); @@ -148,38 +182,32 @@ export class IoSocketController { socket.on(SockerIoEvent.WEBRTC_SIGNAL, (data: any) => { //send only at user - let client = this.searchClientById(data.receiverId); - if (!client) { - console.error("While exchanging a WebRTC signal: client doesn't exist for ", data.receiverId); + let client = this.sockets.get(data.receiverId); + if (client === undefined) { + console.warn("While exchanging a WebRTC signal: client with id ", data.receiverId, " does not exist. This might be a race condition."); return; } return client.emit(SockerIoEvent.WEBRTC_SIGNAL, data); }); socket.on(SockerIoEvent.WEBRTC_OFFER, (data: any) => { - //send only at user - let client = this.searchClientById(data.receiverId); - if (!client) { - console.error("client doesn't exist for ", data.receiverId); + let client = this.sockets.get(data.receiverId); + if (client === undefined) { + console.warn("While exchanging a WebRTC offer: client with id ", data.receiverId, " does not exist. This might be a race condition."); return; } client.emit(SockerIoEvent.WEBRTC_OFFER, data); }); socket.on(SockerIoEvent.DISCONNECT, () => { + let Client = (socket as ExSocketInterface); try { - let Client = (socket as ExSocketInterface); - this.sendDisconnectedEvent(Client); - - //refresh position of all user in all rooms in real time - this.refreshUserPosition(Client); - //leave room this.leaveRoom(Client); //leave webrtc room - socket.leave(Client.webRtcRoomId); + //socket.leave(Client.webRtcRoomId); //delete all socket information delete Client.webRtcRoomId; @@ -190,6 +218,7 @@ export class IoSocketController { console.error('An error occurred on "disconnect"'); console.error(e); } + this.sockets.delete(Client.userId); }); // Let's send the user id to the user @@ -197,69 +226,28 @@ export class IoSocketController { let Client = (socket as ExSocketInterface); Client.name = playerDetails.name; Client.character = playerDetails.character; - answerFn(socket.id); + answerFn(Client.userId); }); }); } - /** - * TODO: each call to this method is suboptimal. It means that instead of passing an ID, we should pass a client object. - * @param userId - */ - searchClientById(userId: string): ExSocketInterface | null { - let clients: Array = Object.values(this.Io.sockets.sockets); - for (let i = 0; i < clients.length; i++) { - let client: ExSocketInterface = clients[i]; - if (client.id !== userId) { - continue; - } - return client; + searchClientByIdOrFail(userId: string): ExSocketInterface { + let client: ExSocketInterface|undefined = this.sockets.get(userId); + if (client === undefined) { + throw new Error("Could not find user with id " + userId); } - console.log("Could not find user with id ", userId); - //throw new Error("Could not find user with id " + userId); - return null; - } - - /** - * @param userId - */ - searchClientByToken(userId: string): ExSocketInterface | null { - let clients: Array = Object.values(this.Io.sockets.sockets); - for (let i = 0; i < clients.length; i++) { - let client: ExSocketInterface = clients[i]; - if (client.id !== userId) { - continue - } - return client; - } - return null; - } - - /** - * - * @param Client: ExSocketInterface - */ - sendDisconnectedEvent(Client: ExSocketInterface) { - Client.broadcast.emit(SockerIoEvent.WEBRTC_DISCONNECT, { - userId: Client.id - }); - - //disconnect webrtc room - if(!Client.webRtcRoomId){ - return; - } - Client.leave(Client.webRtcRoomId); - delete Client.webRtcRoomId; + return client; } leaveRoom(Client : ExSocketInterface){ - //lease previous room and world + // leave previous room and world if(Client.roomId){ + Client.to(Client.roomId).emit(SockerIoEvent.USER_LEFT, Client.userId); + //user leave previous world let world : World|undefined = this.Worlds.get(Client.roomId); if(world){ world.leave(Client); - //this.Worlds.set(Client.roomId, world); } //user leave previous room Client.leave(Client.roomId); @@ -267,15 +255,16 @@ export class IoSocketController { } } - joinRoom(Client : ExSocketInterface, roomId: string){ + private joinRoom(Client : ExSocketInterface, roomId: string, position: Point): World { //join user in room Client.join(roomId); Client.roomId = roomId; - Client.position = new Point(-1000, -1000); + Client.position = position; //check and create new world for a room - if(!this.Worlds.get(roomId)){ - let world = new World((user1: string, group: Group) => { + let world = this.Worlds.get(roomId) + if(world === undefined){ + world = new World((user1: string, group: Group) => { this.connectedUser(user1, group); }, (user1: string, group: Group) => { this.disConnectedUser(user1, group); @@ -287,20 +276,16 @@ export class IoSocketController { this.Worlds.set(roomId, world); } - let world : World|undefined = this.Worlds.get(roomId); - - if(world) { - // Dispatch groups position to newly connected user - world.getGroups().forEach((group: Group) => { - Client.emit(SockerIoEvent.GROUP_CREATE_UPDATE, { - position: group.getPosition(), - groupId: group.getId() - }); + // Dispatch groups position to newly connected user + world.getGroups().forEach((group: Group) => { + Client.emit(SockerIoEvent.GROUP_CREATE_UPDATE, { + position: group.getPosition(), + groupId: group.getId() }); - //join world - world.join(Client, Client.position); - this.Worlds.set(roomId, world); - } + }); + //join world + world.join(Client, Client.position); + return world; } /** @@ -325,11 +310,11 @@ export class IoSocketController { clients.forEach((client: ExSocketInterface, index: number) => { let clientsId = clients.reduce((tabs: Array, clientId: ExSocketInterface, indexClientId: number) => { - if (!clientId.id || clientId.id === client.id) { + if (!clientId.userId || clientId.userId === client.userId) { return tabs; } tabs.push({ - userId: clientId.id, + userId: clientId.userId, name: clientId.name, initiator: index <= indexClientId }); @@ -340,31 +325,13 @@ export class IoSocketController { }); } - refreshUserPosition(Client : ExSocketInterface) { - //refresh position of all user in all rooms in real time - let rooms = (this.Io.sockets.adapter.rooms as ExtRoomsInterface); - if (!rooms.refreshUserPosition) { - rooms.refreshUserPosition = RefreshUserPositionFunction; - } - rooms.refreshUserPosition(rooms, this.Io); - - // update position in the world - let messageUserPosition = new MessageUserPosition(Client.id, Client.name, Client.character, Client.position); - let world = this.Worlds.get(Client.roomId); - if (!world) { - return; - } - world.updatePosition(Client, messageUserPosition.position); - this.Worlds.set(Client.roomId, world); - } - //Hydrate and manage error hydratePositionReceive(message: any): Point | Error { try { - if (!message.x || !message.y || !message.direction) { + if (!message.x || !message.y || !message.direction || message.moving === undefined) { return new Error("invalid point message sent"); } - return new Point(message.x, message.y, message.direction); + return new Point(message.x, message.y, message.direction, message.moving); } catch (err) { //TODO log error return new Error(err); @@ -387,44 +354,29 @@ export class IoSocketController { ... ] **/ - seTimeOutInProgress: any = null; - - shareUsersPosition() { - if (this.seTimeOutInProgress) { - clearTimeout(this.seTimeOutInProgress); - } - //send for each room, all data of position user - let arrayMap = (this.Io.sockets.adapter.rooms as ExtRooms).userPositionMapByRoom; - if (!arrayMap) { - this.seTimeOutInProgress = setTimeout(() => { - this.shareUsersPosition(); - }, 10); - return; - } - arrayMap.forEach((value: any) => { - let roomId = value[0]; - this.Io.in(roomId).emit(SockerIoEvent.USER_POSITION, value); - }); - this.seTimeOutInProgress = setTimeout(() => { - this.shareUsersPosition(); - }, 10); - } //connected user connectedUser(userId: string, group: Group) { - let Client = this.searchClientById(userId); - if (!Client) { + /*let Client = this.sockets.get(userId); + if (Client === undefined) { return; - } + }*/ + let Client = this.searchClientByIdOrFail(userId); this.joinWebRtcRoom(Client, group.getId()); } //disconnect user disConnectedUser(userId: string, group: Group) { - let Client = this.searchClientById(userId); - if (!Client) { + let Client = this.searchClientByIdOrFail(userId); + Client.to(group.getId()).emit(SockerIoEvent.WEBRTC_DISCONNECT, { + userId: userId + }); + + //disconnect webrtc room + if(!Client.webRtcRoomId){ return; } - this.sendDisconnectedEvent(Client) + Client.leave(Client.webRtcRoomId); + delete Client.webRtcRoomId; } } diff --git a/back/src/Model/Websocket/ExSocketInterface.ts b/back/src/Model/Websocket/ExSocketInterface.ts index c55a9759..df72321f 100644 --- a/back/src/Model/Websocket/ExSocketInterface.ts +++ b/back/src/Model/Websocket/ExSocketInterface.ts @@ -6,7 +6,7 @@ export interface ExSocketInterface extends Socket, Identificable { token: any; roomId: string; webRtcRoomId: string; - //userId: string; + userId: string; name: string; character: string; position: PointInterface; diff --git a/back/src/Model/Websocket/ExtRooms.ts b/back/src/Model/Websocket/ExtRooms.ts deleted file mode 100644 index b5493fdb..00000000 --- a/back/src/Model/Websocket/ExtRooms.ts +++ /dev/null @@ -1,44 +0,0 @@ -import {ExtRoomsInterface} from "./ExtRoomsInterface"; -import socketIO = require('socket.io'); -import {ExSocketInterface} from "./ExSocketInterface"; - -export class ExtRooms implements ExtRoomsInterface{ - userPositionMapByRoom: any; - refreshUserPosition: any; - Worlds: any; - - [room: string]: SocketIO.Room; -} - -let RefreshUserPositionFunction = function(rooms : ExtRooms, Io: socketIO.Server) { - let clients = Io.clients(); - let socketsKey = Object.keys(Io.clients().sockets); - - //create mapping with all users in all rooms - let mapPositionUserByRoom = new Map(); - for (let i = 0; i < socketsKey.length; i++) { - let socket = clients.sockets[socketsKey[i]] as ExSocketInterface; - if (!socket.position) { - continue; - } - let data = { - userId: socket.id, - position: socket.position, - name: socket.name, - character: socket.character, - }; - let dataArray = []; - if (mapPositionUserByRoom.get(socket.roomId)) { - dataArray = mapPositionUserByRoom.get(socket.roomId); - dataArray.push(data); - } else { - dataArray = [data]; - } - mapPositionUserByRoom.set(socket.roomId, dataArray); - } - rooms.userPositionMapByRoom = Array.from(mapPositionUserByRoom); -}; - -export { - RefreshUserPositionFunction -}; diff --git a/back/src/Model/Websocket/ExtRoomsInterface.ts b/back/src/Model/Websocket/ExtRoomsInterface.ts deleted file mode 100644 index a71fcbf9..00000000 --- a/back/src/Model/Websocket/ExtRoomsInterface.ts +++ /dev/null @@ -1,6 +0,0 @@ -import {Rooms} from "socket.io"; - -export interface ExtRoomsInterface extends Rooms{ - userPositionMapByRoom: any; - refreshUserPosition: any; -} \ No newline at end of file diff --git a/back/src/Model/Websocket/Identificable.ts b/back/src/Model/Websocket/Identificable.ts index 8c344259..4e3228ae 100644 --- a/back/src/Model/Websocket/Identificable.ts +++ b/back/src/Model/Websocket/Identificable.ts @@ -1,3 +1,3 @@ export interface Identificable { - id: string; + userId: string; } diff --git a/back/src/Model/Websocket/MessageUserJoined.ts b/back/src/Model/Websocket/MessageUserJoined.ts new file mode 100644 index 00000000..fff9db5d --- /dev/null +++ b/back/src/Model/Websocket/MessageUserJoined.ts @@ -0,0 +1,7 @@ +import {Point} from "./MessageUserPosition"; +import {PointInterface} from "_Model/Websocket/PointInterface"; + +export class MessageUserJoined { + constructor(public userId: string, public name: string, public character: string, public position: PointInterface) { + } +} diff --git a/back/src/Model/Websocket/MessageUserMoved.ts b/back/src/Model/Websocket/MessageUserMoved.ts new file mode 100644 index 00000000..283c011d --- /dev/null +++ b/back/src/Model/Websocket/MessageUserMoved.ts @@ -0,0 +1,6 @@ +import {PointInterface} from "./PointInterface"; + +export class MessageUserMoved { + constructor(public userId: string, public position: PointInterface) { + } +} diff --git a/back/src/Model/Websocket/MessageUserPosition.ts b/back/src/Model/Websocket/MessageUserPosition.ts index 613e47e6..ed604940 100644 --- a/back/src/Model/Websocket/MessageUserPosition.ts +++ b/back/src/Model/Websocket/MessageUserPosition.ts @@ -1,7 +1,7 @@ import {PointInterface} from "./PointInterface"; export class Point implements PointInterface{ - constructor(public x : number, public y : number, public direction : string = "none") { + constructor(public x : number, public y : number, public direction : string = "none", public moving : boolean = false) { } } diff --git a/back/src/Model/Websocket/PointInterface.ts b/back/src/Model/Websocket/PointInterface.ts index 9243acbe..61b02339 100644 --- a/back/src/Model/Websocket/PointInterface.ts +++ b/back/src/Model/Websocket/PointInterface.ts @@ -1,5 +1,5 @@ export interface PointInterface { - x: number; - y: number; - direction: string; + readonly x: number; + readonly y: number; + readonly direction: string; } diff --git a/back/src/Model/World.ts b/back/src/Model/World.ts index 39233271..5f70a32f 100644 --- a/back/src/Model/World.ts +++ b/back/src/Model/World.ts @@ -48,9 +48,13 @@ export class World { return this.groups; } + public getUsers(): Map { + return this.users; + } + public join(socket : Identificable, userPosition: PointInterface): void { - this.users.set(socket.id, { - id: socket.id, + this.users.set(socket.userId, { + id: socket.userId, position: userPosition }); // Let's call update position to trigger the join / leave room @@ -58,25 +62,23 @@ export class World { } public leave(user : Identificable){ - let userObj = this.users.get(user.id); + let userObj = this.users.get(user.userId); if (userObj === undefined) { - // FIXME: this seems always wrong. I guess user.id is different from userPosition.userId - console.warn('User ', user.id, 'does not belong to world! It should!'); + console.warn('User ', user.userId, 'does not belong to world! It should!'); } if (userObj !== undefined && typeof userObj.group !== 'undefined') { this.leaveGroup(userObj); } - this.users.delete(user.id); + this.users.delete(user.userId); } public updatePosition(socket : Identificable, userPosition: PointInterface): void { - let user = this.users.get(socket.id); + let user = this.users.get(socket.userId); if(typeof user === 'undefined') { return; } - user.position.x = userPosition.x; - user.position.y = userPosition.y; + user.position = userPosition; if (typeof user.group === 'undefined') { // If the user is not part of a group: diff --git a/back/tests/WorldTest.ts b/back/tests/WorldTest.ts index aed9b838..88c87430 100644 --- a/back/tests/WorldTest.ts +++ b/back/tests/WorldTest.ts @@ -15,19 +15,19 @@ describe("World", () => { let world = new World(connect, disconnect, 160, 160, () => {}, () => {}); - world.join({ id: "foo" }, new Point(100, 100)); + world.join({ userId: "foo" }, new Point(100, 100)); - world.join({ id: "bar" }, new Point(500, 100)); + world.join({ userId: "bar" }, new Point(500, 100)); - world.updatePosition({ id: "bar" }, new Point(261, 100)); + world.updatePosition({ userId: "bar" }, new Point(261, 100)); expect(connectCalledNumber).toBe(0); - world.updatePosition({ id: "bar" }, new Point(101, 100)); + world.updatePosition({ userId: "bar" }, new Point(101, 100)); expect(connectCalledNumber).toBe(2); - world.updatePosition({ id: "bar" }, new Point(102, 100)); + world.updatePosition({ userId: "bar" }, new Point(102, 100)); expect(connectCalledNumber).toBe(2); }); @@ -42,19 +42,19 @@ describe("World", () => { let world = new World(connect, disconnect, 160, 160, () => {}, () => {}); - world.join({ id: "foo" }, new Point(100, 100)); + world.join({ userId: "foo" }, new Point(100, 100)); - world.join({ id: "bar" }, new Point(200, 100)); + world.join({ userId: "bar" }, new Point(200, 100)); expect(connectCalled).toBe(true); connectCalled = false; // baz joins at the outer limit of the group - world.join({ id: "baz" }, new Point(311, 100)); + world.join({ userId: "baz" }, new Point(311, 100)); expect(connectCalled).toBe(false); - world.updatePosition({ id: "baz" }, new Point(309, 100)); + world.updatePosition({ userId: "baz" }, new Point(309, 100)); expect(connectCalled).toBe(true); }); @@ -71,18 +71,18 @@ describe("World", () => { let world = new World(connect, disconnect, 160, 160, () => {}, () => {}); - world.join({ id: "foo" }, new Point(100, 100)); + world.join({ userId: "foo" }, new Point(100, 100)); - world.join({ id: "bar" }, new Point(259, 100)); + world.join({ userId: "bar" }, new Point(259, 100)); expect(connectCalled).toBe(true); expect(disconnectCallNumber).toBe(0); - world.updatePosition({ id: "bar" }, new Point(100+160+160+1, 100)); + world.updatePosition({ userId: "bar" }, new Point(100+160+160+1, 100)); expect(disconnectCallNumber).toBe(2); - world.updatePosition({ id: "bar" }, new Point(262, 100)); + world.updatePosition({ userId: "bar" }, new Point(262, 100)); expect(disconnectCallNumber).toBe(2); }); diff --git a/front/src/Connexion.ts b/front/src/Connexion.ts index f3e4f4ce..0d4429f5 100644 --- a/front/src/Connexion.ts +++ b/front/src/Connexion.ts @@ -6,14 +6,17 @@ import {SetPlayerDetailsMessage} from "./Messages/SetPlayerDetailsMessage"; const SocketIo = require('socket.io-client'); import Socket = SocketIOClient.Socket; +import {PlayerAnimationNames} from "./Phaser/Player/Animation"; enum EventMessage{ WEBRTC_SIGNAL = "webrtc-signal", WEBRTC_START = "webrtc-start", WEBRTC_JOIN_ROOM = "webrtc-join-room", - JOIN_ROOM = "join-room", - USER_POSITION = "user-position", + JOIN_ROOM = "join-room", // bi-directional + USER_POSITION = "user-position", // bi-directional + USER_MOVED = "user-moved", // From server to client + USER_LEFT = "user-left", // From server to client MESSAGE_ERROR = "message-error", WEBRTC_DISCONNECT = "webrtc-disconect", GROUP_CREATE_UPDATE = "group-create-update", @@ -40,20 +43,14 @@ export interface PointInterface { x: number; y: number; direction : string; + moving: boolean; } -class Point implements PointInterface{ - x: number; - y: number; - direction : string; - - constructor(x : number, y : number, direction : string = "none") { +export class Point implements PointInterface{ + constructor(public x : number, public y : number, public direction : string = PlayerAnimationNames.WalkDown, public moving : boolean = false) { if(x === null || y === null){ throw Error("position x and y cannot be null"); } - this.x = x; - this.y = y; - this.direction = direction; } } @@ -64,6 +61,11 @@ export interface MessageUserPositionInterface { position: PointInterface; } +export interface MessageUserMovedInterface { + userId: string; + position: PointInterface; +} + class MessageUserPosition extends Message implements MessageUserPositionInterface{ position: PointInterface; @@ -73,6 +75,13 @@ class MessageUserPosition extends Message implements MessageUserPositionInterfac } } +export interface MessageUserJoined { + userId: string; + name: string; + character: string; + position: PointInterface +} + export interface ListMessageUserPositionInterface { roomId: string; listUsersPosition: Array; @@ -91,7 +100,8 @@ class ListMessageUserPosition { new Point( userPosition.position.x, userPosition.position.y, - userPosition.position.direction + userPosition.position.direction, + userPosition.position.moving ), userPosition.name, userPosition.character @@ -120,9 +130,9 @@ export interface ConnexionInterface { loadMaps(): Promise; - joinARoom(roomId: string, character: string): void; + joinARoom(roomId: string, startX: number, startY: number, direction: string, moving: boolean): void; - sharePosition(x: number, y: number, direction: string, roomId: string, character: string): void; + sharePosition(x: number, y: number, direction: string, moving: boolean): void; positionOfAllUser(): void; @@ -155,25 +165,20 @@ export class Connexion implements ConnexionInterface { createConnexion(name: string, characterSelected: string): Promise { this.name = name; this.character = characterSelected; - /*return Axios.post(`${API_URL}/login`, {email: this.email}) + return Axios.post(`${API_URL}/login`, {name: name}) .then((res) => { this.token = res.data.token; - this.userId = res.data.userId;*/ - this.socket = SocketIo(`${API_URL}`, { - /*query: { + query: { token: this.token - }*/ + } }); - return this.connectSocketServer(); - - /* return res.data; }) .catch((err) => { console.error(err); throw err; - });*/ + }); } /** @@ -187,6 +192,9 @@ export class Connexion implements ConnexionInterface { this.errorMessage(); this.groupUpdatedOrCreated(); this.groupDeleted(); + this.onUserJoins(); + this.onUserMoved(); + this.onUserLeft(); return new Promise((resolve, reject) => { this.socket.emit(EventMessage.SET_PLAYER_DETAILS, { @@ -197,22 +205,25 @@ export class Connexion implements ConnexionInterface { }); //if try to reconnect with last position - if(this.lastRoom) { + /*if(this.lastRoom) { //join the room - this.joinARoom( - this.lastRoom - ); - } + 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) { + /*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.direction, + this.lastPositionShared.moving ); - } + }*/ resolve(this); }); @@ -229,29 +240,18 @@ export class Connexion implements ConnexionInterface { }); } - /** - * - * @param roomId - * @param character - */ - joinARoom(roomId: string): void { - this.socket.emit(EventMessage.JOIN_ROOM, roomId); + 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.GameManager.initUsersPosition(userPositions); + }); this.lastRoom = roomId; } - /** - * - * @param x - * @param y - * @param character - * @param roomId - * @param direction - */ - sharePosition(x : number, y : number, direction : string = "none") : void{ + sharePosition(x : number, y : number, direction : string, moving: boolean) : void{ if(!this.socket){ return; } - let point = new Point(x, y, direction); + let point = new Point(x, y, direction, moving); this.lastPositionShared = point; this.socket.emit(EventMessage.USER_POSITION, point); } @@ -261,11 +261,11 @@ export class Connexion implements ConnexionInterface { * [ * { * userId: , - * roomId: , * position: { * x : , * y : , - * direction: + * direction: , + * moving: * } * }, * ... @@ -280,6 +280,24 @@ export class Connexion implements ConnexionInterface { }); } + onUserJoins(): void { + this.socket.on(EventMessage.JOIN_ROOM, (message: MessageUserJoined) => { + this.GameManager.onUserJoins(message); + }); + } + + onUserMoved(): void { + this.socket.on(EventMessage.USER_MOVED, (message: MessageUserMovedInterface) => { + this.GameManager.onUserMoved(message); + }); + } + + onUserLeft(): void { + this.socket.on(EventMessage.USER_LEFT, (userId: string) => { + this.GameManager.onUserLeft(userId); + }); + } + private groupUpdatedOrCreated(): void { this.socket.on(EventMessage.GROUP_CREATE_UPDATE, (groupCreateUpdateMessage: GroupCreatedUpdatedMessageInterface) => { //console.log('Group ', groupCreateUpdateMessage.groupId, " position :", groupCreateUpdateMessage.position.x, groupCreateUpdateMessage.position.y) @@ -318,12 +336,12 @@ export class Connexion implements ConnexionInterface { disconnectServer(): void { this.socket.on(EventMessage.CONNECT_ERROR, () => { - MessageUI.warningMessage("Trying to connect!"); + this.GameManager.switchToDisconnectedScene(); }); this.socket.on(EventMessage.RECONNECT, () => { - MessageUI.removeMessage(); this.connectSocketServer(); + this.GameManager.reconnectToGameScene(this.lastPositionShared); }); } diff --git a/front/src/Phaser/Entity/PlayableCaracter.ts b/front/src/Phaser/Entity/PlayableCaracter.ts index e126dbb9..826bfc6a 100644 --- a/front/src/Phaser/Entity/PlayableCaracter.ts +++ b/front/src/Phaser/Entity/PlayableCaracter.ts @@ -26,7 +26,7 @@ export const PLAYER_RESOURCES: Array = [ export class PlayableCaracter extends Phaser.Physics.Arcade.Sprite { private bubble: SpeechBubble; - private playerName: BitmapText; + private readonly playerName: BitmapText; public PlayerValue: string; public PlayerTexture: string; diff --git a/front/src/Phaser/Game/AddPlayerInterface.ts b/front/src/Phaser/Game/AddPlayerInterface.ts new file mode 100644 index 00000000..473fea29 --- /dev/null +++ b/front/src/Phaser/Game/AddPlayerInterface.ts @@ -0,0 +1,8 @@ +import {PointInterface} from "../../Connexion"; + +export interface AddPlayerInterface { + userId: string; + name: string; + character: string; + position: PointInterface; +} diff --git a/front/src/Phaser/Game/GameManager.ts b/front/src/Phaser/Game/GameManager.ts index a84653cd..7a439fbe 100644 --- a/front/src/Phaser/Game/GameManager.ts +++ b/front/src/Phaser/Game/GameManager.ts @@ -1,12 +1,19 @@ -import {GameScene, GameSceneInterface} from "./GameScene"; +import {GameScene} from "./GameScene"; import { Connexion, GroupCreatedUpdatedMessageInterface, - ListMessageUserPositionInterface + ListMessageUserPositionInterface, + MessageUserJoined, + MessageUserMovedInterface, + MessageUserPositionInterface, + Point, + PointInterface } from "../../Connexion"; import {SimplePeerInterface, SimplePeer} from "../../WebRtc/SimplePeer"; import {getMapKeyByUrl} from "../Login/LogincScene"; import ScenePlugin = Phaser.Scenes.ScenePlugin; +import {AddPlayerInterface} from "./AddPlayerInterface"; +import {ReconnectingSceneName} from "../Reconnecting/ReconnectingScene"; export enum StatusGameManagerEnum { IN_PROGRESS = 1, @@ -15,6 +22,7 @@ export enum StatusGameManagerEnum { export interface HasMovedEvent { direction: string; + moving: boolean; x: number; y: number; } @@ -27,7 +35,7 @@ export interface MapObject { export class GameManager { status: number; private ConnexionInstance: Connexion; - private currentGameScene: GameSceneInterface; + private currentGameScene: GameScene; private playerName: string; SimplePeer : SimplePeerInterface; private characterUserSelected: string; @@ -56,7 +64,7 @@ export class GameManager { }); } - setCurrentGameScene(gameScene: GameSceneInterface) { + setCurrentGameScene(gameScene: GameScene) { this.currentGameScene = gameScene; } @@ -70,13 +78,32 @@ export class GameManager { this.status = StatusGameManagerEnum.CURRENT_USER_CREATED; } - joinRoom(sceneKey : string){ - this.ConnexionInstance.joinARoom(sceneKey); + joinRoom(sceneKey: string, startX: number, startY: number, direction: string, moving: boolean){ + this.ConnexionInstance.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.currentGameScene.addPlayer(userMessage); + } + + onUserMoved(message: MessageUserMovedInterface): void { + this.currentGameScene.updatePlayerPosition(message); + } + + onUserLeft(userId: string): void { + this.currentGameScene.removePlayer(userId); } /** * Share position in game * @param ListMessageUserPosition + * @deprecated */ shareUserPosition(ListMessageUserPosition: ListMessageUserPositionInterface): void { if (this.status === StatusGameManagerEnum.IN_PROGRESS) { @@ -89,6 +116,18 @@ export class GameManager { } } + initUsersPosition(usersPosition: MessageUserPositionInterface[]): void { + // Shall we wait for room to be loaded? + /*if (this.status === StatusGameManagerEnum.IN_PROGRESS) { + return; + }*/ + try { + this.currentGameScene.initUsersPosition(usersPosition) + } catch (e) { + console.error(e); + } + } + /** * Share group position in game */ @@ -127,7 +166,7 @@ export class GameManager { } pushPlayerPosition(event: HasMovedEvent) { - this.ConnexionInstance.sharePosition(event.x, event.y, event.direction); + this.ConnexionInstance.sharePosition(event.x, event.y, event.direction, event.moving); } loadMap(mapUrl: string, scene: ScenePlugin): string { @@ -141,6 +180,16 @@ export class GameManager { } return sceneKey; } + + private oldSceneKey : string; + switchToDisconnectedScene(): void { + this.oldSceneKey = this.currentGameScene.scene.key; + this.currentGameScene.scene.start(ReconnectingSceneName); + } + + reconnectToGameScene(lastPositionShared: PointInterface) { + this.currentGameScene.scene.start(this.oldSceneKey, { initPosition: lastPositionShared }); + } } export const gameManager = new GameManager(); diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 56c775ed..2ec6ea13 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -1,31 +1,33 @@ -import {GameManager, gameManager, HasMovedEvent, MapObject, StatusGameManagerEnum} from "./GameManager"; -import {GroupCreatedUpdatedMessageInterface, MessageUserPositionInterface} from "../../Connexion"; +import {GameManager, gameManager, HasMovedEvent} from "./GameManager"; +import { + GroupCreatedUpdatedMessageInterface, + MessageUserMovedInterface, + MessageUserPositionInterface, PointInterface, PositionInterface +} from "../../Connexion"; import {CurrentGamerInterface, GamerInterface, hasMovedEventName, Player} from "../Player/Player"; -import { DEBUG_MODE, RESOLUTION, ROOM, ZOOM_LEVEL} from "../../Enum/EnvironmentVariable"; +import { DEBUG_MODE, ZOOM_LEVEL} from "../../Enum/EnvironmentVariable"; import {ITiledMap, ITiledMapLayer, ITiledTileSet} from "../Map/ITiledMap"; import {PLAYER_RESOURCES} from "../Entity/PlayableCaracter"; import Texture = Phaser.Textures.Texture; import Sprite = Phaser.GameObjects.Sprite; import CanvasTexture = Phaser.Textures.CanvasTexture; -import CreateSceneFromObjectConfig = Phaser.Types.Scenes.CreateSceneFromObjectConfig; +import {AddPlayerInterface} from "./AddPlayerInterface"; +import {PlayerAnimationNames} from "../Player/Animation"; export enum Textures { Player = "male1" } -export interface GameSceneInterface extends Phaser.Scene { - Map: Phaser.Tilemaps.Tilemap; - createCurrentPlayer() : void; - shareUserPosition(UsersPosition : Array): void; - shareGroupPosition(groupPositionMessage: GroupCreatedUpdatedMessageInterface): void; - updateOrCreateMapPlayer(UsersPosition : Array): void; - deleteGroup(groupId: string): void; +interface GameSceneInitInterface { + initPosition: PointInterface|null } -export class GameScene extends Phaser.Scene implements GameSceneInterface, CreateSceneFromObjectConfig{ + +export class GameScene extends Phaser.Scene { GameManager : GameManager; Terrains : Array; CurrentPlayer: CurrentGamerInterface; MapPlayers : Phaser.Physics.Arcade.Group; + MapPlayersByKey : Map = new Map(); Map: Phaser.Tilemaps.Tilemap; Layers : Array; Objects : Array; @@ -34,6 +36,7 @@ export class GameScene extends Phaser.Scene implements GameSceneInterface, Creat startX = 704;// 22 case startY = 32; // 1 case circleTexture: CanvasTexture; + initPosition: PositionInterface; MapKey: string; MapUrlFile: string; @@ -86,7 +89,9 @@ export class GameScene extends Phaser.Scene implements GameSceneInterface, Creat } //hook initialisation - init() {} + init(initData : GameSceneInitInterface) { + this.initPosition = initData.initPosition; + } //hook create scene create(): void { @@ -210,7 +215,13 @@ export class GameScene extends Phaser.Scene implements GameSceneInterface, Creat /** * @param layer */ - private startUser(layer: ITiledMapLayer){ + private startUser(layer: ITiledMapLayer): void { + if (this.initPosition !== undefined) { + this.startX = this.initPosition.x; + this.startY = this.initPosition.y; + return; + } + let tiles : any = layer.data; tiles.forEach((objectKey : number, key: number) => { if(objectKey === 0){ @@ -275,16 +286,17 @@ export class GameScene extends Phaser.Scene implements GameSceneInterface, Creat this.startX, this.startY, this.GameManager.getPlayerName(), - this.GameManager.getCharacterSelected() + this.GameManager.getCharacterSelected(), + PlayerAnimationNames.WalkDown, + false ); - this.CurrentPlayer.initAnimation(); //create collision this.createCollisionWithPlayer(); this.createCollisionObject(); //join room - this.GameManager.joinRoom(this.scene.key); + this.GameManager.joinRoom(this.scene.key, this.startX, this.startY, PlayerAnimationNames.WalkDown, false); //listen event to share position of user this.CurrentPlayer.on(hasMovedEventName, this.pushPlayerPosition.bind(this)) @@ -333,6 +345,7 @@ export class GameScene extends Phaser.Scene implements GameSceneInterface, Creat /** * Share position in scene * @param UsersPosition + * @deprecated */ shareUserPosition(UsersPosition : Array): void { this.updateOrCreateMapPlayer(UsersPosition); @@ -358,7 +371,7 @@ export class GameScene extends Phaser.Scene implements GameSceneInterface, Creat if(!player){ this.addPlayer(userPosition); }else{ - player.updatePosition(userPosition); + player.updatePosition(userPosition.position); } }); @@ -372,31 +385,65 @@ export class GameScene extends Phaser.Scene implements GameSceneInterface, Creat }); } + public initUsersPosition(usersPosition: MessageUserPositionInterface[]): void { + if(!this.CurrentPlayer){ + console.error('Cannot initiate users list because map is not loaded yet') + return; + } + + let currentPlayerId = this.GameManager.getPlayerId(); + + // clean map + this.MapPlayersByKey.forEach((player: GamerInterface) => { + player.destroy(); + this.MapPlayers.remove(player); + }); + this.MapPlayersByKey = new Map(); + + // load map + usersPosition.forEach((userPosition : MessageUserPositionInterface) => { + if(userPosition.userId === currentPlayerId){ + return; + } + this.addPlayer(userPosition); + }); + } + private findPlayerInMap(UserId : string) : GamerInterface | null{ - let player = this.MapPlayers.getChildren().find((player: Player) => UserId === player.userId); + return this.MapPlayersByKey.get(UserId); + /*let player = this.MapPlayers.getChildren().find((player: Player) => UserId === player.userId); if(!player){ return null; } - return (player as GamerInterface); + return (player as GamerInterface);*/ } /** * Create new player - * @param MessageUserPosition */ - addPlayer(MessageUserPosition : MessageUserPositionInterface) : void{ + public addPlayer(addPlayerData : AddPlayerInterface) : void{ + //check if exist player, if exist, move position + if(this.MapPlayersByKey.has(addPlayerData.userId)){ + this.updatePlayerPosition({ + userId: addPlayerData.userId, + position: addPlayerData.position + }); + return; + } //initialise player let player = new Player( - MessageUserPosition.userId, + addPlayerData.userId, this, - MessageUserPosition.position.x, - MessageUserPosition.position.y, - MessageUserPosition.name, - MessageUserPosition.character + addPlayerData.position.x, + addPlayerData.position.y, + addPlayerData.name, + addPlayerData.character, + addPlayerData.position.direction, + addPlayerData.position.moving ); - player.initAnimation(); this.MapPlayers.add(player); - player.updatePosition(MessageUserPosition); + this.MapPlayersByKey.set(player.userId, player); + player.updatePosition(addPlayerData.position); //init collision /*this.physics.add.collider(this.CurrentPlayer, player, (CurrentPlayer: CurrentGamerInterface, MapPlayer: GamerInterface) => { @@ -404,6 +451,25 @@ export class GameScene extends Phaser.Scene implements GameSceneInterface, Creat });*/ } + public removePlayer(userId: string) { + console.log('Removing player ', userId) + let player = this.MapPlayersByKey.get(userId); + if (player === undefined) { + console.error('Cannot find user with id ', userId); + } + player.destroy(); + this.MapPlayers.remove(player); + this.MapPlayersByKey.delete(userId); + } + + updatePlayerPosition(message: MessageUserMovedInterface): void { + let player : GamerInterface | undefined = this.MapPlayersByKey.get(message.userId); + if (player === undefined) { + throw new Error('Cannot find player with ID "' + message.userId +'"'); + } + player.updatePosition(message.position); + } + shareGroupPosition(groupPositionMessage: GroupCreatedUpdatedMessageInterface) { let groupId = groupPositionMessage.groupId; diff --git a/front/src/Phaser/Login/LogincScene.ts b/front/src/Phaser/Login/LogincScene.ts index f7dd0029..c289b999 100644 --- a/front/src/Phaser/Login/LogincScene.ts +++ b/front/src/Phaser/Login/LogincScene.ts @@ -2,12 +2,11 @@ import {gameManager} from "../Game/GameManager"; import {TextField} from "../Components/TextField"; import {TextInput} from "../Components/TextInput"; import {ClickButton} from "../Components/ClickButton"; -import {GameScene, GameSceneInterface} from "../Game/GameScene"; import Image = Phaser.GameObjects.Image; import Rectangle = Phaser.GameObjects.Rectangle; import {PLAYER_RESOURCES} from "../Entity/PlayableCaracter"; import {cypressAsserter} from "../../Cypress/CypressAsserter"; -import {GroupCreatedUpdatedMessageInterface, MessageUserPositionInterface} from "../../Connexion"; +import {GroupCreatedUpdatedMessageInterface, MessageUserJoined, MessageUserPositionInterface} from "../../Connexion"; export function getMapKeyByUrl(mapUrlStart: string){ // FIXME: the key should be computed from the full URL of the map. @@ -28,7 +27,7 @@ enum LoginTextures { mainFont = "main_font" } -export class LogincScene extends Phaser.Scene implements GameSceneInterface { +export class LogincScene extends Phaser.Scene { private nameInput: TextInput; private textField: TextField; private playButton: ClickButton; @@ -168,20 +167,4 @@ export class LogincScene extends Phaser.Scene implements GameSceneInterface { this.selectedPlayer = this.players[0]; this.selectedPlayer.play(PLAYER_RESOURCES[0].name); } - - shareUserPosition(UsersPosition: import("../../Connexion").MessageUserPositionInterface[]): void { - throw new Error("Method not implemented."); - } - - deleteGroup(groupId: string): void { - throw new Error("Method not implemented."); - } - - shareGroupPosition(groupPositionMessage: GroupCreatedUpdatedMessageInterface): void { - throw new Error("Method not implemented."); - } - - updateOrCreateMapPlayer(UsersPosition: Array): void { - throw new Error("Method not implemented."); - } } diff --git a/front/src/Phaser/Player/Animation.ts b/front/src/Phaser/Player/Animation.ts index 332c862d..10cee1e8 100644 --- a/front/src/Phaser/Player/Animation.ts +++ b/front/src/Phaser/Player/Animation.ts @@ -14,7 +14,6 @@ export enum PlayerAnimationNames { WalkLeft = 'left', WalkUp = 'up', WalkRight = 'right', - None = 'none', } export const getPlayerAnimations = (name: string = Textures.Player): AnimationData[] => { @@ -48,11 +47,3 @@ export const getPlayerAnimations = (name: string = Textures.Player): AnimationDa repeat: -1 }]; }; - -export const playAnimation = (Player : Phaser.GameObjects.Sprite, direction : string) => { - if (direction !== PlayerAnimationNames.None && (!Player.anims.currentAnim || Player.anims.currentAnim.key !== direction)) { - Player.anims.play(direction); - } else if (direction === PlayerAnimationNames.None && Player.anims.currentAnim) { - Player.anims.stop(); - } -} diff --git a/front/src/Phaser/Player/Player.ts b/front/src/Phaser/Player/Player.ts index 73aad77e..850be563 100644 --- a/front/src/Phaser/Player/Player.ts +++ b/front/src/Phaser/Player/Player.ts @@ -1,36 +1,37 @@ -import {getPlayerAnimations, playAnimation, PlayerAnimationNames} from "./Animation"; -import {GameSceneInterface, Textures} from "../Game/GameScene"; -import {MessageUserPositionInterface} from "../../Connexion"; +import {getPlayerAnimations, PlayerAnimationNames} from "./Animation"; +import {GameScene, Textures} from "../Game/GameScene"; +import {MessageUserPositionInterface, PointInterface} from "../../Connexion"; import {ActiveEventList, UserInputEvent, UserInputManager} from "../UserInput/UserInputManager"; import {PlayableCaracter} from "../Entity/PlayableCaracter"; export const hasMovedEventName = "hasMoved"; export interface CurrentGamerInterface extends PlayableCaracter{ - initAnimation() : void; moveUser(delta: number) : void; say(text : string) : void; } export interface GamerInterface extends PlayableCaracter{ userId : string; - initAnimation() : void; - updatePosition(MessageUserPosition : MessageUserPositionInterface) : void; + updatePosition(position: PointInterface): void; say(text : string) : void; } export class Player extends PlayableCaracter implements CurrentGamerInterface, GamerInterface { userId: string; userInputManager: UserInputManager; - previousMove: string; + previousDirection: string; + wasMoving: boolean; constructor( userId: string, - Scene: GameSceneInterface, + Scene: GameScene, x: number, y: number, name: string, - PlayerTexture: string = Textures.Player + PlayerTexture: string, + direction: string, + moving: boolean ) { super(Scene, x, y, PlayerTexture, name, 1); @@ -42,9 +43,18 @@ export class Player extends PlayableCaracter implements CurrentGamerInterface, G //the current player model should be push away by other players to prevent conflict this.setImmovable(false); + this.initAnimation(); + + console.warn("Start direction for added player ", direction) + console.warn("Position ", x, y) + /*this.play(`${PlayerTexture}-${direction}`, true); + if (!moving) { + this.stop(); + }*/ + this.playAnimation(direction, moving); } - initAnimation(): void { + private initAnimation(): void { getPlayerAnimations(this.PlayerTexture).forEach(d => { this.scene.anims.create({ key: d.key, @@ -58,6 +68,7 @@ export class Player extends PlayableCaracter implements CurrentGamerInterface, G moveUser(delta: number): void { //if user client on shift, camera and player speed let direction = null; + let moving = false; let activeEvents = this.userInputManager.getEventListForGameTick(); let speedMultiplier = activeEvents.get(UserInputEvent.SpeedUp) ? 25 : 9; @@ -67,39 +78,56 @@ export class Player extends PlayableCaracter implements CurrentGamerInterface, G let y = 0; if (activeEvents.get(UserInputEvent.MoveUp)) { y = - moveAmount; - direction = `${this.PlayerTexture}-${PlayerAnimationNames.WalkUp}`; + direction = PlayerAnimationNames.WalkUp; + moving = true; } else if (activeEvents.get(UserInputEvent.MoveDown)) { y = moveAmount; - direction = `${this.PlayerTexture}-${PlayerAnimationNames.WalkDown}`; + direction = PlayerAnimationNames.WalkDown; + moving = true; } if (activeEvents.get(UserInputEvent.MoveLeft)) { x = -moveAmount; - direction = `${this.PlayerTexture}-${PlayerAnimationNames.WalkLeft}`; + direction = PlayerAnimationNames.WalkLeft; + moving = true; } else if (activeEvents.get(UserInputEvent.MoveRight)) { x = moveAmount; - direction = `${this.PlayerTexture}-${PlayerAnimationNames.WalkRight}`; + direction = PlayerAnimationNames.WalkRight; + moving = true; } if (x !== 0 || y !== 0) { this.move(x, y); - this.emit(hasMovedEventName, {direction, x: this.x, y: this.y}); + this.emit(hasMovedEventName, {moving, direction, x: this.x, y: this.y}); } else { - if (this.previousMove !== PlayerAnimationNames.None) { - direction = PlayerAnimationNames.None; + if (this.wasMoving) { + //direction = PlayerAnimationNames.None; this.stop(); - this.emit(hasMovedEventName, {direction, x: this.x, y: this.y}); + this.emit(hasMovedEventName, {moving, direction: this.previousDirection, x: this.x, y: this.y}); } } if (direction !== null) { - this.previousMove = direction; + this.previousDirection = direction; } + this.wasMoving = moving; } //todo: put this method into the NonPlayer class instead - updatePosition(MessageUserPosition: MessageUserPositionInterface) { - playAnimation(this, MessageUserPosition.position.direction); - this.setX(MessageUserPosition.position.x); - this.setY(MessageUserPosition.position.y); - this.setDepth(MessageUserPosition.position.y); + 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.anims.play(this.PlayerTexture+'-'+direction); + } else if (!moving) { + /*if (this.anims.currentAnim) { + this.anims.stop(); + }*/ + this.anims.play(this.PlayerTexture+'-'+direction); + this.anims.stop(); + } } } diff --git a/front/src/Phaser/Reconnecting/ReconnectingScene.ts b/front/src/Phaser/Reconnecting/ReconnectingScene.ts new file mode 100644 index 00000000..273820b7 --- /dev/null +++ b/front/src/Phaser/Reconnecting/ReconnectingScene.ts @@ -0,0 +1,56 @@ +import {gameManager} from "../Game/GameManager"; +import {TextField} from "../Components/TextField"; +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 {cypressAsserter} from "../../Cypress/CypressAsserter"; +import Sprite = Phaser.GameObjects.Sprite; + +export const ReconnectingSceneName = "ReconnectingScene"; +enum ReconnectingTextures { + icon = "icon", + mainFont = "main_font" +} + +export class ReconnectingScene extends Phaser.Scene { + private reconnectingField: TextField; + private logo: Image; + private cat: Sprite; + + constructor() { + super({ + key: ReconnectingSceneName + }); + } + + preload() { + this.load.image(ReconnectingTextures.icon, "resources/logos/tcm_full.png"); + // Note: arcade.png from the Phaser 3 examples at: https://github.com/photonstorm/phaser3-examples/tree/master/public/assets/fonts/bitmap + this.load.bitmapFont(ReconnectingTextures.mainFont, 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml'); + this.load.spritesheet( + 'cat', + 'resources/characters/pipoya/Cat 01-1.png', + {frameWidth: 32, frameHeight: 32} + ); + } + + create() { + this.logo = new Image(this, this.game.renderer.width - 30, this.game.renderer.height - 20, ReconnectingTextures.icon); + this.add.existing(this.logo); + + this.reconnectingField = new TextField(this, this.game.renderer.width / 2, this.game.renderer.height / 2, "Connection lost. Reconnecting..."); + this.reconnectingField.setOrigin(0.5, 0.5).setCenterAlign(); + + let cat = this.physics.add.sprite(this.game.renderer.width / 2, this.game.renderer.height / 2 - 32, 'cat'); + this.anims.create({ + key: 'right', + frames: this.anims.generateFrameNumbers('cat', { start: 6, end: 8 }), + frameRate: 10, + repeat: -1 + }); + cat.play('right'); + + } +} diff --git a/front/src/Phaser/UserInput/UserInputManager.ts b/front/src/Phaser/UserInput/UserInputManager.ts index 6c5b32c2..9c4ca660 100644 --- a/front/src/Phaser/UserInput/UserInputManager.ts +++ b/front/src/Phaser/UserInput/UserInputManager.ts @@ -1,5 +1,5 @@ import Map = Phaser.Structs.Map; -import {GameSceneInterface} from "../Game/GameScene"; +import {GameScene} from "../Game/GameScene"; interface UserInputManagerDatum { keyCode: number; @@ -29,7 +29,7 @@ export class ActiveEventList { set(event: UserInputEvent, value: boolean): boolean { return this.KeysCode[event] = true; } -} +} //this class is responsible for catching user inputs and listing all active user actions at every game tick events. export class UserInputManager { @@ -38,19 +38,19 @@ export class UserInputManager { {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}, ]; - - constructor(Scene : GameSceneInterface) { + + constructor(Scene : GameScene) { this.KeysCode.forEach(d => { d.keyInstance = Scene.input.keyboard.addKey(d.keyCode); }); @@ -65,4 +65,4 @@ export class UserInputManager { }); return eventsMap; } -} \ No newline at end of file +} diff --git a/front/src/WebRtc/SimplePeer.ts b/front/src/WebRtc/SimplePeer.ts index 595e83b2..b301876e 100644 --- a/front/src/WebRtc/SimplePeer.ts +++ b/front/src/WebRtc/SimplePeer.ts @@ -141,9 +141,9 @@ export class SimplePeer implements SimplePeerInterface{ this.stream(user.userId, stream); }); - this.PeerConnexionArray.get(user.userId).on('track', (track: MediaStreamTrack, stream: MediaStream) => { + /*this.PeerConnexionArray.get(user.userId).on('track', (track: MediaStreamTrack, stream: MediaStream) => { this.stream(user.userId, stream); - }); + });*/ this.PeerConnexionArray.get(user.userId).on('close', () => { this.closeConnexion(user.userId); @@ -157,6 +157,13 @@ export class SimplePeer implements SimplePeerInterface{ console.info(`connect => ${user.userId}`); }); + this.PeerConnexionArray.get(user.userId).on('data', (chunk: Buffer) => { + let data = JSON.parse(chunk.toString('utf8')); + if(data.type === "stream"){ + this.stream(user.userId, data.stream); + } + }); + this.addMedia(user.userId); } @@ -205,6 +212,11 @@ export class SimplePeer implements SimplePeerInterface{ * @param stream */ private stream(userId : any, stream: MediaStream) { + if(!stream){ + this.MediaManager.disabledVideoByUserId(userId); + this.MediaManager.disabledMicrophoneByUserId(userId); + return; + } this.MediaManager.addStreamRemoteVideo(userId, stream); } @@ -216,6 +228,14 @@ export class SimplePeer implements SimplePeerInterface{ try { let transceiver : any = null; if(!this.MediaManager.localStream){ + //send fake signal + if(!this.PeerConnexionArray.has(userId)){ + return; + } + this.PeerConnexionArray.get(userId).write(new Buffer(JSON.stringify({ + type: "stream", + stream: null + }))); return; } this.MediaManager.localStream.getTracks().forEach( diff --git a/front/src/index.ts b/front/src/index.ts index e6c8ce40..2ab8d91a 100644 --- a/front/src/index.ts +++ b/front/src/index.ts @@ -3,13 +3,15 @@ import GameConfig = Phaser.Types.Core.GameConfig; import {DEBUG_MODE, RESOLUTION} from "./Enum/EnvironmentVariable"; import {cypressAsserter} from "./Cypress/CypressAsserter"; import {LogincScene} from "./Phaser/Login/LogincScene"; +import {ReconnectingScene} from "./Phaser/Reconnecting/ReconnectingScene"; +import {gameManager} from "./Phaser/Game/GameManager"; const config: GameConfig = { title: "Office game", width: window.innerWidth / RESOLUTION, height: window.innerHeight / RESOLUTION, parent: "game", - scene: [LogincScene], + scene: [LogincScene, ReconnectingScene], zoom: RESOLUTION, physics: { default: "arcade",