Merge branch 'develop' of github.com:thecodingmachine/workadventure

This commit is contained in:
_Bastler 2021-08-27 20:19:30 +02:00
commit a633f79dd7
27 changed files with 470 additions and 374 deletions

View File

@ -1,5 +1,11 @@
## Version develop ## Version develop
### Updates
- New scripting API features :
- Use `WA.ui.registerMenuCommand(commandDescriptor: string, options: MenuOptions): Menu` to add a custom menu or an iframe to the menu.
## Version 1.4.14
### Updates ### Updates
- New scripting API features : - New scripting API features :
- Use `WA.room.loadTileset(url: string) : Promise<number>` to load a tileset from a JSON file. - Use `WA.room.loadTileset(url: string) : Promise<number>` to load a tileset from a JSON file.

View File

@ -18,3 +18,4 @@ The list of functions below is **deprecated**. You should not use those but. use
- Method `WA.onChatMessage` is deprecated. It has been renamed to `WA.chat.onChatMessage`. - Method `WA.onChatMessage` is deprecated. It has been renamed to `WA.chat.onChatMessage`.
- Method `WA.onEnterZone` is deprecated. It has been renamed to `WA.room.onEnterZone`. - Method `WA.onEnterZone` is deprecated. It has been renamed to `WA.room.onEnterZone`.
- Method `WA.onLeaveZone` is deprecated. It has been renamed to `WA.room.onLeaveZone`. - Method `WA.onLeaveZone` is deprecated. It has been renamed to `WA.room.onLeaveZone`.
- Method `WA.ui.registerMenuCommand` parameter `callback` is deprecated. Use `WA.ui.registerMenuCommand(commandDescriptor: string, options: MenuOptions)`.

View File

@ -9,6 +9,7 @@ Moreover, `WA.state` functions can be used to persist this state across reloads.
``` ```
WA.state.saveVariable(key : string, data : unknown): void WA.state.saveVariable(key : string, data : unknown): void
WA.state.loadVariable(key : string) : unknown WA.state.loadVariable(key : string) : unknown
WA.state.hasVariable(key : string) : boolean
WA.state.onVariableChange(key : string).subscribe((data: unknown) => {}) : Subscription WA.state.onVariableChange(key : string).subscribe((data: unknown) => {}) : Subscription
WA.state.[any property]: unknown WA.state.[any property]: unknown
``` ```

View File

