From 5472d220ba10e0bb6245e65c526d3ebe947a171b Mon Sep 17 00:00:00 2001 From: jonny Date: Wed, 23 Jun 2021 17:32:32 +0200 Subject: [PATCH] added trigger message code --- docs/maps/api-ui.md | 22 ++++++- front/src/Api/Events/IframeEvent.ts | 5 ++ .../src/Api/Events/ui/TriggerMessageEvent.ts | 21 ++++++ .../Events/ui/TriggerMessageEventHandler.ts | 42 ++++++++++++ front/src/Api/IframeListener.ts | 53 ++++++++------- front/src/Api/iframe/Ui/TriggerMessage.ts | 51 ++++++++++++++ front/src/Api/iframe/ui.ts | 66 ++++++++++--------- front/src/Phaser/Game/GameScene.ts | 46 ++++++++----- maps/tests/script.js | 60 +++++++++-------- 9 files changed, 267 insertions(+), 99 deletions(-) create mode 100644 front/src/Api/Events/ui/TriggerMessageEvent.ts create mode 100644 front/src/Api/Events/ui/TriggerMessageEventHandler.ts create mode 100644 front/src/Api/iframe/Ui/TriggerMessage.ts diff --git a/docs/maps/api-ui.md b/docs/maps/api-ui.md index 286f2ac7..b1d244da 100644 --- a/docs/maps/api-ui.md +++ b/docs/maps/api-ui.md @@ -86,4 +86,24 @@ WA.ui.registerMenuCommand("test", () => {
-
\ No newline at end of file + + + + +### Awaiting User Confirmation (with space bar) + +```typescript +triggerMessage(message: string): TriggerMessage +``` + +Displays a message at the bottom of the screen (that will disappear when space bar is pressed). + +Example: + +```javascript +const triggerMessage = WA.ui.triggerMessage("press 'space' to confirm"); +setTimeout(()=>{ + // later + triggerMessage.remove(); +},1000) +``` \ No newline at end of file diff --git a/front/src/Api/Events/IframeEvent.ts b/front/src/Api/Events/IframeEvent.ts index 7325f811..d2df4ded 100644 --- a/front/src/Api/Events/IframeEvent.ts +++ b/front/src/Api/Events/IframeEvent.ts @@ -18,6 +18,7 @@ import type { PlaySoundEvent } from "./PlaySoundEvent"; import type { MenuItemClickedEvent } from "./ui/MenuItemClickedEvent"; import type { MenuItemRegisterEvent } from './ui/MenuItemRegisterEvent'; import type { HasPlayerMovedEvent } from "./HasPlayerMovedEvent"; +import type { MessageReferenceEvent, TriggerMessageEvent } from '../iframe/TriggerMessageEvent'; export interface TypedMessageEvent extends MessageEvent { @@ -49,6 +50,9 @@ export type IframeEventMap = { stopSound: null, getState: undefined, registerMenuCommand: MenuItemRegisterEvent + + triggerMessage: TriggerMessageEvent + removeTriggerMessage: MessageReferenceEvent } export interface IframeEvent { type: T; @@ -68,6 +72,7 @@ export interface IframeResponseEventMap { hasPlayerMoved: HasPlayerMovedEvent dataLayer: DataLayerEvent menuItemClicked: MenuItemClickedEvent + messageTriggered: MessageReferenceEvent } export interface IframeResponseEvent { type: T; diff --git a/front/src/Api/Events/ui/TriggerMessageEvent.ts b/front/src/Api/Events/ui/TriggerMessageEvent.ts new file mode 100644 index 00000000..5b42b02e --- /dev/null +++ b/front/src/Api/Events/ui/TriggerMessageEvent.ts @@ -0,0 +1,21 @@ +import * as tg from "generic-type-guard"; + +export const triggerMessage = "triggerMessage" +export const removeTriggerMessage = "removeTriggerMessage" + +export const isTriggerMessageEvent = new tg.IsInterface().withProperties({ + message: tg.isString, + uuid: tg.isString +}).get() + + +export type TriggerMessageEvent = tg.GuardedType; + + +export const isMessageReferenceEvent = + new tg.IsInterface().withProperties({ + uuid: tg.isString + }).get(); + + +export type MessageReferenceEvent = tg.GuardedType; diff --git a/front/src/Api/Events/ui/TriggerMessageEventHandler.ts b/front/src/Api/Events/ui/TriggerMessageEventHandler.ts new file mode 100644 index 00000000..d690dbc0 --- /dev/null +++ b/front/src/Api/Events/ui/TriggerMessageEventHandler.ts @@ -0,0 +1,42 @@ +import { Subject } from 'rxjs'; +import { iframeListener } from '../../IframeListener'; +import { isMessageReferenceEvent, isTriggerMessageEvent, MessageReferenceEvent, removeTriggerMessage, triggerMessage, TriggerMessageEvent } from './TriggerMessageEvent'; +import * as tg from "generic-type-guard"; +export function sendMessageTriggeredEvent(uuid: string) { + iframeListener.postMessage({ + 'type': 'messageTriggered', + 'data': { + uuid, + } as MessageReferenceEvent + }); +} + +const _triggerMessageEvent: Subject = new Subject(); +const _removeTriggerMessageEvent: Subject = new Subject(); + +export const triggerMessageEvent = _triggerMessageEvent.asObservable(); + +export const removeTriggerMessageEvent = _removeTriggerMessageEvent.asObservable(); + +const isTriggerMessageEventObject = new tg.IsInterface().withProperties({ + type: tg.isSingletonString(triggerMessage), + data: isTriggerMessageEvent +}).get() +const isTriggerMessageRemoveEventObject = new tg.IsInterface().withProperties({ + type: tg.isSingletonString(removeTriggerMessage), + data: isMessageReferenceEvent +}).get() + + +export const isTriggerMessageHandlerEvent = tg.isUnion(isTriggerMessageEventObject, isTriggerMessageRemoveEventObject) + + + + +export function triggerMessageEventHandler(event: tg.GuardedType) { + if (isTriggerMessageEventObject(event)) { + _triggerMessageEvent.next(event.data) + } else if (isTriggerMessageRemoveEventObject(event)) { + _removeTriggerMessageEvent.next(event.data) + } +} \ No newline at end of file diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index 9311d7b6..3320519a 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -1,14 +1,14 @@ -import {Subject} from "rxjs"; -import {ChatEvent, isChatEvent} from "./Events/ChatEvent"; -import {HtmlUtils} from "../WebRtc/HtmlUtils"; -import type {EnterLeaveEvent} from "./Events/EnterLeaveEvent"; -import {isOpenPopupEvent, OpenPopupEvent} from "./Events/OpenPopupEvent"; -import {isOpenTabEvent, OpenTabEvent} from "./Events/OpenTabEvent"; -import type {ButtonClickedEvent} from "./Events/ButtonClickedEvent"; -import {ClosePopupEvent, isClosePopupEvent} from "./Events/ClosePopupEvent"; -import {scriptUtils} from "./ScriptUtils"; -import {GoToPageEvent, isGoToPageEvent} from "./Events/GoToPageEvent"; -import {isOpenCoWebsite, OpenCoWebSiteEvent} from "./Events/OpenCoWebSiteEvent"; +import { Subject } from "rxjs"; +import { ChatEvent, isChatEvent } from "./Events/ChatEvent"; +import { HtmlUtils } from "../WebRtc/HtmlUtils"; +import type { EnterLeaveEvent } from "./Events/EnterLeaveEvent"; +import { isOpenPopupEvent, OpenPopupEvent } from "./Events/OpenPopupEvent"; +import { isOpenTabEvent, OpenTabEvent } from "./Events/OpenTabEvent"; +import type { ButtonClickedEvent } from "./Events/ButtonClickedEvent"; +import { ClosePopupEvent, isClosePopupEvent } from "./Events/ClosePopupEvent"; +import { scriptUtils } from "./ScriptUtils"; +import { GoToPageEvent, isGoToPageEvent } from "./Events/GoToPageEvent"; +import { isOpenCoWebsite, OpenCoWebSiteEvent } from "./Events/OpenCoWebSiteEvent"; import { IframeEvent, IframeEventMap, @@ -17,19 +17,20 @@ import { isIframeEventWrapper, TypedMessageEvent } from "./Events/IframeEvent"; -import type {UserInputChatEvent} from "./Events/UserInputChatEvent"; +import type { UserInputChatEvent } from "./Events/UserInputChatEvent"; //import { isLoadPageEvent } from './Events/LoadPageEvent'; -import {isPlaySoundEvent, PlaySoundEvent} from "./Events/PlaySoundEvent"; -import {isStopSoundEvent, StopSoundEvent} from "./Events/StopSoundEvent"; -import {isLoadSoundEvent, LoadSoundEvent} from "./Events/LoadSoundEvent"; -import {isSetPropertyEvent, SetPropertyEvent} from "./Events/setPropertyEvent"; -import {isLayerEvent, LayerEvent} from "./Events/LayerEvent"; -import {isMenuItemRegisterEvent,} from "./Events/ui/MenuItemRegisterEvent"; -import type {DataLayerEvent} from "./Events/DataLayerEvent"; -import type {GameStateEvent} from "./Events/GameStateEvent"; -import type {HasPlayerMovedEvent} from "./Events/HasPlayerMovedEvent"; -import {isLoadPageEvent} from "./Events/LoadPageEvent"; -import {handleMenuItemRegistrationEvent, isMenuItemRegisterIframeEvent} from "./Events/ui/MenuItemRegisterEvent"; +import { isPlaySoundEvent, PlaySoundEvent } from "./Events/PlaySoundEvent"; +import { isStopSoundEvent, StopSoundEvent } from "./Events/StopSoundEvent"; +import { isLoadSoundEvent, LoadSoundEvent } from "./Events/LoadSoundEvent"; +import { isSetPropertyEvent, SetPropertyEvent } from "./Events/setPropertyEvent"; +import { isLayerEvent, LayerEvent } from "./Events/LayerEvent"; +import { isMenuItemRegisterEvent, } from "./Events/ui/MenuItemRegisterEvent"; +import type { DataLayerEvent } from "./Events/DataLayerEvent"; +import type { GameStateEvent } from "./Events/GameStateEvent"; +import type { HasPlayerMovedEvent } from "./Events/HasPlayerMovedEvent"; +import { isLoadPageEvent } from "./Events/LoadPageEvent"; +import { handleMenuItemRegistrationEvent, isMenuItemRegisterIframeEvent } from "./Events/ui/MenuItemRegisterEvent"; +import { isTriggerMessageHandlerEvent, triggerMessageEventHandler } from './Events/ui/TriggerMessageEventHandler'; /** * Listens to messages from iframes and turn those messages into easy to use observables. @@ -190,6 +191,8 @@ class IframeListener { this._unregisterMenuCommandStream.next(data); }) handleMenuItemRegistrationEvent(payload.data) + } else if (isTriggerMessageHandlerEvent(payload)) { + triggerMessageEventHandler(payload) } } }, false); @@ -198,8 +201,8 @@ class IframeListener { sendDataLayerEvent(dataLayerEvent: DataLayerEvent) { this.postMessage({ - 'type' : 'dataLayer', - 'data' : dataLayerEvent + 'type': 'dataLayer', + 'data': dataLayerEvent }) } diff --git a/front/src/Api/iframe/Ui/TriggerMessage.ts b/front/src/Api/iframe/Ui/TriggerMessage.ts new file mode 100644 index 00000000..af0e20ce --- /dev/null +++ b/front/src/Api/iframe/Ui/TriggerMessage.ts @@ -0,0 +1,51 @@ + +import { removeTriggerMessage, triggerMessage, TriggerMessageEvent } from '../../Events/ui/TriggerMessageEvent'; +import { sendToWorkadventure } from '../IframeApiContribution'; +function uuidv4() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { + const r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); +} + +export let triggerMessageInstance: TriggerMessage | undefined = undefined + + + +export class TriggerMessage { + uuid: string + + constructor(private message: string, private callback: () => void) { + this.uuid = uuidv4() + if (triggerMessageInstance) { + triggerMessageInstance.remove(); + } + triggerMessageInstance = this; + this.create(); + } + + create(): this { + sendToWorkadventure({ + type: triggerMessage, + data: { + message: this.message, + uuid: this.uuid + } as TriggerMessageEvent + }) + return this + } + + remove() { + sendToWorkadventure({ + type: removeTriggerMessage, + data: { + uuid: this.uuid + } as TriggerMessageEvent + }) + triggerMessageInstance = undefined + } + + trigger() { + this.callback(); + } +} \ No newline at end of file diff --git a/front/src/Api/iframe/ui.ts b/front/src/Api/iframe/ui.ts index c7655b84..834cc347 100644 --- a/front/src/Api/iframe/ui.ts +++ b/front/src/Api/iframe/ui.ts @@ -1,10 +1,11 @@ import { isButtonClickedEvent } from '../Events/ButtonClickedEvent'; import { isMenuItemClickedEvent } from '../Events/ui/MenuItemClickedEvent'; -import type { MenuItemRegisterEvent } from '../Events/ui/MenuItemRegisterEvent'; +import { isMessageReferenceEvent } from '../Events/ui/TriggerMessageEvent'; import { IframeApiContribution, sendToWorkadventure } from './IframeApiContribution'; import { apiCallback } from "./registeredCallbacks"; import type { ButtonClickedCallback, ButtonDescriptor } from "./Ui/ButtonDescriptor"; import { Popup } from "./Ui/Popup"; +import { TriggerMessage, triggerMessageInstance } from './Ui/TriggerMessage'; let popupId = 0; const popups: Map = new Map(); @@ -12,41 +13,41 @@ const popupCallbacks: Map> = new Map< const menuCallbacks: Map void> = new Map() -interface ZonedPopupOptions { - zone: string - objectLayerName?: string, - popupText: string, - delay?: number - popupOptions: Array -} - - class WorkAdventureUiCommands extends IframeApiContribution { - callbacks = [apiCallback({ - type: "buttonClickedEvent", - typeChecker: isButtonClickedEvent, - callback: (payloadData) => { - const callback = popupCallbacks.get(payloadData.popupId)?.get(payloadData.buttonId); - const popup = popups.get(payloadData.popupId); - if (popup === undefined) { - throw new Error('Could not find popup with ID "' + payloadData.popupId + '"'); + callbacks = [ + apiCallback({ + type: "buttonClickedEvent", + typeChecker: isButtonClickedEvent, + callback: (payloadData) => { + const callback = popupCallbacks.get(payloadData.popupId)?.get(payloadData.buttonId); + const popup = popups.get(payloadData.popupId); + if (popup === undefined) { + throw new Error('Could not find popup with ID "' + payloadData.popupId + '"'); + } + if (callback) { + callback(popup); + } } - if (callback) { - callback(popup); + }), + apiCallback({ + type: "menuItemClicked", + typeChecker: isMenuItemClickedEvent, + callback: event => { + const callback = menuCallbacks.get(event.menuItem); + if (callback) { + callback(event.menuItem) + } } - } - }), - apiCallback({ - type: "menuItemClicked", - typeChecker: isMenuItemClickedEvent, - callback: event => { - const callback = menuCallbacks.get(event.menuItem); - if (callback) { - callback(event.menuItem) + }), + apiCallback({ + type: "messageTriggered", + typeChecker: isMessageReferenceEvent, + callback: event => { + triggerMessageInstance?.trigger(); } - } - })]; + }) + ]; openPopup(targetObject: string, message: string, buttons: ButtonDescriptor[]): Popup { @@ -101,6 +102,9 @@ class WorkAdventureUiCommands extends IframeApiContribution void): TriggerMessage { + return new TriggerMessage(message, callback); + } } export default new WorkAdventureUiCommands(); diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 7c07f187..2cb4a363 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -64,7 +64,8 @@ import type { ITiledMapLayerProperty, ITiledMapObject, ITiledMapTileLayer, - ITiledTileSet } from "../Map/ITiledMap"; + ITiledTileSet +} from "../Map/ITiledMap"; import { MenuScene, MenuSceneName } from '../Menu/MenuScene'; import { PlayerAnimationDirections } from "../Player/Animation"; import { hasMovedEventName, Player, requestEmoteEventName } from "../Player/Player"; @@ -93,7 +94,8 @@ import Tilemap = Phaser.Tilemaps.Tilemap; import type { HasPlayerMovedEvent } from '../../Api/Events/HasPlayerMovedEvent'; import AnimatedTiles from "phaser-animated-tiles"; -import {soundManager} from "./SoundManager"; +import { soundManager } from "./SoundManager"; +import { removeTriggerMessageEvent, sendMessageTriggeredEvent, triggerMessageEvent } from '../../Api/Events/ui/TriggerMessageEventHandler'; export interface GameSceneInitInterface { initPosition: PointInterface | null, @@ -932,11 +934,11 @@ ${escapedMessage} scriptedBubbleSprite.destroy(); })); - this.iframeSubscriptionList.push(iframeListener.showLayerStream.subscribe((layerEvent)=>{ + this.iframeSubscriptionList.push(iframeListener.showLayerStream.subscribe((layerEvent) => { this.setLayerVisibility(layerEvent.name, true); })); - this.iframeSubscriptionList.push(iframeListener.hideLayerStream.subscribe((layerEvent)=>{ + this.iframeSubscriptionList.push(iframeListener.hideLayerStream.subscribe((layerEvent) => { this.setLayerVisibility(layerEvent.name, false); })); @@ -945,7 +947,7 @@ ${escapedMessage} })); this.iframeSubscriptionList.push(iframeListener.dataLayerChangeStream.subscribe(() => { - iframeListener.sendDataLayerEvent({data: this.gameMap.getMap()}); + iframeListener.sendDataLayerEvent({ data: this.gameMap.getMap() }); })) this.iframeSubscriptionList.push(iframeListener.gameStateStream.subscribe(() => { @@ -959,21 +961,33 @@ ${escapedMessage} }) })); + + this.iframeSubscriptionList.push(triggerMessageEvent.subscribe(message => { + layoutManager.addActionButton(message.uuid, message.message, () => { + sendMessageTriggeredEvent(message.uuid) + layoutManager.removeActionButton(message.uuid, this.userInputManager); + }, this.userInputManager); + })) + + this.iframeSubscriptionList.push(removeTriggerMessageEvent.subscribe(message => { + layoutManager.removeActionButton(message.uuid, this.userInputManager); + })) + } private setPropertyLayer(layerName: string, propertyName: string, propertyValue: string | number | boolean | undefined): void { const layer = this.gameMap.findLayer(layerName); - if (layer === undefined) { + if (layer === undefined) { console.warn('Could not find layer "' + layerName + '" when calling setProperty'); return; } - const property = (layer.properties as ITiledMapLayerProperty[])?.find((property) => property.name === propertyName); - if (property === undefined) { - layer.properties = []; - layer.properties.push({name : propertyName, type : typeof propertyValue, value : propertyValue}); - return; - } - property.value = propertyValue; + const property = (layer.properties as ITiledMapLayerProperty[])?.find((property) => property.name === propertyName); + if (property === undefined) { + layer.properties = []; + layer.properties.push({ name: propertyName, type: typeof propertyValue, value: propertyValue }); + return; + } + property.value = propertyValue; } private setLayerVisibility(layerName: string, visible: boolean): void { @@ -1150,7 +1164,7 @@ ${escapedMessage} } //todo: push that into the gameManager - private loadNextGame(exitSceneIdentifier: string) : Promise{ + private loadNextGame(exitSceneIdentifier: string): Promise { const { roomId, hash } = Room.getIdFromIdentifier(exitSceneIdentifier, this.MapUrlFile, this.instance); const room = new Room(roomId); return gameManager.loadMap(room, this.scene).catch(() => { }); @@ -1197,7 +1211,7 @@ ${escapedMessage} this.physics.add.collider(this.CurrentPlayer, phaserLayer, (object1: GameObject, object2: GameObject) => { //this.CurrentPlayer.say("Collision with layer : "+ (object2 as Tile).layer.name) }); - phaserLayer.setCollisionByProperty({collides: true}); + phaserLayer.setCollisionByProperty({ collides: true }); if (DEBUG_MODE) { //debug code to see the collision hitbox of the object in the top layer phaserLayer.renderDebug(this.add.graphics(), { @@ -1206,7 +1220,7 @@ ${escapedMessage} faceColor: new Phaser.Display.Color(40, 39, 37, 255) // Colliding face edges }); } - //}); + //}); } } } diff --git a/maps/tests/script.js b/maps/tests/script.js index b300700f..ac3541f6 100644 --- a/maps/tests/script.js +++ b/maps/tests/script.js @@ -1,40 +1,41 @@ +/// console.log('SCRIPT LAUNCHED'); //WA.sendChatMessage('Hi, my name is Poly and I repeat what you say!', 'Poly Parrot'); var isFirstTimeTuto = false; var textFirstPopup = 'Hey ! This is how to open start a discussion with someone ! You can be 4 max in a booble'; var textSecondPopup = 'You can also use the chat to communicate ! '; -var targetObjectTutoBubble ='myPopup1'; -var targetObjectTutoChat ='myPopup2'; +var targetObjectTutoBubble = 'myPopup1'; +var targetObjectTutoChat = 'myPopup2'; var popUpExplanation = undefined; -function launchTuto (){ - WA.ui.openPopup(targetObjectTutoBubble, textFirstPopup, [ - { - label: "Next", - className: "popUpElement", - callback: (popup) => { - popup.close(); +function launchTuto() { + WA.ui.openPopup(targetObjectTutoBubble, textFirstPopup, [ + { + label: "Next", + className: "popUpElement", + callback: (popup) => { + popup.close(); - WA.ui.openPopup(targetObjectTutoChat, textSecondPopup, [ - { - label: "Open Chat", - className: "popUpElement", - callback: (popup1) => { - WA.chat.sendChatMessage("Hey you can talk here too ! ", 'WA Guide'); - popup1.close(); - WA.controls.restorePlayerControls(); - } + WA.ui.openPopup(targetObjectTutoChat, textSecondPopup, [ + { + label: "Open Chat", + className: "popUpElement", + callback: (popup1) => { + WA.chat.sendChatMessage("Hey you can talk here too ! ", 'WA Guide'); + popup1.close(); + WA.controls.restorePlayerControls(); } + } - ]) - } + ]) } - ]); - WA.controls.disablePlayerControls(); + } + ]); + WA.controls.disablePlayerControls(); } WA.chat.onChatMessage((message => { console.log('CHAT MESSAGE RECEIVED BY SCRIPT'); - WA.chat.sendChatMessage('Poly Parrot says: "'+message+'"', 'Poly Parrot'); + WA.chat.sendChatMessage('Poly Parrot says: "' + message + '"', 'Poly Parrot'); })); WA.room.onEnterZone('myTrigger', () => { @@ -50,11 +51,11 @@ WA.room.onEnterZone('notExist', () => { WA.room.onEnterZone('popupZone', () => { WA.ui.displayBubble(); - if (!isFirstTimeTuto) { + if(!isFirstTimeTuto) { isFirstTimeTuto = true; launchTuto(); } - else popUpExplanation = WA.ui.openPopup(targetObjectTutoChat,'Do you want to review the explanation ? ', [ + else popUpExplanation = WA.ui.openPopup(targetObjectTutoChat, 'Do you want to review the explanation ? ', [ { label: "No", className: "popUpElementReviewexplanation", @@ -74,6 +75,13 @@ WA.room.onEnterZone('popupZone', () => { }); WA.room.onLeaveZone('popupZone', () => { - if (popUpExplanation !== undefined) popUpExplanation.close(); + if(popUpExplanation !== undefined) popUpExplanation.close(); WA.ui.removeBubble(); }) + +const message = WA.ui.triggerMessage("testMessage", () => { + WA.chat.sendChatMessage("triggered", "triggerbot"); +}) +setTimeout(() => { + message.remove(); +}, 5000) \ No newline at end of file