From ee612f65859b5629c8db945ad25c4eb647c38e4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Mon, 27 Jul 2020 22:36:07 +0200 Subject: [PATCH] Adding event support to items --- back/src/Controller/IoSocketController.ts | 38 +++++++++- back/src/Model/Websocket/ItemEventMessage.ts | 10 +++ back/src/Model/World.ts | 10 +++ front/package.json | 1 + front/src/Connection.ts | 34 ++++++++- front/src/Phaser/Game/GameScene.ts | 68 +++++++++++++---- front/src/Phaser/Items/ActionableItem.ts | 36 ++++++++- front/src/Phaser/Items/Computer/computer.ts | 74 +++++++++++++++++-- .../src/Phaser/Items/ItemFactoryInterface.ts | 2 +- front/src/Phaser/Player/Player.ts | 11 +-- front/yarn.lock | 5 ++ 11 files changed, 253 insertions(+), 36 deletions(-) create mode 100644 back/src/Model/Websocket/ItemEventMessage.ts diff --git a/back/src/Controller/IoSocketController.ts b/back/src/Controller/IoSocketController.ts index edd29e7b..f0ac0337 100644 --- a/back/src/Controller/IoSocketController.ts +++ b/back/src/Controller/IoSocketController.ts @@ -19,6 +19,7 @@ import {isJoinRoomMessageInterface} from "../Model/Websocket/JoinRoomMessage"; import {isPointInterface, PointInterface} from "../Model/Websocket/PointInterface"; import {isWebRtcSignalMessageInterface} from "../Model/Websocket/WebRtcSignalMessage"; import {UserInGroupInterface} from "../Model/Websocket/UserInGroupInterface"; +import {isItemEventMessageInterface} from "../Model/Websocket/ItemEventMessage"; enum SockerIoEvent { CONNECTION = "connection", @@ -33,7 +34,8 @@ enum SockerIoEvent { MESSAGE_ERROR = "message-error", GROUP_CREATE_UPDATE = "group-create-update", GROUP_DELETE = "group-delete", - SET_PLAYER_DETAILS = "set-player-details" + SET_PLAYER_DETAILS = "set-player-details", + ITEM_EVENT = 'item-event', } export class IoSocketController { @@ -190,7 +192,16 @@ export class IoSocketController { } return new MessageUserPosition(user.id, player.name, player.character, player.position); }).filter((item: MessageUserPosition|null) => item !== null); - answerFn(listOfUsers); + + const listOfItems: {[itemId: string]: unknown} = {}; + for (const [itemId, item] of world.getItemsState().entries()) { + listOfItems[itemId] = item; + } + + answerFn({ + users: listOfUsers, + items: listOfItems + }); } catch (e) { console.error('An error occurred on "join_room" event'); console.error(e); @@ -281,6 +292,29 @@ export class IoSocketController { Client.character = playerDetails.character; answerFn(Client.userId); }); + + socket.on(SockerIoEvent.ITEM_EVENT, (itemEvent: unknown) => { + if (!isItemEventMessageInterface(itemEvent)) { + socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: 'Invalid ITEM_EVENT message.'}); + console.warn('Invalid ITEM_EVENT message received: ', itemEvent); + return; + } + try { + const Client = (socket as ExSocketInterface); + + socket.to(Client.roomId).emit(SockerIoEvent.ITEM_EVENT, itemEvent); + + const world = this.Worlds.get(Client.roomId); + if (!world) { + console.error("Could not find world with id '", Client.roomId, "'"); + return; + } + world.setItemState(itemEvent.itemId, itemEvent.state); + } catch (e) { + console.error('An error occurred on "item_event"'); + console.error(e); + } + }); }); } diff --git a/back/src/Model/Websocket/ItemEventMessage.ts b/back/src/Model/Websocket/ItemEventMessage.ts new file mode 100644 index 00000000..b1f9203e --- /dev/null +++ b/back/src/Model/Websocket/ItemEventMessage.ts @@ -0,0 +1,10 @@ +import * as tg from "generic-type-guard"; + +export const isItemEventMessageInterface = + new tg.IsInterface().withProperties({ + itemId: tg.isNumber, + event: tg.isString, + state: tg.isUnknown, + parameters: tg.isUnknown, + }).get(); +export type ItemEventMessageInterface = tg.GuardedType; diff --git a/back/src/Model/World.ts b/back/src/Model/World.ts index 6d4fc205..5d26f817 100644 --- a/back/src/Model/World.ts +++ b/back/src/Model/World.ts @@ -27,6 +27,8 @@ export class World { private readonly groupUpdatedCallback: GroupUpdatedCallback; private readonly groupDeletedCallback: GroupDeletedCallback; + private itemsState: Map = new Map(); + constructor(connectCallback: ConnectCallback, disconnectCallback: DisconnectCallback, minDistance: number, @@ -227,6 +229,14 @@ export class World { return Math.sqrt(Math.pow(position2.x - position1.x, 2) + Math.pow(position2.y - position1.y, 2)); } + public setItemState(itemId: number, state: unknown) { + this.itemsState.set(itemId, state); + } + + public getItemsState(): Map { + return this.itemsState; + } + /*getDistancesBetweenGroupUsers(group: Group): Distance[] { let i = 0; diff --git a/front/package.json b/front/package.json index e5ea5b66..a9c7b3f8 100644 --- a/front/package.json +++ b/front/package.json @@ -22,6 +22,7 @@ "@types/axios": "^0.14.0", "@types/simple-peer": "^9.6.0", "@types/socket.io-client": "^1.4.32", + "generic-type-guard": "^3.2.0", "phaser": "^3.22.0", "queue-typescript": "^1.0.1", "simple-peer": "^9.6.2", diff --git a/front/src/Connection.ts b/front/src/Connection.ts index c4ac92c6..5e51974f 100644 --- a/front/src/Connection.ts +++ b/front/src/Connection.ts @@ -22,6 +22,7 @@ enum EventMessage{ GROUP_CREATE_UPDATE = "group-create-update", GROUP_DELETE = "group-delete", SET_PLAYER_DETAILS = "set-player-details", // Send the name and character to the server (on connect), receive back the id. + ITEM_EVENT = 'item-event', CONNECT_ERROR = "connect_error", } @@ -91,6 +92,18 @@ export interface StartMapInterface { startInstance: string } +export interface ItemEventMessageInterface { + itemId: number, + event: string, + state: unknown, + parameters: unknown +} + +export interface RoomJoinedMessageInterface { + users: MessageUserPositionInterface[] + items: { [itemId: number] : unknown } +} + export class Connection implements Connection { private readonly socket: Socket; private userId: string|null = null; @@ -147,10 +160,10 @@ export class Connection implements Connection { } - public joinARoom(roomId: string, startX: number, startY: number, direction: string, moving: boolean): Promise { - const promise = new Promise((resolve, reject) => { - this.socket.emit(EventMessage.JOIN_ROOM, { roomId, position: {x: startX, y: startY, direction, moving }}, (userPositions: MessageUserPositionInterface[]) => { - resolve(userPositions); + public joinARoom(roomId: string, startX: number, startY: number, direction: string, moving: boolean): Promise { + const promise = new Promise((resolve, reject) => { + this.socket.emit(EventMessage.JOIN_ROOM, { roomId, position: {x: startX, y: startY, direction, moving }}, (roomJoinedMessage: RoomJoinedMessageInterface) => { + resolve(roomJoinedMessage); }); }) return promise; @@ -223,4 +236,17 @@ export class Connection implements Connection { disconnectMessage(callback: (message: WebRtcDisconnectMessageInterface) => void): void { this.socket.on(EventMessage.WEBRTC_DISCONNECT, callback); } + + emitActionableEvent(itemId: number, event: string, state: unknown, parameters: unknown) { + return this.socket.emit(EventMessage.ITEM_EVENT, { + itemId, + event, + state, + parameters + }); + } + + onActionableEvent(callback: (message: ItemEventMessageInterface) => void): void { + this.socket.on(EventMessage.ITEM_EVENT, callback); + } } diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 9ee54058..c7c9e626 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -3,7 +3,7 @@ import { Connection, GroupCreatedUpdatedMessageInterface, MessageUserJoined, MessageUserMovedInterface, - MessageUserPositionInterface, PointInterface, PositionInterface + MessageUserPositionInterface, PointInterface, PositionInterface, RoomJoinedMessageInterface } from "../../Connection"; import {CurrentGamerInterface, hasMovedEventName, Player} from "../Player/Player"; import { DEBUG_MODE, ZOOM_LEVEL, POSITION_DELAY } from "../../Enum/EnvironmentVariable"; @@ -30,6 +30,7 @@ import FILE_LOAD_ERROR = Phaser.Loader.Events.FILE_LOAD_ERROR; import {FourOFourSceneName} from "../Reconnecting/FourOFourScene"; import {ItemFactoryInterface} from "../Items/ItemFactoryInterface"; import {ActionableItem} from "../Items/ActionableItem"; +import {UserInputManager} from "../UserInput/UserInputManager"; export enum Textures { @@ -91,6 +92,8 @@ export class GameScene extends Phaser.Scene { private connection: Connection; private simplePeer : SimplePeer; private connectionPromise: Promise + private connectionAnswerPromise: Promise; + private connectionAnswerPromiseResolve: (value?: RoomJoinedMessageInterface | PromiseLike) => void; // A promise that will resolve when the "create" method is called (signaling loading is ended) private createPromise: Promise; private createPromiseResolve: (value?: void | PromiseLike) => void; @@ -111,9 +114,10 @@ export class GameScene extends Phaser.Scene { private PositionNextScene: Array> = new Array>(); private startLayerName: string|undefined; - private actionableItems: Array = new Array(); + private actionableItems: Map = new Map(); // The item that can be selected by pressing the space key. private outlinedItem: ActionableItem|null = null; + private userInputManager: UserInputManager; static createFromUrl(mapUrlFile: string, instance: string, key: string|null = null): GameScene { const mapKey = GameScene.getMapKeyByUrl(mapUrlFile); @@ -140,6 +144,9 @@ export class GameScene extends Phaser.Scene { this.createPromise = new Promise((resolve, reject): void => { this.createPromiseResolve = resolve; }) + this.connectionAnswerPromise = new Promise((resolve, reject): void => { + this.connectionAnswerPromiseResolve = resolve; + }) } //hook preload scene @@ -225,6 +232,15 @@ export class GameScene extends Phaser.Scene { this.scene.remove(this.scene.key); }) + connection.onActionableEvent((message => { + const item = this.actionableItems.get(message.itemId); + if (item === undefined) { + console.warn('Received an event about object "'+message.itemId+'" but cannot find this item on the map.'); + return; + } + item.fire(message.event, message.state, message.parameters); + })); + // When connection is performed, let's connect SimplePeer this.simplePeer = new SimplePeer(this.connection); @@ -293,13 +309,19 @@ export class GameScene extends Phaser.Scene { this.load.on('complete', () => { // FIXME: the factory might fail because the resources might not be loaded yet... // We would need to add a loader ended event in addition to the createPromise - this.createPromise.then(() => { + this.createPromise.then(async () => { itemFactory.create(this); + const roomJoinedAnswer = await this.connectionAnswerPromise; + for (const object of objectsOfType) { // TODO: we should pass here a factory to create sprites (maybe?) - const actionableItem = itemFactory.factory(this, object); - this.actionableItems.push(actionableItem); + + // Do we have a state for this object? + const state = roomJoinedAnswer.items[object.id]; + + const actionableItem = itemFactory.factory(this, object, state); + this.actionableItems.set(actionableItem.getId(), actionableItem); } }); }); @@ -414,13 +436,15 @@ export class GameScene extends Phaser.Scene { //initialise list of other player this.MapPlayers = this.physics.add.group({ immovable: true }); + //create input to move + this.userInputManager = new UserInputManager(this); + //notify game manager can to create currentUser in map this.createCurrentPlayer(); //initialise camera this.initCamera(); - // Let's generate the circle for the group delimiter const circleElement = Object.values(this.textures.list).find((object: Texture) => object.key === 'circleSprite'); if(circleElement) { @@ -455,6 +479,11 @@ export class GameScene extends Phaser.Scene { } this.createPromiseResolve(); + + // TODO: use inputmanager instead + this.input.keyboard.on('keyup-SPACE', () => { + this.outlinedItem?.activate(); + }); } private getExitSceneUrl(layer: ITiledMapLayer): string|undefined { @@ -605,7 +634,8 @@ export class GameScene extends Phaser.Scene { this.GameManager.getPlayerName(), this.GameManager.getCharacterSelected(), PlayerAnimationNames.WalkDown, - false + false, + this.userInputManager ); //create collision @@ -614,8 +644,9 @@ export class GameScene extends Phaser.Scene { //join room this.connectionPromise.then((connection: Connection) => { - connection.joinARoom(this.RoomId, this.startX, this.startY, PlayerAnimationNames.WalkDown, false).then((userPositions: MessageUserPositionInterface[]) => { - this.initUsersPosition(userPositions); + connection.joinARoom(this.RoomId, this.startX, this.startY, PlayerAnimationNames.WalkDown, false).then((roomJoinedMessage: RoomJoinedMessageInterface) => { + this.initUsersPosition(roomJoinedMessage.users); + this.connectionAnswerPromiseResolve(roomJoinedMessage); }); //listen event to share position of user @@ -676,7 +707,7 @@ export class GameScene extends Phaser.Scene { let shortestDistance: number = Infinity; let selectedItem: ActionableItem|null = null; - for (const item of this.actionableItems) { + for (const item of this.actionableItems.values()) { const distance = item.actionableDistance(x, y); if (distance !== null && distance < shortestDistance) { shortestDistance = distance; @@ -766,10 +797,7 @@ export class GameScene extends Phaser.Scene { } } - /** - * - */ - checkToExit(): {key: string, hash: string} | null { + private checkToExit(): {key: string, hash: string} | null { const x = Math.floor(this.CurrentPlayer.x / 32); const y = Math.floor(this.CurrentPlayer.y / 32); @@ -947,4 +975,16 @@ export class GameScene extends Phaser.Scene { const endPos = mapUrlStart.indexOf(".json"); return mapUrlStart.substring(startPos, endPos); } + + /** + * Sends to the server an event emitted by one of the ActionableItems. + * + * @param itemId + * @param eventName + * @param state + * @param parameters + */ + emitActionableEvent(itemId: number, eventName: string, state: unknown, parameters: unknown) { + this.connection.emitActionableEvent(itemId, eventName, state, parameters); + } } diff --git a/front/src/Phaser/Items/ActionableItem.ts b/front/src/Phaser/Items/ActionableItem.ts index 01e85c64..36b14921 100644 --- a/front/src/Phaser/Items/ActionableItem.ts +++ b/front/src/Phaser/Items/ActionableItem.ts @@ -4,15 +4,23 @@ */ import Sprite = Phaser.GameObjects.Sprite; import {OutlinePipeline} from "../Shaders/OutlinePipeline"; +import {GameScene} from "../Game/GameScene"; + +type EventCallback = (state: unknown, parameters: unknown) => void; export class ActionableItem { private readonly activationRadiusSquared : number; private isSelectable: boolean = false; + private callbacks: Map> = new Map>(); - public constructor(private sprite: Sprite, private activationRadius: number) { + public constructor(private id: number, private sprite: Sprite, private eventHandler: GameScene, private activationRadius: number, private onActivateCallback: (item: ActionableItem) => void) { this.activationRadiusSquared = activationRadius * activationRadius; } + public getId(): number { + return this.id; + } + /** * Returns the square of the distance to the object center IF we are in item action range * OR null if we are out of range. @@ -54,7 +62,31 @@ export class ActionableItem { * Triggered when the "space" key is pressed and the object is in range of being activated. */ public activate(): void { + this.onActivateCallback(this); + } + public emit(eventName: string, state: unknown, parameters: unknown = null): void { + this.eventHandler.emitActionableEvent(this.id, eventName, state, parameters); + // Also, execute the action locally. + this.fire(eventName, state, parameters); + } + + public on(eventName: string, callback: EventCallback): void { + let callbacksArray: Array|undefined = this.callbacks.get(eventName); + if (callbacksArray === undefined) { + callbacksArray = new Array(); + this.callbacks.set(eventName, callbacksArray); + } + callbacksArray.push(callback); + } + + public fire(eventName: string, state: unknown, parameters: unknown): void { + const callbacksArray = this.callbacks.get(eventName); + if (callbacksArray === undefined) { + return; + } + for (const callback of callbacksArray) { + callback(state, parameters); + } } } - diff --git a/front/src/Phaser/Items/Computer/computer.ts b/front/src/Phaser/Items/Computer/computer.ts index b979ebf6..fdc7a358 100644 --- a/front/src/Phaser/Items/Computer/computer.ts +++ b/front/src/Phaser/Items/Computer/computer.ts @@ -5,20 +5,82 @@ import {ITiledMapObject} from "../../Map/ITiledMap"; import {ItemFactoryInterface} from "../ItemFactoryInterface"; import {GameScene} from "../../Game/GameScene"; import {ActionableItem} from "../ActionableItem"; +import * as tg from "generic-type-guard"; + +const isComputerState = + new tg.IsInterface().withProperties({ + status: tg.isString, + }).get(); +type ComputerState = tg.GuardedType; + +let state: ComputerState = { + 'status': 'off' +}; export default { preload: (loader: Phaser.Loader.LoaderPlugin): void => { loader.atlas('computer', '/resources/items/computer/computer.png', '/resources/items/computer/computer_atlas.json'); }, create: (scene: GameScene): void => { - + scene.anims.create({ + key: 'computer_off', + frames: [ + { + key: 'computer', + frame: 'computer_off' + } + ], + frameRate: 10, + repeat: -1 + }); + scene.anims.create({ + key: 'computer_run', + frames: [ + { + key: 'computer', + frame: 'computer_on1' + }, + { + key: 'computer', + frame: 'computer_on2' + } + ], + frameRate: 5, + repeat: -1 + }); }, - factory: (scene: GameScene, object: ITiledMapObject): ActionableItem => { - // Idée: ESSAYER WebPack? https://paultavares.wordpress.com/2018/07/02/webpack-how-to-generate-an-es-module-bundle/ - const foo = new Sprite(scene, object.x, object.y, 'computer'); - scene.add.existing(foo); + factory: (scene: GameScene, object: ITiledMapObject, initState: unknown): ActionableItem => { + if (initState !== undefined) { + if (!isComputerState(initState)) { + throw new Error('Invalid state received for computer object'); + } + state = initState; + } - return new ActionableItem(foo, 32); + // Idée: ESSAYER WebPack? https://paultavares.wordpress.com/2018/07/02/webpack-how-to-generate-an-es-module-bundle/ + const computer = new Sprite(scene, object.x, object.y, 'computer'); + scene.add.existing(computer); + if (state.status === 'on') { + computer.anims.play('computer_run'); + } + + const item = new ActionableItem(object.id, computer, scene, 32, (item: ActionableItem) => { + if (state.status === 'off') { + state.status = 'on'; + item.emit('TURN_ON', state); + } else { + state.status = 'off'; + item.emit('TURN_OFF', state); + } + }); + item.on('TURN_ON', () => { + computer.anims.play('computer_run'); + }); + item.on('TURN_OFF', () => { + computer.anims.play('computer_off'); + }); + + return item; //scene.add.sprite(object.x, object.y, 'computer'); } } as ItemFactoryInterface; diff --git a/front/src/Phaser/Items/ItemFactoryInterface.ts b/front/src/Phaser/Items/ItemFactoryInterface.ts index 0f88f76b..e3e52517 100644 --- a/front/src/Phaser/Items/ItemFactoryInterface.ts +++ b/front/src/Phaser/Items/ItemFactoryInterface.ts @@ -6,5 +6,5 @@ import {ActionableItem} from "./ActionableItem"; export interface ItemFactoryInterface { preload: (loader: LoaderPlugin) => void; create: (scene: GameScene) => void; - factory: (scene: GameScene, object: ITiledMapObject) => ActionableItem; + factory: (scene: GameScene, object: ITiledMapObject, state: unknown) => ActionableItem; } diff --git a/front/src/Phaser/Player/Player.ts b/front/src/Phaser/Player/Player.ts index 64adc246..1ddd8b87 100644 --- a/front/src/Phaser/Player/Player.ts +++ b/front/src/Phaser/Player/Player.ts @@ -13,9 +13,8 @@ export interface CurrentGamerInterface extends Character{ } export class Player extends Character implements CurrentGamerInterface { - userInputManager: UserInputManager; - previousDirection: string; - wasMoving: boolean; + private previousDirection: string; + private wasMoving: boolean; constructor( Scene: GameScene, @@ -24,13 +23,11 @@ export class Player extends Character implements CurrentGamerInterface { name: string, PlayerTexture: string, direction: string, - moving: boolean + moving: boolean, + private userInputManager: UserInputManager ) { super(Scene, x, y, PlayerTexture, name, direction, moving, 1); - //create input to move - this.userInputManager = new UserInputManager(Scene); - //the current player model should be push away by other players to prevent conflict this.setImmovable(false); } diff --git a/front/yarn.lock b/front/yarn.lock index b943ebf0..ae7b5558 100644 --- a/front/yarn.lock +++ b/front/yarn.lock @@ -1904,6 +1904,11 @@ functional-red-black-tree@^1.0.1: resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= +generic-type-guard@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/generic-type-guard/-/generic-type-guard-3.2.0.tgz#1fb136f934730c776486526b8a21fe96b067e691" + integrity sha512-EkkrXYbOtJ3VPB+SOrU7EhwY65rZErItGtBg5wAqywaj07BOubwOZqMYaxOWekJ9akioGqXIsw1fYk3wwbWsDQ== + get-browser-rtc@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/get-browser-rtc/-/get-browser-rtc-1.0.2.tgz#bbcd40c8451a7ed4ef5c373b8169a409dd1d11d9"