@ -68,25 +68,53 @@ WA.room.onLeaveZone('myZone', () => {
### Add custom menu ### Add custom menu
```typescript
WA.ui.registerMenuCommand(menuCommand: string, callback: (menuCommand: string) => void): void
``` ```
Add a custom menu item containing the text `commandDescriptor` in the main menu. A click on the menu will trigger the `callback`. WA.ui.registerMenuCommand(commandDescriptor: string, options: MenuOptions): Menu
```
Add a custom menu item containing the text `commandDescriptor` in the navbar of the menu.
`options` attribute accepts an object with three properties :
- `callback : (commandDescriptor: string) => void` : A click on the custom menu will trigger the `callback`.
- `iframe: string` : A click on the custom menu will open the `iframe` inside the menu.
- `allowApi?: boolean` : Allow the iframe of the custom menu to use the Scripting API.
Important : `options` accepts only `callback` or `iframe` not both.
Custom menu exist only until the map is unloaded, or you leave the iframe zone of the script. Custom menu exist only until the map is unloaded, or you leave the iframe zone of the script.
Example: <div class="row">
<div class="col">
<img src="images/custom-menu-navbar.png" class="figure-img img-fluid rounded" alt="" />
</div>
<div class="col">
<img src="images/custom-menu-iframe.png" class="figure-img img-fluid rounded" alt="" />
</div>
</div>
Example:
```javascript ```javascript
const menu = WA.ui.registerMenuCommand('menu test',
{
callback: () => {
WA.chat.sendChatMessage('test');
}
})
WA.ui.registerMenuCommand("test", () => { // Some time later, if you want to remove the menu:
WA.chat.sendChatMessage("test clicked", "menu cmd") menu.remove();
})
``` ```
<div class="col"> Please note that `registerMenuCommand` returns an object of the `Menu` class.
<img src="images/menu-command.png" class="figure-img img-fluid rounded" alt="" />
</div> The `Menu` class contains a single method: `remove(): void`. This will obviously remove the menu when called.
```javascript
class Menu {
/**
* Remove the menu
*/
remove() {};
}
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
front/dist/static/images/logo-WA-min.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -15,7 +15,6 @@ import type { SetPropertyEvent } from "./setPropertyEvent";
import type { LoadSoundEvent } from "./LoadSoundEvent"; import type { LoadSoundEvent } from "./LoadSoundEvent";
import type { PlaySoundEvent } from "./PlaySoundEvent"; import type { PlaySoundEvent } from "./PlaySoundEvent";
import type { MenuItemClickedEvent } from "./ui/MenuItemClickedEvent"; import type { MenuItemClickedEvent } from "./ui/MenuItemClickedEvent";
import type { MenuItemRegisterEvent } from "./ui/MenuItemRegisterEvent";
import type { HasPlayerMovedEvent } from "./HasPlayerMovedEvent"; import type { HasPlayerMovedEvent } from "./HasPlayerMovedEvent";
import type { SetTilesEvent } from "./SetTilesEvent"; import type { SetTilesEvent } from "./SetTilesEvent";
import type { SetVariableEvent } from "./SetVariableEvent"; import type { SetVariableEvent } from "./SetVariableEvent";
@ -33,6 +32,7 @@ import type {
TriggerActionMessageEvent, TriggerActionMessageEvent,
} from "./ui/TriggerActionMessageEvent"; } from "./ui/TriggerActionMessageEvent";
import { isMessageReferenceEvent, isTriggerActionMessageEvent } from "./ui/TriggerActionMessageEvent"; import { isMessageReferenceEvent, isTriggerActionMessageEvent } from "./ui/TriggerActionMessageEvent";
import type { MenuRegisterEvent, UnregisterMenuEvent } from "./ui/MenuRegisterEvent";
export interface TypedMessageEvent<T> extends MessageEvent { export interface TypedMessageEvent<T> extends MessageEvent {
data: T; data: T;
@ -63,7 +63,8 @@ export type IframeEventMap = {
stopSound: null; stopSound: null;
getState: undefined; getState: undefined;
loadTileset: LoadTilesetEvent; loadTileset: LoadTilesetEvent;
registerMenuCommand: MenuItemRegisterEvent; registerMenu: MenuRegisterEvent;
unregisterMenu: UnregisterMenuEvent;
setTiles: SetTilesEvent; setTiles: SetTilesEvent;
modifyEmbeddedWebsite: Partial<EmbeddedWebsite>; // Note: name should be compulsory in fact modifyEmbeddedWebsite: Partial<EmbeddedWebsite>; // Note: name should be compulsory in fact
}; };

View File

@ -1,5 +1,4 @@
import * as tg from "generic-type-guard"; import * as tg from "generic-type-guard";
import { isMenuItemRegisterEvent } from "./ui/MenuItemRegisterEvent";
export const isSetVariableEvent = new tg.IsInterface() export const isSetVariableEvent = new tg.IsInterface()
.withProperties({ .withProperties({

View File

@ -1,24 +0,0 @@
import * as tg from "generic-type-guard";
import { Subject } from "rxjs";
import { subMenusStore } from "../../../Stores/MenuStore";
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();
export function handleMenuItemRegistrationEvent(event: MenuItemRegisterEvent) {
subMenusStore.addMenu(event.menutItem);
}

View File

@ -0,0 +1,31 @@
import * as tg from "generic-type-guard";
/**
* A message sent from a script to the game to remove a custom menu from the menu
*/
export const isUnregisterMenuEvent = new tg.IsInterface()
.withProperties({
name: tg.isString,
})
.get();
export type UnregisterMenuEvent = tg.GuardedType<typeof isUnregisterMenuEvent>;
export const isMenuRegisterOptions = new tg.IsInterface()
.withProperties({
allowApi: tg.isBoolean,
})
.get();
/**
* A message sent from a script to the game to add a custom menu from the menu
*/
export const isMenuRegisterEvent = new tg.IsInterface()
.withProperties({
name: tg.isString,
iframe: tg.isUnion(tg.isString, tg.isUndefined),
options: isMenuRegisterOptions,
})
.get();
export type MenuRegisterEvent = tg.GuardedType<typeof isMenuRegisterEvent>;

View File

@ -29,12 +29,12 @@ import { isSetPropertyEvent, SetPropertyEvent } from "./Events/setPropertyEvent"
import { isLayerEvent, LayerEvent } from "./Events/LayerEvent"; import { isLayerEvent, LayerEvent } from "./Events/LayerEvent";
import type { HasPlayerMovedEvent } from "./Events/HasPlayerMovedEvent"; import type { HasPlayerMovedEvent } from "./Events/HasPlayerMovedEvent";
import { isLoadPageEvent } from "./Events/LoadPageEvent"; import { isLoadPageEvent } from "./Events/LoadPageEvent";
import { handleMenuItemRegistrationEvent, isMenuItemRegisterIframeEvent } from "./Events/ui/MenuItemRegisterEvent"; import { isMenuRegisterEvent, isUnregisterMenuEvent } from "./Events/ui/MenuRegisterEvent";
import { SetTilesEvent, isSetTilesEvent } from "./Events/SetTilesEvent"; import { SetTilesEvent, isSetTilesEvent } from "./Events/SetTilesEvent";
import type { SetVariableEvent } from "./Events/SetVariableEvent"; import type { SetVariableEvent } from "./Events/SetVariableEvent";
import { ModifyEmbeddedWebsiteEvent, isEmbeddedWebsiteEvent } from "./Events/EmbeddedWebsiteEvent"; import { ModifyEmbeddedWebsiteEvent, isEmbeddedWebsiteEvent } from "./Events/EmbeddedWebsiteEvent";
import { EmbeddedWebsite } from "./iframe/Room/EmbeddedWebsite"; import { EmbeddedWebsite } from "./iframe/Room/EmbeddedWebsite";
import { subMenusStore } from "../Stores/MenuStore"; import { handleMenuRegistrationEvent, handleMenuUnregisterEvent } from "../Stores/MenuStore";
type AnswererCallback<T extends keyof IframeQueryMap> = ( type AnswererCallback<T extends keyof IframeQueryMap> = (
query: IframeQueryMap[T]["query"], query: IframeQueryMap[T]["query"],
@ -258,17 +258,23 @@ class IframeListener {
this._removeBubbleStream.next(); this._removeBubbleStream.next();
} else if (payload.type == "onPlayerMove") { } else if (payload.type == "onPlayerMove") {
this.sendPlayerMove = true; this.sendPlayerMove = true;
} else if (isMenuItemRegisterIframeEvent(payload)) {
const data = payload.data.menutItem;
// @ts-ignore
this.iframeCloseCallbacks.get(iframe).push(() => {
subMenusStore.removeMenu(data);
});
handleMenuItemRegistrationEvent(payload.data);
} else if (payload.type == "setTiles" && isSetTilesEvent(payload.data)) { } else if (payload.type == "setTiles" && isSetTilesEvent(payload.data)) {
this._setTilesStream.next(payload.data); this._setTilesStream.next(payload.data);
} else if (payload.type == "modifyEmbeddedWebsite" && isEmbeddedWebsiteEvent(payload.data)) { } else if (payload.type == "modifyEmbeddedWebsite" && isEmbeddedWebsiteEvent(payload.data)) {
this._modifyEmbeddedWebsiteStream.next(payload.data); this._modifyEmbeddedWebsiteStream.next(payload.data);
} else if (payload.type == "registerMenu" && isMenuRegisterEvent(payload.data)) {
const dataName = payload.data.name;
this.iframeCloseCallbacks.get(iframe)?.push(() => {
handleMenuUnregisterEvent(dataName);
});
handleMenuRegistrationEvent(
payload.data.name,
payload.data.iframe,
foundSrc,
payload.data.options
);
} else if (payload.type == "unregisterMenu" && isUnregisterMenuEvent(payload.data)) {
handleMenuUnregisterEvent(payload.data.name);
} }
} }
}, },

View File

@ -0,0 +1,17 @@
import { sendToWorkadventure } from "../IframeApiContribution";
export class Menu {
constructor(private menuName: string) {}
/**
* remove the menu
*/
public remove() {
sendToWorkadventure({
type: "unregisterMenu",
data: {
name: this.menuName,
},
});
}
}

View File

@ -62,6 +62,10 @@ export class WorkadventureStateCommands extends IframeApiContribution<Workadvent
return variables.get(key); return variables.get(key);
} }
hasVariable(key: string): boolean {
return variables.has(key);
}
onVariableChange(key: string): Observable<unknown> { onVariableChange(key: string): Observable<unknown> {
let subject = variableSubscribers.get(key); let subject = variableSubscribers.get(key);
if (subject === undefined) { if (subject === undefined) {
@ -85,6 +89,12 @@ const proxyCommand = new Proxy(new WorkadventureStateCommands(), {
target.saveVariable(p.toString(), value); target.saveVariable(p.toString(), value);
return true; return true;
}, },
has(target: WorkadventureStateCommands, p: PropertyKey): boolean {
if (p in target) {
return true;
}
return target.hasVariable(p.toString());
},
}) as WorkadventureStateCommands & { [key: string]: unknown }; }) as WorkadventureStateCommands & { [key: string]: unknown };
export default proxyCommand; export default proxyCommand;

View File

@ -6,6 +6,8 @@ import type { ButtonClickedCallback, ButtonDescriptor } from "./Ui/ButtonDescrip
import { Popup } from "./Ui/Popup"; import { Popup } from "./Ui/Popup";
import { ActionMessage } from "./Ui/ActionMessage"; import { ActionMessage } from "./Ui/ActionMessage";
import { isMessageReferenceEvent } from "../Events/ui/TriggerActionMessageEvent"; import { isMessageReferenceEvent } from "../Events/ui/TriggerActionMessageEvent";
import { Menu } from "./Ui/Menu";
import type { RequireOnlyOne } from "../types";
let popupId = 0; let popupId = 0;
const popups: Map<number, Popup> = new Map<number, Popup>(); const popups: Map<number, Popup> = new Map<number, Popup>();
@ -14,9 +16,18 @@ const popupCallbacks: Map<number, Map<number, ButtonClickedCallback>> = new Map<
Map<number, ButtonClickedCallback> Map<number, ButtonClickedCallback>
>(); >();
const menus: Map<string, Menu> = new Map<string, Menu>();
const menuCallbacks: Map<string, (command: string) => void> = new Map(); const menuCallbacks: Map<string, (command: string) => void> = new Map();
const actionMessages = new Map<string, ActionMessage>(); const actionMessages = new Map<string, ActionMessage>();
interface MenuDescriptor {
callback?: (commandDescriptor: string) => void;
iframe?: string;
allowApi?: boolean;
}
export type MenuOptions = RequireOnlyOne<MenuDescriptor, "callback" | "iframe">;
interface ZonedPopupOptions { interface ZonedPopupOptions {
zone: string; zone: string;
objectLayerName?: string; objectLayerName?: string;
@ -53,6 +64,10 @@ export class WorkAdventureUiCommands extends IframeApiContribution<WorkAdventure
typeChecker: isMenuItemClickedEvent, typeChecker: isMenuItemClickedEvent,
callback: (event) => { callback: (event) => {
const callback = menuCallbacks.get(event.menuItem); const callback = menuCallbacks.get(event.menuItem);
const menu = menus.get(event.menuItem);
if (menu === undefined) {
throw new Error('Could not find menu named "' + event.menuItem + '"');
}
if (callback) { if (callback) {
callback(event.menuItem); callback(event.menuItem);
} }
@ -106,14 +121,53 @@ export class WorkAdventureUiCommands extends IframeApiContribution<WorkAdventure
return popup; return popup;
} }
registerMenuCommand(commandDescriptor: string, callback: (commandDescriptor: string) => void) { registerMenuCommand(commandDescriptor: string, options: MenuOptions | ((commandDescriptor: string) => void)): Menu {
menuCallbacks.set(commandDescriptor, callback); const menu = new Menu(commandDescriptor);
sendToWorkadventure({
type: "registerMenuCommand", if (typeof options === "function") {
data: { menuCallbacks.set(commandDescriptor, options);
menutItem: commandDescriptor, sendToWorkadventure({
}, type: "registerMenu",
}); data: {
name: commandDescriptor,
options: {
allowApi: false,
},
},
});
} else {
options.allowApi = options.allowApi === undefined ? options.iframe !== undefined : options.allowApi;
if (options.iframe !== undefined) {
sendToWorkadventure({
type: "registerMenu",
data: {
name: commandDescriptor,
iframe: options.iframe,
options: {
allowApi: options.allowApi,
},
},
});
} else if (options.callback !== undefined) {
menuCallbacks.set(commandDescriptor, options.callback);
sendToWorkadventure({
type: "registerMenu",
data: {
name: commandDescriptor,
options: {
allowApi: options.allowApi,
},
},
});
} else {
throw new Error(
"When adding a menu with WA.ui.registerMenuCommand, you must pass either an iframe or a callback"
);
}
}
menus.set(commandDescriptor, menu);
return menu;
} }
displayBubble(): void { displayBubble(): void {

4
front/src/Api/types.ts Normal file
View File

@ -0,0 +1,4 @@
export type RequireOnlyOne<T, keys extends keyof T = keyof T> = Pick<T, Exclude<keyof T, keys>> &
{
[K in keys]-?: Required<Pick<T, K>> & Partial<Record<Exclude<keys, K>, undefined>>;
}[keys];

View File

@ -0,0 +1,33 @@
<script lang="ts">
import {onDestroy, onMount} from "svelte";
import {iframeListener} from "../../Api/IframeListener";
export let url: string;
export let allowApi: boolean;
let HTMLIframe: HTMLIFrameElement;
onMount( () => {
if (allowApi) {
iframeListener.registerIframe(HTMLIframe);
}
})
onDestroy( () => {
if (allowApi) {
iframeListener.unregisterIframe(HTMLIframe);
}
})
</script>
<iframe title="customSubMenu" src="{url}" bind:this={HTMLIframe}></iframe>
<style lang="scss">
iframe {
border: none;
height: calc(100% - 56px);
width: 100%;
margin: 0;
}
</style>

View File

@ -6,18 +6,34 @@
import AboutRoomSubMenu from "./AboutRoomSubMenu.svelte"; import AboutRoomSubMenu from "./AboutRoomSubMenu.svelte";
import GlobalMessageSubMenu from "./GlobalMessagesSubMenu.svelte"; import GlobalMessageSubMenu from "./GlobalMessagesSubMenu.svelte";
import ContactSubMenu from "./ContactSubMenu.svelte"; import ContactSubMenu from "./ContactSubMenu.svelte";
import {menuVisiblilityStore, SubMenusInterface, subMenusStore} from "../../Stores/MenuStore"; import CustomSubMenu from "./CustomSubMenu.svelte"
import {onMount} from "svelte"; import {customMenuIframe, menuVisiblilityStore, SubMenusInterface, subMenusStore} from "../../Stores/MenuStore";
import {onDestroy, onMount} from "svelte";
import {get} from "svelte/store"; import {get} from "svelte/store";
import type {Unsubscriber} from "svelte/store";
import {sendMenuClickedEvent} from "../../Api/iframe/Ui/MenuItem"; import {sendMenuClickedEvent} from "../../Api/iframe/Ui/MenuItem";
let activeSubMenu: string = SubMenusInterface.settings; let activeSubMenu: string = SubMenusInterface.settings;
let activeComponent: typeof SettingsSubMenu = SettingsSubMenu; let activeComponent: typeof SettingsSubMenu | typeof CustomSubMenu = SettingsSubMenu;
let props: { url: string, allowApi: boolean };
let unsubscriberSubMenuStore: Unsubscriber;
onMount(() => { onMount(() => {
unsubscriberSubMenuStore = subMenusStore.subscribe(() => {
if(!get(subMenusStore).includes(activeSubMenu)) {
switchMenu(SubMenusInterface.settings);
}
})
switchMenu(SubMenusInterface.settings); switchMenu(SubMenusInterface.settings);
}) })
onDestroy(() => {
if(unsubscriberSubMenuStore) {
unsubscriberSubMenuStore();
}
})
function switchMenu(menu: string) { function switchMenu(menu: string) {
if (get(subMenusStore).find((subMenu) => subMenu === menu)) { if (get(subMenusStore).find((subMenu) => subMenu === menu)) {
activeSubMenu = menu; activeSubMenu = menu;
@ -41,8 +57,14 @@
activeComponent = ContactSubMenu; activeComponent = ContactSubMenu;
break; break;
default: default:
sendMenuClickedEvent(menu); const customMenu = customMenuIframe.get(menu);
menuVisiblilityStore.set(false); if (customMenu !== undefined) {
props = { url: customMenu.url, allowApi: customMenu.allowApi };
activeComponent = CustomSubMenu;
} else {
sendMenuClickedEvent(menu);
menuVisiblilityStore.set(false);
}
break; break;
} }
} else throw ("There is no menu called " + menu); } else throw ("There is no menu called " + menu);
@ -74,7 +96,7 @@
</div> </div>
<div class="menu-submenu-container nes-container is-rounded" transition:fly="{{ y: -1000, duration: 500 }}"> <div class="menu-submenu-container nes-container is-rounded" transition:fly="{{ y: -1000, duration: 500 }}">
<h2>{activeSubMenu}</h2> <h2>{activeSubMenu}</h2>
<svelte:component this={activeComponent}/> <svelte:component this={activeComponent} {...props}/>
</div> </div>
</div> </div>

View File

@ -80,8 +80,11 @@ export class GameMap {
return; return;
} }
this.key = key; this.key = key;
this.triggerAll();
}
const newProps = this.getProperties(key); private triggerAll(): void {
const newProps = this.getProperties(this.key ?? 0);
const oldProps = this.lastProperties; const oldProps = this.lastProperties;
this.lastProperties = newProps; this.lastProperties = newProps;
@ -253,4 +256,34 @@ export class GameMap {
} }
return this.tileNameMap.get(tile); return this.tileNameMap.get(tile);
} }
public setLayerProperty(
layerName: string,
propertyName: string,
propertyValue: string | number | undefined | boolean
) {
const layer = this.findLayer(layerName);
if (layer === undefined) {
console.warn('Could not find layer "' + layerName + '" when calling setProperty');
return;
}
if (layer.properties === undefined) {
layer.properties = [];
}
const property = layer.properties.find((property) => property.name === propertyName);
if (property === undefined) {
if (propertyValue === undefined) {
return;
}
layer.properties.push({ name: propertyName, type: typeof propertyValue, value: propertyValue });
return;
}
if (propertyValue === undefined) {
const index = layer.properties.indexOf(property);
layer.properties.splice(index, 1);
}
property.value = propertyValue;
this.triggerAll();
}
} }

View File

@ -1276,30 +1276,10 @@ export class GameScene extends DirtyScene {
propertyName: string, propertyName: string,
propertyValue: string | number | boolean | undefined propertyValue: string | number | boolean | undefined
): void { ): void {
const layer = this.gameMap.findLayer(layerName);
if (layer === undefined) {
console.warn('Could not find layer "' + layerName + '" when calling setProperty');
return;
}
if (propertyName === "exitUrl" && typeof propertyValue === "string") { if (propertyName === "exitUrl" && typeof propertyValue === "string") {
this.loadNextGameFromExitUrl(propertyValue); this.loadNextGameFromExitUrl(propertyValue);
} }
if (layer.properties === undefined) { this.gameMap.setLayerProperty(layerName, propertyName, propertyValue);
layer.properties = [];
}
const property = layer.properties.find((property) => property.name === propertyName);
if (property === undefined) {
if (propertyValue === undefined) {
return;
}
layer.properties.push({ name: propertyName, type: typeof propertyValue, value: propertyValue });
return;
}
if (propertyValue === undefined) {
const index = layer.properties.indexOf(property);
layer.properties.splice(index, 1);
}
property.value = propertyValue;
} }
private setLayerVisibility(layerName: string, visible: boolean): void { private setLayerVisibility(layerName: string, visible: boolean): void {

View File

@ -82,3 +82,35 @@ function checkSubMenuToShow() {
} }
checkSubMenuToShow(); checkSubMenuToShow();
export const customMenuIframe = new Map<string, { url: string; allowApi: boolean }>();
export function handleMenuRegistrationEvent(
menuName: string,
iframeUrl: string | undefined = undefined,
source: string | undefined = undefined,
options: { allowApi: boolean }
) {
if (get(subMenusStore).includes(menuName)) {
console.warn("The menu " + menuName + " already exist.");
return;
}
subMenusStore.addMenu(menuName);
if (iframeUrl !== undefined) {
const url = new URL(iframeUrl, source);
customMenuIframe.set(menuName, { url: url.toString(), allowApi: options.allowApi });
}
}
export function handleMenuUnregisterEvent(menuName: string) {
const subMenuGeneral: string[] = Object.values(SubMenusInterface);
if (subMenuGeneral.includes(menuName)) {
console.warn("The menu " + menuName + " is a mandatory menu. It can't be remove");
return;
}
subMenusStore.removeMenu(menuName);
customMenuIframe.delete(menuName);
}

View File

@ -1,10 +1,10 @@
import type Phaser from "phaser"; import type Phaser from "phaser";
export type CursorKey = { export type CursorKey = {
isDown: boolean isDown: boolean;
} };
export type Direction = 'left' | 'right' | 'up' | 'down' export type Direction = "left" | "right" | "up" | "down";
export interface CursorKeys extends Record<Direction, CursorKey> { export interface CursorKeys extends Record<Direction, CursorKey> {
left: CursorKey; left: CursorKey;
@ -21,4 +21,3 @@ export interface IVirtualJoystick extends Phaser.GameObjects.GameObject {
visible: boolean; visible: boolean;
createCursorKeys: () => CursorKeys; createCursorKeys: () => CursorKeys;
} }

View File

@ -0,0 +1,9 @@
<!doctype html>
<html lang="en">
<head>
<title>Custom Iframe Menu</title>
</head>
<body style="text-align: center">
<p style="color: whitesmoke">This is an iframe in a custom menu.</p>
</body>
</html>

View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>API in iframe menu</title>
<script>
var script = document.createElement('script');
// Don't do this at home kids! The "document.referrer" part is actually inserting a XSS security.
// We are OK in this precise case because the HTML page is hosted on the "maps" domain that contains only static files.
script.setAttribute('src', document.referrer + 'iframe_api.js');
document.head.appendChild(script);
window.addEventListener('load', () => {
WA.chat.sendChatMessage('The iframe opened by a script works !', 'Mr Robot');
})
</script>
</head>
<body style="text-align: center">
<p style="color: whitesmoke">This is an iframe in a custom menu.</p>
</body>
</html>

View File

@ -1,20 +1,18 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<script> <script>
var script = document.createElement('script'); var script = document.createElement('script');
// Don't do this at home kids! The "document.referrer" part is actually inserting a XSS security. // Don't do this at home kids! The "document.referrer" part is actually inserting a XSS security.
// We are OK in this precise case because the HTML page is hosted on the "maps" domain that contains only static files. // We are OK in this precise case because the HTML page is hosted on the "maps" domain that contains only static files.
script.setAttribute('src', document.referrer + 'iframe_api.js'); script.setAttribute('src', document.referrer + 'iframe_api.js');
document.head.appendChild(script); document.head.appendChild(script);
window.addEventListener('load', () => { window.addEventListener('load', () => {
WA.ui.registerMenuCommand("test", () => { WA.ui.registerMenuCommand('test', 'customIframeMenu.html', {autoClose: true});
WA.chat.sendChatMessage("test clicked", "menu cmd") })
}) </script>
}) </head>
</script> <body>
</head> <p>Add a custom menu</p>
<body> </body>
<p>Add a custom menu</p>
</body>
</html> </html>

View File

@ -0,0 +1,15 @@
let menuIframeApi = undefined;
WA.ui.registerMenuCommand('custom callback menu', () => {
WA.nav.openTab("https://workadventu.re/");
})
WA.ui.registerMenuCommand('custom iframe menu', {iframe: 'customIframeMenu.html'});
WA.room.onEnterZone('iframeMenu', () => {
menuIframeApi = WA.ui.registerMenuCommand('IFRAME USE API', {iframe: 'customIframeMenuApi.html', allowApi: true});
})
WA.room.onLeaveZone('iframeMenu', () => {
menuIframeApi.remove();
})

View File

@ -1,279 +1,100 @@
{ "compressionlevel":-1, { "compressionlevel":-1,
"editorsettings": "height":10,
"infinite":false,
"layers":[
{ {
"export": "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, 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, 0, 0, 0, 0, 0, 0, 0, 0],
{ "height":10,
"target":"." "id":1,
} "name":"start",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
}, },
"height":10, {
"infinite":false, "data":[33, 34, 34, 34, 34, 34, 35, 0, 0, 0, 41, 42, 42, 42, 42, 42, 43, 0, 0, 0, 41, 42, 42, 42, 42, 42, 43, 0, 0, 0, 41, 42, 42, 42, 42, 42, 59, 34, 34, 35, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 0, 0, 0, 42, 42, 42, 42, 42, 42, 43, 0, 0, 0, 42, 42, 42, 42, 42, 42, 43, 0, 0, 0, 50, 50, 50, 50, 50, 50, 51],
"layers":[ "height":10,
"id":2,
"name":"bottom",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"data":[0, 0, 0, 0, 0, 0, 0, 33, 34, 35, 0, 0, 0, 0, 0, 0, 0, 41, 42, 43, 0, 0, 0, 0, 0, 0, 0, 49, 50, 51, 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,
"id":3,
"name":"ZoneIframeMenu",
"opacity":1,
"properties":[
{ {
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, 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, 0, 0, 0, 0, 0, 0, 0, 0], "name":"zone",
"height":10, "type":"string",
"id":1, "value":"iframeMenu"
"name":"start",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"data":[33, 34, 34, 34, 34, 34, 34, 34, 34, 35, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 49, 50, 50, 50, 50, 50, 50, 50, 50, 51],
"height":10,
"id":2,
"name":"bottom",
"opacity":1,
"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, 0, 0, 0, 0, 0, 0, 0, 0, 101, 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,
"id":6,
"name":"exit",
"opacity":1,
"properties":[
{
"name":"exitUrl",
"type":"string",
"value":"showHideLayer.json"
}],
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"data":[0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 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,
"id":4,
"name":"metadata",
"opacity":1,
"properties":[
{
"name":"openWebsite",
"type":"string",
"value":"customMenu.html"
},
{
"name":"openWebsiteAllowApi",
"type":"bool",
"value":true
}],
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":5,
"name":"floorLayer",
"objects":[
{
"height":217.142414860681,
"id":1,
"name":"",
"rotation":0,
"text":
{
"fontfamily":"Sans Serif",
"pixelsize":9,
"text":"Test : \nWalk on the grass, an iframe open.\nResult : \nOpen the menu, a new sub-menu is displayed.\n\nTest : \nExit the grass\nResult : \nOpen the menu, the submenu has disappeared.\n\nTest : \nClick on the 'HELP' menu.\nResult : \nChat open and a 'HELP' message is displayed.\n\nTest : \nWalk on the red tile then open the menu.\nResult : \nYou have exit the room to another room, the submenu has disappeared.\n",
"wrap":true
},
"type":"",
"visible":true,
"width":305.097705765524,
"x":15.1244925229277,
"y":103.029937496349
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
}], }],
"nextlayerid":7, "type":"tilelayer",
"nextobjectid":2, "visible":true,
"orientation":"orthogonal", "width":10,
"renderorder":"right-down", "x":0,
"tiledversion":"1.4.3", "y":0
"tileheight":32, },
"tilesets":[ {
"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, 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, 33, 34, 35, 0, 0, 0, 0, 0, 0, 0, 41, 42, 43, 0, 0, 0, 0, 0, 0, 0, 49, 50, 51, 0, 0, 0, 0, 0, 0, 0],
"height":10,
"id":4,
"name":"CoWebSiteIframeMenu",
"opacity":1,
"properties":[
{ {
"columns":8, "name":"openWebsite",
"firstgid":1, "type":"string",
"image":"tileset_dungeon.png", "value":"customMenu.html"
"imageheight":256, },
"imagewidth":256,
"margin":0,
"name":"TDungeon",
"spacing":0,
"tilecount":64,
"tileheight":32,
"tiles":[
{
"id":0,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":2,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":3,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":4,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":8,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":9,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":10,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":11,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":12,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":16,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":17,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":18,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":19,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":20,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
}],
"tilewidth":32
},
{ {
"columns":8, "name":"openWebsiteAllowApi",
"firstgid":65, "type":"bool",
"image":"floortileset.png", "value":true
"imageheight":288,
"imagewidth":256,
"margin":0,
"name":"Floor",
"spacing":0,
"tilecount":72,
"tileheight":32,
"tilewidth":32
}], }],
"tilewidth":32, "type":"tilelayer",
"type":"map", "visible":true,
"version":1.4, "width":10,
"width":10 "x":0,
"y":0
}],
"nextlayerid":5,
"nextobjectid":1,
"orientation":"orthogonal",
"properties":[
{
"name":"script",
"type":"string",
"value":"customMenu.js"
}],
"renderorder":"right-down",
"tiledversion":"1.7.1",
"tileheight":32,
"tilesets":[
{
"columns":8,
"firstgid":1,
"image":"tileset_dungeon.png",
"imageheight":256,
"imagewidth":256,
"margin":0,
"name":"tileset_dungeon",
"spacing":0,
"tilecount":64,
"tileheight":32,
"tilewidth":32
}],
"tilewidth":32,
"type":"map",
"version":"1.6",
"width":10
} }