From 1e002f93ed2edc6c285d7fbade8c795e0869111c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Tue, 9 Mar 2021 16:21:14 +0100 Subject: [PATCH] Implementation of openPopup script method (WIP) --- front/src/Api/Events/OpenPopupEvent.ts | 22 +++++++ front/src/Api/IframeListener.ts | 9 +++ front/src/Phaser/Game/GameScene.ts | 81 +++++++++++++++++--------- front/src/WebRtc/HtmlUtils.ts | 2 +- front/src/iframe_api.ts | 40 +++++++++++++ maps/tests/script.js | 23 ++++++++ maps/tests/script_api.json | 43 +++++++++++--- 7 files changed, 184 insertions(+), 36 deletions(-) create mode 100644 front/src/Api/Events/OpenPopupEvent.ts diff --git a/front/src/Api/Events/OpenPopupEvent.ts b/front/src/Api/Events/OpenPopupEvent.ts new file mode 100644 index 00000000..bbfc12bf --- /dev/null +++ b/front/src/Api/Events/OpenPopupEvent.ts @@ -0,0 +1,22 @@ +import * as tg from "generic-type-guard"; + +const isButtonDescriptor = + new tg.IsInterface().withProperties({ + label: tg.isString, + className: tg.isOptional(tg.isString), + closeOnClick: tg.isOptional(tg.isBoolean) + }).get(); +type ButtonDescriptor = tg.GuardedType; + +export const isOpenPopupEvent = + new tg.IsInterface().withProperties({ + popupId: tg.isNumber, + targetObject: tg.isString, + message: tg.isString, + buttons: tg.isAny //tg.isArray, + }).get(); + +/** + * A message sent from the iFrame to the game to add a message in the chat. + */ +export type OpenPopupEvent = tg.GuardedType; diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index 3cd0d38c..1a6a0ea7 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -5,6 +5,7 @@ import {UserInputChatEvent} from "./Events/UserInputChatEvent"; import * as crypto from "crypto"; import {HtmlUtils} from "../WebRtc/HtmlUtils"; import {EnterLeaveEvent} from "./Events/EnterLeaveEvent"; +import {isOpenPopupEvent, OpenPopupEvent} from "./Events/OpenPopupEvent"; @@ -16,6 +17,9 @@ class IframeListener { private readonly _chatStream: Subject = new Subject(); public readonly chatStream = this._chatStream.asObservable(); + private readonly _openPopupStream: Subject = new Subject(); + public readonly openPopupStream = this._openPopupStream.asObservable(); + private readonly iframes = new Set(); private readonly scripts = new Map(); @@ -36,9 +40,14 @@ class IframeListener { } const payload = message.data; + console.log('FOO'); if (isIframeEventWrapper(payload)) { + console.log('FOOBAR', payload); if (payload.type === 'chat' && isChatEvent(payload.data)) { this._chatStream.next(payload.data); + } else if (payload.type === 'openPopup' && isOpenPopupEvent(payload.data)) { + console.log('OPENPOPUP called'); + this._openPopupStream.next(payload.data); } } diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 18493fde..0a953363 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -75,6 +75,7 @@ import {localUserStore} from "../../Connexion/LocalUserStore"; import {iframeListener} from "../../Api/IframeListener"; import DOMElement = Phaser.GameObjects.DOMElement; import Tween = Phaser.Tweens.Tween; +import {HtmlUtils} from "../../WebRtc/HtmlUtils"; export interface GameSceneInitInterface { initPosition: PointInterface|null, @@ -165,8 +166,8 @@ export class GameScene extends ResizableScene implements CenterListener { private openChatIcon!: OpenChatIcon; private playerName!: string; private characterLayers!: string[]; + private popUpElements : Map = new Map(); - private popUpElement : DOMElement| undefined; constructor(private room: Room, MapUrlFile: string, customKey?: string|undefined) { super({ key: customKey ?? room.id @@ -442,6 +443,7 @@ export class GameScene extends ResizableScene implements CenterListener { // From now, this game scene will be notified of reposition events layoutManager.setListener(this); this.triggerOnMapLayerPropertyChange(); + this.listenToIframeEvents(); const camera = this.cameras.main; @@ -655,33 +657,6 @@ export class GameScene extends ResizableScene implements CenterListener { this.gameMap.onPropertyChange('exitSceneUrl', (newValue, oldValue) => { if (newValue) this.onMapExit(newValue as string); }); - this.gameMap.onPropertyChange('inGameConsoleMessage', (newValue, oldValue, allProps) => { - if (newValue !== undefined) { - this.popUpElement?.destroy(); - this.popUpElement = this.add.dom(2100, 150).createFromHTML(newValue as string); - this.popUpElement.scale = 0; - this.tweens.add({ - targets : this.popUpElement , - scale : 1, - ease : "EaseOut", - duration : 400, - }); - - this.popUpElement.setClassName("popUpElement"); - - } else { - this.tweens.add({ - targets : this.popUpElement , - scale : 0, - ease : "EaseOut", - duration : 400, - onComplete : () => { - this.popUpElement?.destroy(); - this.popUpElement = undefined; - }, - }); - } - }); this.gameMap.onPropertyChange('exitUrl', (newValue, oldValue) => { if (newValue) this.onMapExit(newValue as string); }); @@ -765,6 +740,56 @@ export class GameScene extends ResizableScene implements CenterListener { } + private listenToIframeEvents(): void { + iframeListener.openPopupStream.subscribe((openPopupEvent) => { + const escapedMessage = HtmlUtils.escapeHtml(openPopupEvent.message); + + let html = `
+${escapedMessage} +
`; + + const domElement = this.add.dom(150, 150).createFromHTML(html); + domElement.scale = 0; + domElement.setClassName('popUpElement'); + this.tweens.add({ + targets : domElement , + scale : 1, + ease : "EaseOut", + duration : 400, + }); + + this.popUpElements.set(openPopupEvent.popupId, domElement); + }); + /*this.gameMap.onPropertyChange('inGameConsoleMessage', (newValue, oldValue, allProps) => { + if (newValue !== undefined) { + this.popUpElement?.destroy(); + this.popUpElement = this.add.dom(2100, 150).createFromHTML(newValue as string); + this.popUpElement.scale = 0; + this.tweens.add({ + targets : this.popUpElement , + scale : 1, + ease : "EaseOut", + duration : 400, + }); + + this.popUpElement.setClassName("popUpElement"); + + } else { + this.tweens.add({ + targets : this.popUpElement , + scale : 0, + ease : "EaseOut", + duration : 400, + onComplete : () => { + this.popUpElement?.destroy(); + this.popUpElement = undefined; + }, + }); + } + });*/ + + } + private onMapExit(exitKey: string) { const {roomId, hash} = Room.getIdFromIdentifier(exitKey, this.MapUrlFile, this.instance); if (!roomId) throw new Error('Could not find the room from its exit key: '+exitKey); diff --git a/front/src/WebRtc/HtmlUtils.ts b/front/src/WebRtc/HtmlUtils.ts index db5c3fc6..86f38216 100644 --- a/front/src/WebRtc/HtmlUtils.ts +++ b/front/src/WebRtc/HtmlUtils.ts @@ -24,7 +24,7 @@ export class HtmlUtils { throw new Error("Cannot find HTML element with id '"+id+"'"); } - private static escapeHtml(html: string): string { + public static escapeHtml(html: string): string { const text = document.createTextNode(html); const p = document.createElement('p'); p.appendChild(text); diff --git a/front/src/iframe_api.ts b/front/src/iframe_api.ts index 7d334f85..c5e5e604 100644 --- a/front/src/iframe_api.ts +++ b/front/src/iframe_api.ts @@ -3,12 +3,14 @@ import {isIframeEventWrapper} from "./Api/Events/IframeEvent"; import {isUserInputChatEvent, UserInputChatEvent} from "./Api/Events/UserInputChatEvent"; import {Subject} from "rxjs"; import {EnterLeaveEvent, isEnterLeaveEvent} from "./Api/Events/EnterLeaveEvent"; +import {OpenPopupEvent} from "./Api/Events/OpenPopupEvent"; interface WorkAdventureApi { sendChatMessage(message: string, author: string): void; onChatMessage(callback: (message: string) => void): void; onEnterZone(name: string, callback: () => void): void; onLeaveZone(name: string, callback: () => void): void; + openPopup(targetObject: string, message: string, buttons: ButtonDescriptor[]): number; } declare global { @@ -21,6 +23,25 @@ type ChatMessageCallback = (message: string) => void; const userInputChatStream: Subject = new Subject(); const enterStreams: Map> = new Map>(); const leaveStreams: Map> = new Map>(); +let popupId = 0; +interface ButtonDescriptor { + /** + * The label of the button + */ + label: string, + /** + * The type of the button. Can be one of "normal", "primary", "success", "warning", "error", "disabled" + */ + className?: "normal"|"primary"|"success"|"warning"|"error"|"disabled", + /** + * Callback called if the button is pressed + */ + callback?: () => void, + /** + * If set to true, the popup is closed when the button is clicked + */ + closeOnClick?: boolean +} window.WA = { @@ -37,6 +58,25 @@ window.WA = { } as ChatEvent }, '*'); }, + openPopup(targetObject: string, message: string, buttons: ButtonDescriptor[]): number { + popupId++; + window.parent.postMessage({ + 'type': 'openPopup', + 'data': { + popupId, + targetObject, + message, + buttons: buttons.map((button) => { + return { + label: button.label, + className: button.className, + closeOnClick: button.closeOnClick + }; + }) + } as OpenPopupEvent + }, '*'); + return popupId; + }, /** * Listen to messages sent by the local user, in the chat. */ diff --git a/maps/tests/script.js b/maps/tests/script.js index 53d9ccb5..5d5f3c62 100644 --- a/maps/tests/script.js +++ b/maps/tests/script.js @@ -18,3 +18,26 @@ WA.onLeaveZone('myTrigger', () => { WA.onEnterZone('notExist', () => { WA.sendChatMessage("YOU SHOULD NEVER SEE THIS", 'Poly Parrot'); }) + +let popupId; + +WA.onEnterZone('popupZone', () => { + popupId = WA.openPopup('foobar', 'This is a test message. Hi!', [ + { + label: "Close", + className: "normal", + closeOnClick: true + }, + { + label: "Next", + className: "success", + callback: () => { + console.log('BUTTON CLICKED') + } + } + ]) +}) + +/*WA.onLeaveZone('popupZone', () => { + WA.sendChatMessage("Thanks!", 'Poly Parrot'); +})*/ diff --git a/maps/tests/script_api.json b/maps/tests/script_api.json index ff1b0898..6b6c91ad 100644 --- a/maps/tests/script_api.json +++ b/maps/tests/script_api.json @@ -20,7 +20,7 @@ "width":10, "x":0, "y":0 - }, + }, { "data":[0, 0, 0, 0, 0, 0, 0, 0, 23, 23, 0, 0, 0, 0, 0, 0, 0, 0, 23, 23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "height":10, @@ -38,7 +38,25 @@ "width":10, "x":0, "y":0 - }, + }, + { + "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23, 23, 0, 0, 0, 0, 0, 0, 0, 0, 23, 23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":10, + "id":7, + "name":"popupZone", + "opacity":1, + "properties":[ + { + "name":"zone", + "type":"string", + "value":"popupZone" + }], + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, { "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "height":10, @@ -50,20 +68,31 @@ "width":10, "x":0, "y":0 - }, + }, { "draworder":"topdown", "id":3, "name":"floorLayer", - "objects":[], + "objects":[ + { + "height":120.377012261239, + "id":1, + "name":"myPopup", + "rotation":0, + "type":"", + "visible":true, + "width":162.815914588373, + "x":77.4042872633247, + "y":61.1226958044874 + }], "opacity":1, "type":"objectgroup", "visible":true, "x":0, "y":0 }], - "nextlayerid":7, - "nextobjectid":1, + "nextlayerid":8, + "nextobjectid":2, "orientation":"orthogonal", "properties":[ { @@ -92,4 +121,4 @@ "type":"map", "version":1.2, "width":10 -} +} \ No newline at end of file