diff --git a/front/src/Api/Events/IframeEvent.ts b/front/src/Api/Events/IframeEvent.ts index e267fe90..8383cfbd 100644 --- a/front/src/Api/Events/IframeEvent.ts +++ b/front/src/Api/Events/IframeEvent.ts @@ -5,6 +5,8 @@ import type { ChatEvent } from './ChatEvent'; import type { ClosePopupEvent } from './ClosePopupEvent'; import type { EnterLeaveEvent } from './EnterLeaveEvent'; import type { GoToPageEvent } from './GoToPageEvent'; +import type { MenuItemClickedEvent } from './MenuItemClickedEvent'; +import type { MenuItemRegisterEvent } from './MenuItemRegisterEvent'; import type { HasPlayerMovedEvent } from './HasPlayerMovedEvent'; import type { OpenCoWebSiteEvent } from './OpenCoWebSiteEvent'; import type { OpenPopupEvent } from './OpenPopupEvent'; @@ -21,6 +23,7 @@ export interface TypedMessageEvent extends MessageEvent { export type IframeEventMap = { getState: GameStateEvent, // updateTile: UpdateTileEvent + registerMenuCommand: MenuItemRegisterEvent chat: ChatEvent, openPopup: OpenPopupEvent closePopup: ClosePopupEvent @@ -56,6 +59,7 @@ export interface IframeResponseEventMap { gameState: GameStateEvent hasPlayerMoved: HasPlayerMovedEvent dataLayer: DataLayerEvent + menuItemClicked: MenuItemClickedEvent } export interface IframeResponseEvent { type: T; diff --git a/front/src/Api/Events/MenuItemClickedEvent.ts b/front/src/Api/Events/MenuItemClickedEvent.ts new file mode 100644 index 00000000..0735eda4 --- /dev/null +++ b/front/src/Api/Events/MenuItemClickedEvent.ts @@ -0,0 +1,10 @@ +import * as tg from "generic-type-guard"; + +export const isMenuItemClickedEvent = + new tg.IsInterface().withProperties({ + menuItem: tg.isString + }).get(); +/** + * A message sent from the game to the iFrame when a menu item is clicked. + */ +export type MenuItemClickedEvent = tg.GuardedType; diff --git a/front/src/Api/Events/MenuItemRegisterEvent.ts b/front/src/Api/Events/MenuItemRegisterEvent.ts new file mode 100644 index 00000000..a25e5cc3 --- /dev/null +++ b/front/src/Api/Events/MenuItemRegisterEvent.ts @@ -0,0 +1,10 @@ +import * as tg from "generic-type-guard"; + +export const isMenuItemRegisterEvent = + new tg.IsInterface().withProperties({ + menutItem: tg.isString + }).get(); +/** + * A message sent from the iFrame to the game to add a new menu item. + */ +export type MenuItemRegisterEvent = tg.GuardedType; diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index 600ff0a6..07246333 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -17,7 +17,8 @@ import type { GameStateEvent } from './Events/GameStateEvent'; import type { HasPlayerMovedEvent } from './Events/HasPlayerMovedEvent'; import { Math } from 'phaser'; import type { DataLayerEvent } from "./Events/DataLayerEvent"; - +import { isMenuItemRegisterEvent } from './Events/MenuItemRegisterEvent'; +import type { MenuItemClickedEvent } from './Events/MenuItemClickedEvent'; /** @@ -74,6 +75,8 @@ class IframeListener { private readonly _dataLayerChangeStream: Subject = new Subject(); public readonly dataLayerChangeStream = this._dataLayerChangeStream.asObservable(); + private readonly _registerMenuCommandStream: Subject = new Subject(); + public readonly registerMenuCommandStream = this._registerMenuCommandStream.asObservable(); private readonly iframes = new Set(); private readonly scripts = new Map(); private sendPlayerMove: boolean = false; @@ -140,6 +143,8 @@ class IframeListener { this.sendPlayerMove = true } else if (payload.type == "getDataLayer") { this._dataLayerChangeStream.next(); + } else if (payload.type == "registerMenuCommand" && isMenuItemRegisterEvent(payload.data)) { + this._registerMenuCommandStream.next(payload.data.menutItem) } } }, false); @@ -237,6 +242,15 @@ class IframeListener { this.scripts.delete(scriptUrl); } + sendMenuClickedEvent(menuItem: string) { + this.postMessage({ + 'type': 'menuItemClicked', + 'data': { + menuItem: menuItem, + } as MenuItemClickedEvent + }); + } + sendUserInputChat(message: string) { this.postMessage({ 'type': 'userInputChat', diff --git a/front/src/Phaser/Game/GameMap.ts b/front/src/Phaser/Game/GameMap.ts index f95bfa0f..24ca60c7 100644 --- a/front/src/Phaser/Game/GameMap.ts +++ b/front/src/Phaser/Game/GameMap.ts @@ -137,8 +137,8 @@ export class GameMap { public findPhaserLayer(layerName: string): TilemapLayer | undefined { let i = 0; let found = false; - while (!found && i{ + console.log('show'); this.setLayerVisibility(layerEvent.name, true); })); this.iframeSubscriptionList.push(iframeListener.hideLayerStream.subscribe((layerEvent)=>{ + console.log('hide'); this.setLayerVisibility(layerEvent.name, false); })); @@ -917,16 +921,12 @@ ${escapedMessage} } private setLayerVisibility(layerName: string, visible: boolean): void { - const phaserlayer = this.gameMap.findPhaserLayer(layerName); - if (phaserlayer === undefined) { + const phaserLayer = this.gameMap.findPhaserLayer(layerName); + if (phaserLayer === undefined) { console.warn('Could not find layer "' + layerName + '" when calling WA.hideLayer / WA.showLayer'); return; } - if(phaserlayer.type != "tilelayer"){ - console.warn('The layer "' + layerName + '" is not a tilelayer. It can not be show/hide'); - return; - } - phaserlayer.setVisible(visible); + phaserLayer.setVisible(visible); this.dirty = true; } @@ -941,6 +941,8 @@ ${escapedMessage} 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); urlManager.pushStartLayerNameToUrl(hash); + const menuScene: MenuScene = this.scene.get(MenuSceneName) as MenuScene + menuScene.reset() if (roomId !== this.scene.key) { if (this.scene.get(roomId) === null) { console.error("next room not loaded", exitKey); diff --git a/front/src/Phaser/Menu/MenuScene.ts b/front/src/Phaser/Menu/MenuScene.ts index 76bf520f..c6c59476 100644 --- a/front/src/Phaser/Menu/MenuScene.ts +++ b/front/src/Phaser/Menu/MenuScene.ts @@ -10,6 +10,9 @@ import {GameConnexionTypes} from "../../Url/UrlManager"; import {WarningContainer, warningContainerHtml, warningContainerKey} from "../Components/WarningContainer"; import {worldFullWarningStream} from "../../Connexion/WorldFullWarningStream"; import {menuIconVisible} from "../../Stores/MenuStore"; +import { HtmlUtils } from '../../WebRtc/HtmlUtils'; +import { iframeListener } from '../../Api/IframeListener'; +import { Subscription } from 'rxjs'; export const MenuSceneName = 'MenuScene'; const gameMenuKey = 'gameMenu'; @@ -36,12 +39,17 @@ export class MenuScene extends Phaser.Scene { private menuButton!: Phaser.GameObjects.DOMElement; private warningContainer: WarningContainer | null = null; private warningContainerTimeout: NodeJS.Timeout | null = null; - + private subscriptions = new Subscription() constructor() { super({key: MenuSceneName}); this.gameQualityValue = localUserStore.getGameQualityValue(); this.videoQualityValue = localUserStore.getVideoQualityValue(); + + this.subscriptions.add(iframeListener.registerMenuCommandStream.subscribe(menuCommand => { + this.addMenuOption(menuCommand); + + })) } preload () { @@ -53,6 +61,13 @@ export class MenuScene extends Phaser.Scene { this.load.html(warningContainerKey, warningContainerHtml); } + reset() { + const addedMenuItems=[...this.menuElement.node.querySelectorAll(".fromApi")]; + for(let index=addedMenuItems.length-1;index>=0;index--){ + addedMenuItems[index].remove() + } + } + create() { menuIconVisible.set(true); this.menuElement = this.add.dom(closedSideMenuX, 30).createFromCache(gameMenuKey); @@ -268,13 +283,30 @@ export class MenuScene extends Phaser.Scene { }); } - private onMenuClick(event:MouseEvent) { - if((event?.target as HTMLInputElement).classList.contains('not-button')){ + public addMenuOption(menuText: string) { + const wrappingSection = document.createElement("section") + const excapedHtml = HtmlUtils.escapeHtml(menuText); + wrappingSection.innerHTML = `` + const menuItemContainer = this.menuElement.node.querySelector("#gameMenu main"); + if (menuItemContainer) { + menuItemContainer.querySelector(`#${excapedHtml}.fromApi`)?.remove() + menuItemContainer.insertBefore(wrappingSection, menuItemContainer.querySelector("#socialLinks")) + } + } + + private onMenuClick(event: MouseEvent) { + const htmlMenuItem = (event?.target as HTMLInputElement); + if (htmlMenuItem.classList.contains('not-button')) { return; } event.preventDefault(); - switch ((event?.target as HTMLInputElement).id) { + if (htmlMenuItem.classList.contains("fromApi")) { + iframeListener.sendMenuClickedEvent(htmlMenuItem.id) + return + } + + switch (htmlMenuItem.id) { case 'changeNameButton': this.closeSideMenu(); gameManager.leaveGame(this, LoginSceneName, new LoginScene()); diff --git a/front/src/iframe_api.ts b/front/src/iframe_api.ts index 4fdb0a03..00977157 100644 --- a/front/src/iframe_api.ts +++ b/front/src/iframe_api.ts @@ -14,7 +14,9 @@ import type { SetPropertyEvent } from "./Api/Events/setPropertyEvent"; import { GameStateEvent, isGameStateEvent } from './Api/Events/GameStateEvent'; import { HasPlayerMovedEvent, HasPlayerMovedEventCallback, isHasPlayerMovedEvent } from './Api/Events/HasPlayerMovedEvent'; import { DataLayerEvent, isDataLayerEvent } from "./Api/Events/DataLayerEvent"; -import type {ITiledMap} from "./Phaser/Map/ITiledMap"; +import type { ITiledMap } from "./Phaser/Map/ITiledMap"; +import type { MenuItemRegisterEvent } from "./Api/Events/MenuItemRegisterEvent"; +import { isMenuItemClickedEvent } from "./Api/Events/MenuItemClickedEvent"; interface WorkAdventureApi { sendChatMessage(message: string, author: string): void; @@ -37,6 +39,7 @@ interface WorkAdventureApi { restorePlayerControls(): void; displayBubble(): void; removeBubble(): void; + registerMenuCommand(commandDescriptor: string, callback: (commandDescriptor: string) => void): void getMapUrl(): Promise; getUuid(): Promise; getRoomId(): Promise; @@ -62,7 +65,7 @@ const enterStreams: Map> = new Map> = new Map>(); const popups: Map = new Map(); const popupCallbacks: Map> = new Map>(); - +const menuCallbacks: Map void> = new Map() let popupId = 0; interface ButtonDescriptor { /** @@ -308,6 +311,16 @@ window.WA = { popups.set(popupId, popup) return popup; }, + + registerMenuCommand(commandDescriptor: string, callback: (commandDescriptor: string) => void) { + menuCallbacks.set(commandDescriptor, callback); + window.parent.postMessage({ + 'type': 'registerMenuCommand', + 'data': { + menutItem: commandDescriptor + } as MenuItemRegisterEvent + }, '*'); + }, /** * Listen to messages sent by the local user, in the chat. */ @@ -371,8 +384,12 @@ window.addEventListener('message', message => { dataLayerResolver.forEach(resolver => { resolver(payloadData); }) + } else if (payload.type == "menuItemClicked" && isMenuItemClickedEvent(payload.data)) { + const callback = menuCallbacks.get(payload.data.menuItem); + if (callback) { + callback(payload.data.menuItem) + } } - } // ... diff --git a/maps/tests/iframe.html b/maps/tests/iframe.html index f9f43f20..b46b8c32 100644 --- a/maps/tests/iframe.html +++ b/maps/tests/iframe.html @@ -7,16 +7,22 @@
@@ -30,8 +36,6 @@ WA.hideLayer('Metadata'); } } - -