Merge branch 'menu-commands-apiref' of github.com:jonnytest1/workadventure into metadataScriptingApi

This commit is contained in:
GRL 2021-06-23 09:18:20 +02:00
commit 85fe92f604
13 changed files with 233 additions and 192 deletions

View file

@ -65,3 +65,26 @@ WA.room.onLeaveZone('myZone', () => {
helloWorldPopup.close();
});
```
### register additional menu entries
adds an additional Entry to the main menu , these exist until the map is unloaded
```typescript
WA.ui.registerMenuCommand(menuCommand: string, callback: (menuCommand: string) => void): void
```
Example:
```javascript
WA.ui.registerMenuCommand("test", () => {
WA.chat.sendChatMessage("test clicked", "menu cmd")
})
```
<div class="col">
<img src="./assets/menu-command.png" class="figure-img img-fluid rounded" alt="" />
</div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

View file

@ -15,7 +15,8 @@ import type { LayerEvent } from './LayerEvent';
import type { SetPropertyEvent } from "./setPropertyEvent";
import type { LoadSoundEvent } from "./LoadSoundEvent";
import type { PlaySoundEvent } from "./PlaySoundEvent";
import type { MenuItemClickedEvent } from "./MenuItemClickedEvent";
import type { MenuItemClickedEvent } from "./ui/MenuItemClickedEvent";
import type { MenuItemRegisterEvent } from './ui/MenuItemRegisterEvent';
import type { HasPlayerMovedEvent } from "./HasPlayerMovedEvent";
@ -47,7 +48,7 @@ export type IframeEventMap = {
playSound: PlaySoundEvent
stopSound: null,
getState: undefined,
registerMenuCommand: undefined
registerMenuCommand: MenuItemRegisterEvent
}
export interface IframeEvent<T extends keyof IframeEventMap> {
type: T;

View file

@ -1,10 +0,0 @@
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

@ -8,3 +8,5 @@ export const isMenuItemClickedEvent =
* 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,25 @@
import * as tg from "generic-type-guard";
import { Subject } from 'rxjs';
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>;
export const isMenuItemRegisterIframeEvent =
new tg.IsInterface().withProperties({
type: tg.isSingletonString("registerMenuCommand"),
data: isMenuItemRegisterEvent
}).get();
const _registerMenuCommandStream: Subject<string> = new Subject();
export const registerMenuCommandStream = _registerMenuCommandStream.asObservable();
export function handleMenuItemRegistrationEvent(event: MenuItemRegisterEvent) {
_registerMenuCommandStream.next(event.menutItem)
}

View file

@ -24,12 +24,12 @@ 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/MenuItemRegisterEvent";
import {isMenuItemRegisterEvent,} from "./Events/ui/MenuItemRegisterEvent";
import type {DataLayerEvent} from "./Events/DataLayerEvent";
import type {GameStateEvent} from "./Events/GameStateEvent";
import type {MenuItemClickedEvent} from "./Events/MenuItemClickedEvent";
import type {HasPlayerMovedEvent} from "./Events/HasPlayerMovedEvent";
import {isLoadPageEvent} from "./Events/LoadPageEvent";
import {handleMenuItemRegistrationEvent, isMenuItemRegisterIframeEvent} from "./Events/ui/MenuItemRegisterEvent";
/**
* Listens to messages from iframes and turn those messages into easy to use observables.
@ -115,22 +115,17 @@ class IframeListener {
// Note: maybe we could restrict on the domain too for additional security (in case the iframe goes to another domain).
let foundSrc: string | undefined;
foundSrc = [...this.scripts.keys()].find(key => {
return this.scripts.get(key)?.contentWindow == message.source
});
if (foundSrc === undefined) {
let iframe: HTMLIFrameElement;
for (iframe of this.iframes) {
if (iframe.contentWindow === message.source) {
foundSrc = iframe.src;
break;}
break;
}
}
if (foundSrc === undefined) {
return;
}
}
const payload = message.data;
if (isIframeEventWrapper(payload)) {
@ -188,13 +183,13 @@ class IframeListener {
this.sendPlayerMove = true
} else if (payload.type == "getDataLayer") {
this._dataLayerChangeStream.next();
} else if (payload.type == "registerMenuCommand" && isMenuItemRegisterEvent(payload.data)) {
} else if (isMenuItemRegisterIframeEvent(payload)) {
const data = payload.data.menutItem;
// @ts-ignore
this.iframeCloseCallbacks.get(iframe).push(() => {
this._unregisterMenuCommandStream.next(data);
})
this._registerMenuCommandStream.next(payload.data.menutItem)
handleMenuItemRegistrationEvent(payload.data)
}
}
}, false);
@ -295,15 +290,6 @@ 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',
@ -353,7 +339,7 @@ class IframeListener {
/**
* Sends the message... to all allowed iframes.
*/
private postMessage(message: IframeResponseEvent<keyof IframeResponseEventMap>) {
public postMessage(message: IframeResponseEvent<keyof IframeResponseEventMap>) {
for (const iframe of this.iframes) {
iframe.contentWindow?.postMessage(message, '*');
}

View file

@ -0,0 +1,11 @@
import type { MenuItemClickedEvent } from '../../Events/ui/MenuItemClickedEvent';
import { iframeListener } from '../../IframeListener';
export function sendMenuClickedEvent(menuItem: string) {
iframeListener.postMessage({
'type': 'menuItemClicked',
'data': {
menuItem: menuItem,
} as MenuItemClickedEvent
});
}

View file

@ -1,14 +1,17 @@
import { isButtonClickedEvent } from '../Events/ButtonClickedEvent';
import type { ClosePopupEvent } from '../Events/ClosePopupEvent';
import { isMenuItemClickedEvent } from '../Events/ui/MenuItemClickedEvent';
import type { MenuItemRegisterEvent } from '../Events/ui/MenuItemRegisterEvent';
import { IframeApiContribution, sendToWorkadventure } from './IframeApiContribution';
import { apiCallback } from "./registeredCallbacks";
import {Popup} from "./Ui/Popup";
import type { ButtonClickedCallback, ButtonDescriptor } from "./Ui/ButtonDescriptor";
import { Popup } from "./Ui/Popup";
let popupId = 0;
const popups: Map<number, Popup> = new Map<number, Popup>();
const popupCallbacks: Map<number, Map<number, ButtonClickedCallback>> = new Map<number, Map<number, ButtonClickedCallback>>();
const menuCallbacks: Map<string, (command: string) => void> = new Map()
interface ZonedPopupOptions {
zone: string
objectLayerName?: string,
@ -33,6 +36,16 @@ class WorkAdventureUiCommands extends IframeApiContribution<WorkAdventureUiComma
callback(popup);
}
}
}),
apiCallback({
type: "menuItemClicked",
typeChecker: isMenuItemClickedEvent,
callback: event => {
const callback = menuCallbacks.get(event.menuItem);
if (callback) {
callback(event.menuItem)
}
}
})];
@ -71,6 +84,16 @@ class WorkAdventureUiCommands extends IframeApiContribution<WorkAdventureUiComma
return popup;
}
registerMenuCommand(commandDescriptor: string, callback: (commandDescriptor: string) => void) {
menuCallbacks.set(commandDescriptor, callback);
sendToWorkadventure({
'type': 'registerMenuCommand',
'data': {
menutItem: commandDescriptor
} as MenuItemRegisterEvent
});
}
displayBubble(): void {
sendToWorkadventure({ 'type': 'displayBubble', data: null });
}

View file

@ -1,4 +1,10 @@
import {gameManager, HasMovedEvent} from "./GameManager";
import { Queue } from 'queue-typescript';
import type { Subscription } from "rxjs";
import { ConsoleGlobalMessageManager } from "../../Administration/ConsoleGlobalMessageManager";
import { GlobalMessageManager } from "../../Administration/GlobalMessageManager";
import { userMessageManager } from "../../Administration/UserMessageManager";
import { iframeListener } from "../../Api/IframeListener";
import { connectionManager } from "../../Connexion/ConnectionManager";
import type {
GroupCreatedUpdatedMessageInterface,
MessageUserJoined,
@ -9,71 +15,74 @@ import type {
PositionInterface,
RoomJoinedMessageInterface
} from "../../Connexion/ConnexionModels";
import {hasMovedEventName, Player, requestEmoteEventName} from "../Player/Player";
import { localUserStore } from "../../Connexion/LocalUserStore";
import { Room } from "../../Connexion/Room";
import type { RoomConnection } from "../../Connexion/RoomConnection";
import { worldFullMessageStream } from "../../Connexion/WorldFullMessageStream";
import {
DEBUG_MODE,
JITSI_PRIVATE_MODE,
MAX_PER_GROUP,
POSITION_DELAY,
POSITION_DELAY
} from "../../Enum/EnvironmentVariable";
import type {
ITiledMap,
ITiledMapLayer,
ITiledMapLayerProperty,
ITiledMapObject,
ITiledMapTileLayer,
ITiledTileSet
} from "../Map/ITiledMap";
import type { AddPlayerInterface } from "./AddPlayerInterface";
import { PlayerAnimationDirections } from "../Player/Animation";
import { PlayerMovement } from "./PlayerMovement";
import { PlayersPositionInterpolator } from "./PlayersPositionInterpolator";
import { RemotePlayer } from "../Entity/RemotePlayer";
import { Queue } from 'queue-typescript';
import { SimplePeer, UserSimplePeerInterface } from "../../WebRtc/SimplePeer";
import { ReconnectingSceneName } from "../Reconnecting/ReconnectingScene";
import { lazyLoadPlayerCharacterTextures, loadCustomTexture } from "../Entity/PlayerTexturesLoadingManager";
import { TextureError } from "../../Exception/TextureError";
import type { UserMovedMessage } from "../../Messages/generated/messages_pb";
import { ProtobufClientUtils } from "../../Network/ProtobufClientUtils";
import { peerStore } from "../../Stores/PeerStore";
import { touchScreenManager } from "../../Touch/TouchScreenManager";
import { urlManager } from "../../Url/UrlManager";
import { audioManager } from "../../WebRtc/AudioManager";
import { coWebsiteManager } from "../../WebRtc/CoWebsiteManager";
import { HtmlUtils } from "../../WebRtc/HtmlUtils";
import { jitsiFactory } from "../../WebRtc/JitsiFactory";
import {
CenterListener,
AUDIO_LOOP_PROPERTY, AUDIO_VOLUME_PROPERTY, CenterListener,
JITSI_MESSAGE_PROPERTIES,
layoutManager,
LayoutMode,
ON_ACTION_TRIGGER_BUTTON,
TRIGGER_JITSI_PROPERTIES,
TRIGGER_WEBSITE_PROPERTIES,
WEBSITE_MESSAGE_PROPERTIES,
AUDIO_VOLUME_PROPERTY,
AUDIO_LOOP_PROPERTY
WEBSITE_MESSAGE_PROPERTIES
} from "../../WebRtc/LayoutManager";
import { GameMap } from "./GameMap";
import { coWebsiteManager } from "../../WebRtc/CoWebsiteManager";
import { mediaManager } from "../../WebRtc/MediaManager";
import type { ItemFactoryInterface } from "../Items/ItemFactoryInterface";
import type { ActionableItem } from "../Items/ActionableItem";
import { UserInputManager } from "../UserInput/UserInputManager";
import {soundManager} from "./SoundManager";
import type { UserMovedMessage } from "../../Messages/generated/messages_pb";
import { ProtobufClientUtils } from "../../Network/ProtobufClientUtils";
import { connectionManager } from "../../Connexion/ConnectionManager";
import type { RoomConnection } from "../../Connexion/RoomConnection";
import { GlobalMessageManager } from "../../Administration/GlobalMessageManager";
import { userMessageManager } from "../../Administration/UserMessageManager";
import { ConsoleGlobalMessageManager } from "../../Administration/ConsoleGlobalMessageManager";
import { ResizableScene } from "../Login/ResizableScene";
import { Room } from "../../Connexion/Room";
import { jitsiFactory } from "../../WebRtc/JitsiFactory";
import { urlManager } from "../../Url/UrlManager";
import { audioManager } from "../../WebRtc/AudioManager";
import { PresentationModeIcon } from "../Components/PresentationModeIcon";
import { SimplePeer, UserSimplePeerInterface } from "../../WebRtc/SimplePeer";
import { lazyLoadCompanionResource } from "../Companion/CompanionTexturesLoadingManager";
import { ChatModeIcon } from "../Components/ChatModeIcon";
import { OpenChatIcon, openChatIconName } from "../Components/OpenChatIcon";
import { SelectCharacterScene, SelectCharacterSceneName } from "../Login/SelectCharacterScene";
import { TextureError } from "../../Exception/TextureError";
import { addLoader } from "../Components/Loader";
import { joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey } from "../Components/MobileJoystick";
import { OpenChatIcon, openChatIconName } from "../Components/OpenChatIcon";
import { PresentationModeIcon } from "../Components/PresentationModeIcon";
import { TextUtils } from "../Components/TextUtils";
import { lazyLoadPlayerCharacterTextures, loadCustomTexture } from "../Entity/PlayerTexturesLoadingManager";
import { RemotePlayer } from "../Entity/RemotePlayer";
import type { ActionableItem } from "../Items/ActionableItem";
import type { ItemFactoryInterface } from "../Items/ItemFactoryInterface";
import { SelectCharacterScene, SelectCharacterSceneName } from "../Login/SelectCharacterScene";
import type {
ITiledMap,
ITiledMapLayer,
ITiledMapLayerProperty,
ITiledMapObject,
ITiledMapTileLayer,
ITiledTileSet } from "../Map/ITiledMap";
import { MenuScene, MenuSceneName } from '../Menu/MenuScene';
import { PlayerAnimationDirections } from "../Player/Animation";
import { hasMovedEventName, Player, requestEmoteEventName } from "../Player/Player";
import { ErrorSceneName } from "../Reconnecting/ErrorScene";
import { localUserStore } from "../../Connexion/LocalUserStore";
import { iframeListener } from "../../Api/IframeListener";
import { HtmlUtils } from "../../WebRtc/HtmlUtils";
import { ReconnectingSceneName } from "../Reconnecting/ReconnectingScene";
import { waScaleManager } from "../Services/WaScaleManager";
import { PinchManager } from "../UserInput/PinchManager";
import { UserInputManager } from "../UserInput/UserInputManager";
import type { AddPlayerInterface } from "./AddPlayerInterface";
import { DEPTH_OVERLAY_INDEX } from "./DepthIndexes";
import { DirtyScene } from "./DirtyScene";
import { EmoteManager } from "./EmoteManager";
import { gameManager } from "./GameManager";
import { GameMap } from "./GameMap";
import { PlayerMovement } from "./PlayerMovement";
import { PlayersPositionInterpolator } from "./PlayersPositionInterpolator";
import { soundManager } from "./SoundManager";
import Texture = Phaser.Textures.Texture;
import Sprite = Phaser.GameObjects.Sprite;
import CanvasTexture = Phaser.Textures.CanvasTexture;
@ -81,22 +90,9 @@ import GameObject = Phaser.GameObjects.GameObject;
import FILE_LOAD_ERROR = Phaser.Loader.Events.FILE_LOAD_ERROR;
import DOMElement = Phaser.GameObjects.DOMElement;
import EVENT_TYPE = Phaser.Scenes.Events
import type {Subscription} from "rxjs";
import {worldFullMessageStream} from "../../Connexion/WorldFullMessageStream";
import { lazyLoadCompanionResource } from "../Companion/CompanionTexturesLoadingManager";
import RenderTexture = Phaser.GameObjects.RenderTexture;
import Tilemap = Phaser.Tilemaps.Tilemap;
import { DirtyScene } from "./DirtyScene";
import { TextUtils } from "../Components/TextUtils";
import { touchScreenManager } from "../../Touch/TouchScreenManager";
import { PinchManager } from "../UserInput/PinchManager";
import { joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey } from "../Components/MobileJoystick";
import { DEPTH_OVERLAY_INDEX } from "./DepthIndexes";
import { waScaleManager } from "../Services/WaScaleManager";
import { peerStore} from "../../Stores/PeerStore";
import {EmoteManager } from "./EmoteManager";
import type { HasPlayerMovedEvent } from '../../Api/Events/HasPlayerMovedEvent';
import { MenuScene, MenuSceneName } from '../Menu/MenuScene';
import AnimatedTiles from "phaser-animated-tiles";
@ -899,20 +895,17 @@ ${escapedMessage}
this.userInputManager.disableControls();
}));
this.iframeSubscriptionList.push(iframeListener.playSoundStream.subscribe((playSoundEvent)=>
{
this.iframeSubscriptionList.push(iframeListener.playSoundStream.subscribe((playSoundEvent) => {
const url = new URL(playSoundEvent.url, this.MapUrlFile);
soundManager.playSound(this.load, this.sound, url.toString(), playSoundEvent.config);
}))
this.iframeSubscriptionList.push(iframeListener.stopSoundStream.subscribe((stopSoundEvent)=>
{
this.iframeSubscriptionList.push(iframeListener.stopSoundStream.subscribe((stopSoundEvent) => {
const url = new URL(stopSoundEvent.url, this.MapUrlFile);
soundManager.stopSound(this.sound, url.toString());
}))
this.iframeSubscriptionList.push(iframeListener.loadSoundStream.subscribe((loadSoundEvent)=>
{
this.iframeSubscriptionList.push(iframeListener.loadSoundStream.subscribe((loadSoundEvent) => {
const url = new URL(loadSoundEvent.url, this.MapUrlFile);
soundManager.loadSound(this.load, this.sound, url.toString());
}))

View file

@ -14,6 +14,8 @@ import { HtmlUtils } from '../../WebRtc/HtmlUtils';
import { iframeListener } from '../../Api/IframeListener';
import { Subscription } from 'rxjs';
import { videoConstraintStore } from "../../Stores/MediaStore";
import {registerMenuCommandStream} from "../../Api/Events/ui/MenuItemRegisterEvent";
import {sendMenuClickedEvent} from "../../Api/iframe/Ui/MenuItem";
export const MenuSceneName = 'MenuScene';
const gameMenuKey = 'gameMenu';
@ -47,7 +49,7 @@ export class MenuScene extends Phaser.Scene {
this.gameQualityValue = localUserStore.getGameQualityValue();
this.videoQualityValue = localUserStore.getVideoQualityValue();
this.subscriptions.add(iframeListener.registerMenuCommandStream.subscribe(menuCommand => {
this.subscriptions.add(registerMenuCommandStream.subscribe(menuCommand => {
this.addMenuOption(menuCommand);
}))
@ -56,6 +58,24 @@ export class MenuScene extends Phaser.Scene {
}))
}
reset() {
const addedMenuItems = [...this.menuElement.node.querySelectorAll(".fromApi")];
for (let index = addedMenuItems.length - 1; index >= 0; index--) {
addedMenuItems[index].remove()
}
}
public addMenuOption(menuText: string) {
const wrappingSection = document.createElement("section")
const escapedHtml = HtmlUtils.escapeHtml(menuText);
wrappingSection.innerHTML = `<button class="fromApi" id="${escapedHtml}">${escapedHtml}</button>`
const menuItemContainer = this.menuElement.node.querySelector("#gameMenu main");
if (menuItemContainer) {
menuItemContainer.querySelector(`#${escapedHtml}.fromApi`)?.remove()
menuItemContainer.insertBefore(wrappingSection, menuItemContainer.querySelector("#socialLinks"))
}
}
preload() {
this.load.html(gameMenuKey, 'resources/html/gameMenu.html');
this.load.html(gameMenuIconKey, 'resources/html/gameMenuIcon.html');
@ -65,13 +85,6 @@ 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);
@ -287,17 +300,6 @@ export class MenuScene extends Phaser.Scene {
});
}
public addMenuOption(menuText: string) {
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')) {
@ -306,11 +308,11 @@ export class MenuScene extends Phaser.Scene {
event.preventDefault();
if (htmlMenuItem.classList.contains("fromApi")) {
iframeListener.sendMenuClickedEvent(htmlMenuItem.id)
sendMenuClickedEvent(htmlMenuItem.id)
return
}
switch (htmlMenuItem.id) {
switch ((event?.target as HTMLInputElement).id) {
case 'changeNameButton':
this.closeSideMenu();
gameManager.leaveGame(this, LoginSceneName, new LoginScene());

View file

@ -15,10 +15,7 @@ import room from "./Api/iframe/room";
import type { ButtonDescriptor } from "./Api/iframe/Ui/ButtonDescriptor";
import type { Popup } from "./Api/iframe/Ui/Popup";
import type { Sound } from "./Api/iframe/Sound/Sound";
import type {MenuItemRegisterEvent} from "./Api/Events/MenuItemRegisterEvent";
const menuCallbacks: Map<string, (command: string) => void> = new Map()
const wa = {
ui,
nav,
@ -124,16 +121,6 @@ const wa = {
console.warn('Method WA.openPopup is deprecated. Please use WA.ui.openPopup instead');
return ui.openPopup(targetObject, message, buttons);
},
registerMenuCommand(commandDescriptor: string, callback: (commandDescriptor: string) => void): void {
menuCallbacks.set(commandDescriptor, callback);
window.parent.postMessage({
'type': 'registerMenuCommand',
'data': {
menutItem: commandDescriptor
} as MenuItemRegisterEvent
}, '*');
},
/**
* @deprecated Use WA.chat.onChatMessage instead
*/

View file

@ -5,11 +5,9 @@
</head>
<body>
<script>
let chatbotEnabled = false;
WA.registerMenuCommand('help', () => {
chatbotEnabled = true;
WA.sendChatMessage("HELP", 'Mr Robot');
});
WA.ui.registerMenuCommand("test", () => {
WA.chat.sendChatMessage("test clicked", "menu cmd")
})
</script>
</body>
</html>