Merge branch 'menu-command-api' of github.com:jonnytest1/workadventure into metadataScriptingApi

This commit is contained in:
GRL 2021-05-19 09:36:11 +02:00
commit ce0a72c6ce
9 changed files with 119 additions and 26 deletions

View File

@ -5,6 +5,8 @@ import type { ChatEvent } from './ChatEvent';
import type { ClosePopupEvent } from './ClosePopupEvent'; import type { ClosePopupEvent } from './ClosePopupEvent';
import type { EnterLeaveEvent } from './EnterLeaveEvent'; import type { EnterLeaveEvent } from './EnterLeaveEvent';
import type { GoToPageEvent } from './GoToPageEvent'; import type { GoToPageEvent } from './GoToPageEvent';
import type { MenuItemClickedEvent } from './MenuItemClickedEvent';
import type { MenuItemRegisterEvent } from './MenuItemRegisterEvent';
import type { HasPlayerMovedEvent } from './HasPlayerMovedEvent'; import type { HasPlayerMovedEvent } from './HasPlayerMovedEvent';
import type { OpenCoWebSiteEvent } from './OpenCoWebSiteEvent'; import type { OpenCoWebSiteEvent } from './OpenCoWebSiteEvent';
import type { OpenPopupEvent } from './OpenPopupEvent'; import type { OpenPopupEvent } from './OpenPopupEvent';
@ -21,6 +23,7 @@ export interface TypedMessageEvent<T> extends MessageEvent {
export type IframeEventMap = { export type IframeEventMap = {
getState: GameStateEvent, getState: GameStateEvent,
// updateTile: UpdateTileEvent // updateTile: UpdateTileEvent
registerMenuCommand: MenuItemRegisterEvent
chat: ChatEvent, chat: ChatEvent,
openPopup: OpenPopupEvent openPopup: OpenPopupEvent
closePopup: ClosePopupEvent closePopup: ClosePopupEvent
@ -56,6 +59,7 @@ export interface IframeResponseEventMap {
gameState: GameStateEvent gameState: GameStateEvent
hasPlayerMoved: HasPlayerMovedEvent hasPlayerMoved: HasPlayerMovedEvent
dataLayer: DataLayerEvent dataLayer: DataLayerEvent
menuItemClicked: MenuItemClickedEvent
} }
export interface IframeResponseEvent<T extends keyof IframeResponseEventMap> { export interface IframeResponseEvent<T extends keyof IframeResponseEventMap> {
type: T; type: T;

View File

@ -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<typeof isMenuItemClickedEvent>;

View File

@ -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<typeof isMenuItemRegisterEvent>;

View File

@ -17,7 +17,8 @@ import type { GameStateEvent } from './Events/GameStateEvent';
import type { HasPlayerMovedEvent } from './Events/HasPlayerMovedEvent'; import type { HasPlayerMovedEvent } from './Events/HasPlayerMovedEvent';
import { Math } from 'phaser'; import { Math } from 'phaser';
import type { DataLayerEvent } from "./Events/DataLayerEvent"; 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<void> = new Subject(); private readonly _dataLayerChangeStream: Subject<void> = new Subject();
public readonly dataLayerChangeStream = this._dataLayerChangeStream.asObservable(); public readonly dataLayerChangeStream = this._dataLayerChangeStream.asObservable();
private readonly _registerMenuCommandStream: Subject<string> = new Subject();
public readonly registerMenuCommandStream = this._registerMenuCommandStream.asObservable();
private readonly iframes = new Set<HTMLIFrameElement>(); private readonly iframes = new Set<HTMLIFrameElement>();
private readonly scripts = new Map<string, HTMLIFrameElement>(); private readonly scripts = new Map<string, HTMLIFrameElement>();
private sendPlayerMove: boolean = false; private sendPlayerMove: boolean = false;
@ -140,6 +143,8 @@ class IframeListener {
this.sendPlayerMove = true this.sendPlayerMove = true
} else if (payload.type == "getDataLayer") { } else if (payload.type == "getDataLayer") {
this._dataLayerChangeStream.next(); this._dataLayerChangeStream.next();
} else if (payload.type == "registerMenuCommand" && isMenuItemRegisterEvent(payload.data)) {
this._registerMenuCommandStream.next(payload.data.menutItem)
} }
} }
}, false); }, false);
@ -237,6 +242,15 @@ class IframeListener {
this.scripts.delete(scriptUrl); this.scripts.delete(scriptUrl);
} }
sendMenuClickedEvent(menuItem: string) {
this.postMessage({
'type': 'menuItemClicked',
'data': {
menuItem: menuItem,
} as MenuItemClickedEvent
});
}
sendUserInputChat(message: string) { sendUserInputChat(message: string) {
this.postMessage({ this.postMessage({
'type': 'userInputChat', 'type': 'userInputChat',

View File

@ -137,8 +137,8 @@ export class GameMap {
public findPhaserLayer(layerName: string): TilemapLayer | undefined { public findPhaserLayer(layerName: string): TilemapLayer | undefined {
let i = 0; let i = 0;
let found = false; let found = false;
while (!found && i<this.flatLayers.length) { while (!found && i<this.phaserLayers.length) {
if (this.flatLayers[i].name === layerName) { if (this.phaserLayers[i].layer.name === layerName) {
found = true; found = true;
} }
else { else {

View File

@ -90,9 +90,11 @@ import { TextUtils } from "../Components/TextUtils";
import { touchScreenManager } from "../../Touch/TouchScreenManager"; import { touchScreenManager } from "../../Touch/TouchScreenManager";
import { PinchManager } from "../UserInput/PinchManager"; import { PinchManager } from "../UserInput/PinchManager";
import { joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey } from "../Components/MobileJoystick"; import { joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey } from "../Components/MobileJoystick";
import { waScaleManager } from "../Services/WaScaleManager"; import { MenuScene, MenuSceneName } from '../Menu/MenuScene';
import {waScaleManager } from "../Services/WaScaleManager";
import type { HasPlayerMovedEvent } from '../../Api/Events/HasPlayerMovedEvent'; import type { HasPlayerMovedEvent } from '../../Api/Events/HasPlayerMovedEvent';
export interface GameSceneInitInterface { export interface GameSceneInitInterface {
initPosition: PointInterface | null, initPosition: PointInterface | null,
reconnecting: boolean reconnecting: boolean
@ -884,10 +886,12 @@ ${escapedMessage}
})); }));
this.iframeSubscriptionList.push(iframeListener.showLayerStream.subscribe((layerEvent)=>{ this.iframeSubscriptionList.push(iframeListener.showLayerStream.subscribe((layerEvent)=>{
console.log('show');
this.setLayerVisibility(layerEvent.name, true); this.setLayerVisibility(layerEvent.name, true);
})); }));
this.iframeSubscriptionList.push(iframeListener.hideLayerStream.subscribe((layerEvent)=>{ this.iframeSubscriptionList.push(iframeListener.hideLayerStream.subscribe((layerEvent)=>{
console.log('hide');
this.setLayerVisibility(layerEvent.name, false); this.setLayerVisibility(layerEvent.name, false);
})); }));
@ -917,16 +921,12 @@ ${escapedMessage}
} }
private setLayerVisibility(layerName: string, visible: boolean): void { private setLayerVisibility(layerName: string, visible: boolean): void {
const phaserlayer = this.gameMap.findPhaserLayer(layerName); const phaserLayer = this.gameMap.findPhaserLayer(layerName);
if (phaserlayer === undefined) { if (phaserLayer === undefined) {
console.warn('Could not find layer "' + layerName + '" when calling WA.hideLayer / WA.showLayer'); console.warn('Could not find layer "' + layerName + '" when calling WA.hideLayer / WA.showLayer');
return; return;
} }
if(phaserlayer.type != "tilelayer"){ phaserLayer.setVisible(visible);
console.warn('The layer "' + layerName + '" is not a tilelayer. It can not be show/hide');
return;
}
phaserlayer.setVisible(visible);
this.dirty = true; this.dirty = true;
} }
@ -941,6 +941,8 @@ ${escapedMessage}
const {roomId, hash} = Room.getIdFromIdentifier(exitKey, this.MapUrlFile, this.instance); 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); if (!roomId) throw new Error('Could not find the room from its exit key: '+exitKey);
urlManager.pushStartLayerNameToUrl(hash); urlManager.pushStartLayerNameToUrl(hash);
const menuScene: MenuScene = this.scene.get(MenuSceneName) as MenuScene
menuScene.reset()
if (roomId !== this.scene.key) { if (roomId !== this.scene.key) {
if (this.scene.get(roomId) === null) { if (this.scene.get(roomId) === null) {
console.error("next room not loaded", exitKey); console.error("next room not loaded", exitKey);

View File

@ -10,6 +10,9 @@ import {GameConnexionTypes} from "../../Url/UrlManager";
import {WarningContainer, warningContainerHtml, warningContainerKey} from "../Components/WarningContainer"; import {WarningContainer, warningContainerHtml, warningContainerKey} from "../Components/WarningContainer";
import {worldFullWarningStream} from "../../Connexion/WorldFullWarningStream"; import {worldFullWarningStream} from "../../Connexion/WorldFullWarningStream";
import {menuIconVisible} from "../../Stores/MenuStore"; import {menuIconVisible} from "../../Stores/MenuStore";
import { HtmlUtils } from '../../WebRtc/HtmlUtils';
import { iframeListener } from '../../Api/IframeListener';
import { Subscription } from 'rxjs';
export const MenuSceneName = 'MenuScene'; export const MenuSceneName = 'MenuScene';
const gameMenuKey = 'gameMenu'; const gameMenuKey = 'gameMenu';
@ -36,12 +39,17 @@ export class MenuScene extends Phaser.Scene {
private menuButton!: Phaser.GameObjects.DOMElement; private menuButton!: Phaser.GameObjects.DOMElement;
private warningContainer: WarningContainer | null = null; private warningContainer: WarningContainer | null = null;
private warningContainerTimeout: NodeJS.Timeout | null = null; private warningContainerTimeout: NodeJS.Timeout | null = null;
private subscriptions = new Subscription()
constructor() { constructor() {
super({key: MenuSceneName}); super({key: MenuSceneName});
this.gameQualityValue = localUserStore.getGameQualityValue(); this.gameQualityValue = localUserStore.getGameQualityValue();
this.videoQualityValue = localUserStore.getVideoQualityValue(); this.videoQualityValue = localUserStore.getVideoQualityValue();
this.subscriptions.add(iframeListener.registerMenuCommandStream.subscribe(menuCommand => {
this.addMenuOption(menuCommand);
}))
} }
preload () { preload () {
@ -53,6 +61,13 @@ export class MenuScene extends Phaser.Scene {
this.load.html(warningContainerKey, warningContainerHtml); 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() { create() {
menuIconVisible.set(true); menuIconVisible.set(true);
this.menuElement = this.add.dom(closedSideMenuX, 30).createFromCache(gameMenuKey); this.menuElement = this.add.dom(closedSideMenuX, 30).createFromCache(gameMenuKey);
@ -268,13 +283,30 @@ export class MenuScene extends Phaser.Scene {
}); });
} }
private onMenuClick(event:MouseEvent) { public addMenuOption(menuText: string) {
if((event?.target as HTMLInputElement).classList.contains('not-button')){ const wrappingSection = document.createElement("section")
const excapedHtml = HtmlUtils.escapeHtml(menuText);
wrappingSection.innerHTML = `<button class="fromApi" id="${excapedHtml}">${excapedHtml}</button>`
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; return;
} }
event.preventDefault(); event.preventDefault();
switch ((event?.target as HTMLInputElement).id) { if (htmlMenuItem.classList.contains("fromApi")) {
iframeListener.sendMenuClickedEvent(htmlMenuItem.id)
return
}
switch (htmlMenuItem.id) {
case 'changeNameButton': case 'changeNameButton':
this.closeSideMenu(); this.closeSideMenu();
gameManager.leaveGame(this, LoginSceneName, new LoginScene()); gameManager.leaveGame(this, LoginSceneName, new LoginScene());

View File

@ -14,7 +14,9 @@ import type { SetPropertyEvent } from "./Api/Events/setPropertyEvent";
import { GameStateEvent, isGameStateEvent } from './Api/Events/GameStateEvent'; import { GameStateEvent, isGameStateEvent } from './Api/Events/GameStateEvent';
import { HasPlayerMovedEvent, HasPlayerMovedEventCallback, isHasPlayerMovedEvent } from './Api/Events/HasPlayerMovedEvent'; import { HasPlayerMovedEvent, HasPlayerMovedEventCallback, isHasPlayerMovedEvent } from './Api/Events/HasPlayerMovedEvent';
import { DataLayerEvent, isDataLayerEvent } from "./Api/Events/DataLayerEvent"; 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 { interface WorkAdventureApi {
sendChatMessage(message: string, author: string): void; sendChatMessage(message: string, author: string): void;
@ -37,6 +39,7 @@ interface WorkAdventureApi {
restorePlayerControls(): void; restorePlayerControls(): void;
displayBubble(): void; displayBubble(): void;
removeBubble(): void; removeBubble(): void;
registerMenuCommand(commandDescriptor: string, callback: (commandDescriptor: string) => void): void
getMapUrl(): Promise<string>; getMapUrl(): Promise<string>;
getUuid(): Promise<string | undefined>; getUuid(): Promise<string | undefined>;
getRoomId(): Promise<string>; getRoomId(): Promise<string>;
@ -62,7 +65,7 @@ const enterStreams: Map<string, Subject<EnterLeaveEvent>> = new Map<string, Subj
const leaveStreams: Map<string, Subject<EnterLeaveEvent>> = new Map<string, Subject<EnterLeaveEvent>>(); const leaveStreams: Map<string, Subject<EnterLeaveEvent>> = new Map<string, Subject<EnterLeaveEvent>>();
const popups: Map<number, Popup> = new Map<number, Popup>(); const popups: Map<number, Popup> = new Map<number, Popup>();
const popupCallbacks: Map<number, Map<number, ButtonClickedCallback>> = new Map<number, Map<number, ButtonClickedCallback>>(); const popupCallbacks: Map<number, Map<number, ButtonClickedCallback>> = new Map<number, Map<number, ButtonClickedCallback>>();
const menuCallbacks: Map<string, (command: string) => void> = new Map()
let popupId = 0; let popupId = 0;
interface ButtonDescriptor { interface ButtonDescriptor {
/** /**
@ -308,6 +311,16 @@ window.WA = {
popups.set(popupId, popup) popups.set(popupId, popup)
return 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. * Listen to messages sent by the local user, in the chat.
*/ */
@ -371,8 +384,12 @@ window.addEventListener('message', message => {
dataLayerResolver.forEach(resolver => { dataLayerResolver.forEach(resolver => {
resolver(payloadData); resolver(payloadData);
}) })
} else if (payload.type == "menuItemClicked" && isMenuItemClickedEvent(payload.data)) {
const callback = menuCallbacks.get(payload.data.menuItem);
if (callback) {
callback(payload.data.menuItem)
}
} }
} }
// ... // ...

View File

@ -7,16 +7,22 @@
<button id="sendchat">Send chat message</button> <button id="sendchat">Send chat message</button>
<script> <script>
document.getElementById('sendchat').onclick = () => { document.getElementById('sendchat').onclick = () => {
WA.sendChatMessage('Hello world!', 'Mr Robot'); WA.sendChatMessage('Hello world!', 'Mr ROBOT');
} }
</script> </script>
<div id="chatSent"></div> <div id="chatSent"></div>
<script> <script>
WA.onChatMessage((message => { let chatbotEnabled=false
const chatDiv = document.createElement('p'); WA.registerMenuCommand('help', () => {
chatDiv.innerText = message; chatbotEnabled=true;
document.getElementById('chatSent').append(chatDiv); WA.sendChatMessage("Bonjour Monsieur/Madame, Comment puis-je vous aider ?", 'Mr Robot');
})); WA.onChatMessage((message => {
const chatDiv = document.createElement('p');
chatDiv.innerText = 'Vous avez tapé : ' + message;
document.getElementById('chatSent').append(chatDiv);
WA.sendChatMessage('Votre message : "' + message + '" a bien ete recu. Merci de bien vouloir patienter pendant son traitement.', 'Mr ROBOT');
}));
});
</script> </script>
<div> <div>
<label for="show/hideLayer">Metadata Layer : </label><input type="checkbox" id="show/hideLayer" name="visible" value="show" checked> <label for="show/hideLayer">Metadata Layer : </label><input type="checkbox" id="show/hideLayer" name="visible" value="show" checked>
@ -30,8 +36,6 @@
WA.hideLayer('Metadata'); WA.hideLayer('Metadata');
} }
} }
</script>
<script>
WA.setProperty('start', 'openWebsite', 'https://www.wikipedia.org/'); WA.setProperty('start', 'openWebsite', 'https://www.wikipedia.org/');
</script> </script>
</body> </body>