diff --git a/docs/maps/api-room.md b/docs/maps/api-room.md index 579b3f58..96b62fd2 100644 --- a/docs/maps/api-room.md +++ b/docs/maps/api-room.md @@ -176,4 +176,102 @@ You can create a tileset file in Tile Editor. WA.room.loadTileset("Assets/Tileset.json").then((firstId) => { WA.room.setTiles([{x: 4, y: 4, tile: firstId, layer: 'bottom'}]); }) -``` \ No newline at end of file +``` + + +## Embedding websites in a map + +You can use the scripting API to embed websites in a map, or to edit websites that are already embedded (using the ["website" objects](website-in-map.md)). + +### Getting an instance of a website already embedded in the map + +``` +WA.room.website.get(objectName: string): Promise +``` + +You can get an instance of an embedded website by using the `WA.room.website.get()` method. +It returns a promise of an `EmbeddedWebsite` instance. + +```javascript +// Get an existing website object where 'my_website' is the name of the object (on any layer object of the map) +const website = await WA.room.website.get('my_website'); +website.url = 'https://example.com'; +website.visible = true; +``` + + +### Adding a new website in a map + +``` +WA.room.website.create(website: CreateEmbeddedWebsiteEvent): EmbeddedWebsite + +interface CreateEmbeddedWebsiteEvent { + name: string; // A unique name for this iframe + url: string; // The URL the iframe points to. + position: { + x: number, // In pixels, relative to the map coordinates + y: number, // In pixels, relative to the map coordinates + width: number, // In pixels, sensitive to zoom level + height: number, // In pixels, sensitive to zoom level + }, + visible?: boolean, // Whether to display the iframe or not + allowApi?: boolean, // Whether the scripting API should be available to the iframe + allow?: string, // The list of feature policies allowed +} +``` + +You can create an instance of an embedded website by using the `WA.room.website.create()` method. +It returns an `EmbeddedWebsite` instance. + +```javascript +// Create a new website object +const website = WA.room.website.create({ + name: "my_website", + url: "https://example.com", + position: { + x: 64, + y: 128, + width: 320, + height: 240, + }, + visible: true, + allowApi: true, + allow: "fullscreen", +}); +``` + +### Deleting a website from a map + +``` +WA.room.website.delete(name: string): Promise +``` + +Use `WA.room.website.delete` to completely remove an embedded website from your map. + + +### The EmbeddedWebsite class + +Instances of the `EmbeddedWebsite` class represent the website displayed on the map. + +```typescript +class EmbeddedWebsite { + readonly name: string; + url: string; + visible: boolean; + allow: string; + allowApi: boolean; + x: number; // In pixels, relative to the map coordinates + y: number; // In pixels, relative to the map coordinates + width: number; // In pixels, sensitive to zoom level + height: number; // In pixels, sensitive to zoom level +} +``` + +When you modify a property of an `EmbeddedWebsite` instance, the iframe is automatically modified in the map. + + +{.alert.alert-warning} +The websites you add/edit/delete via the scripting API are only shown locally. If you want them +to be displayed for every player, you can use [variables](api-start.md) to share a common state +between all users. + diff --git a/docs/maps/api-ui.md b/docs/maps/api-ui.md index 286f2ac7..89d46932 100644 --- a/docs/maps/api-ui.md +++ b/docs/maps/api-ui.md @@ -86,4 +86,51 @@ WA.ui.registerMenuCommand("test", () => {
-
\ No newline at end of file + + + + +### Awaiting User Confirmation (with space bar) + +``` +WA.ui.displayActionMessage({ + message: string, + callback: () => void, + type?: "message"|"warning", +}): ActionMessage +``` + +Displays a message at the bottom of the screen (that will disappear when space bar is pressed). + +
+ +
+ +Example: + +```javascript +const triggerMessage = WA.ui.displayActionMessage({ + message: "press 'space' to confirm", + callback: () => { + WA.chat.sendChatMessage("confirmed", "trigger message logic") + } +}); + +setTimeout(() => { + // later + triggerMessage.remove(); +}, 1000) +``` + +Please note that `displayActionMessage` returns an object of the `ActionMessage` class. + +The `ActionMessage` class contains a single method: `remove(): Promise`. This will obviously remove the message when called. + +```javascript +class ActionMessage { + /** + * Hides the message + */ + remove() {}; +} +``` diff --git a/docs/maps/website-in-map.md b/docs/maps/website-in-map.md new file mode 100644 index 00000000..7c7f8025 --- /dev/null +++ b/docs/maps/website-in-map.md @@ -0,0 +1,40 @@ +{.section-title.accent.text-primary} +# Putting a website inside a map + +You can inject a website directly into your map, at a given position. + +To do this in Tiled: + +- Select an object layer +- Create a rectangular object, at the position where you want your website to appear +- Add a `url` property to your object pointing to the URL you want to open + +
+
+ +
A "website" object
+
+
+ +The `url` can be absolute, or relative to your map. + +{.alert.alert-info} +Internally, WorkAdventure will create an "iFrame" to load the website. +Some websites forbid being opened by iframes using the [`X-Frame-Options](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options) +HTTP header. + +{.alert.alert-warning} +Please note that the website always appears **on top** of the tiles (even if you put the object layer that +contains the "website" object under the tiles). + +## Allowing the scripting API in your iframe + +If you are planning to use the WorkAdventure scripting API inside your iframe, you need +to explicitly allow it, by setting an additional `allowApi` property to `true`. + +
+
+ +
A "website" object that can communicate using the Iframe API
+
+
diff --git a/front/.eslintrc.json b/front/.eslintrc.json index 037fddae..45b44456 100644 --- a/front/.eslintrc.json +++ b/front/.eslintrc.json @@ -26,7 +26,6 @@ "rules": { "no-unused-vars": "off", "@typescript-eslint/no-explicit-any": "error", - // TODO: remove those ignored rules and write a stronger code! "@typescript-eslint/no-floating-promises": "off", "@typescript-eslint/no-unsafe-call": "off", diff --git a/front/src/Api/Events/EmbeddedWebsiteEvent.ts b/front/src/Api/Events/EmbeddedWebsiteEvent.ts new file mode 100644 index 00000000..42630be1 --- /dev/null +++ b/front/src/Api/Events/EmbeddedWebsiteEvent.ts @@ -0,0 +1,48 @@ +import * as tg from "generic-type-guard"; + +export const isRectangle = new tg.IsInterface() + .withProperties({ + x: tg.isNumber, + y: tg.isNumber, + width: tg.isNumber, + height: tg.isNumber, + }) + .get(); + +export const isEmbeddedWebsiteEvent = new tg.IsInterface() + .withProperties({ + name: tg.isString, + }) + .withOptionalProperties({ + url: tg.isString, + visible: tg.isBoolean, + allowApi: tg.isBoolean, + allow: tg.isString, + x: tg.isNumber, + y: tg.isNumber, + width: tg.isNumber, + height: tg.isNumber, + }) + .get(); + +export const isCreateEmbeddedWebsiteEvent = new tg.IsInterface() + .withProperties({ + name: tg.isString, + url: tg.isString, + position: isRectangle, + }) + .withOptionalProperties({ + visible: tg.isBoolean, + allowApi: tg.isBoolean, + allow: tg.isString, + }) + .get(); + +/** + * A message sent from the iFrame to the game to modify an embedded website + */ +export type ModifyEmbeddedWebsiteEvent = tg.GuardedType; + +export type CreateEmbeddedWebsiteEvent = tg.GuardedType; +// TODO: make a variation that is all optional (except for the name) +export type Rectangle = tg.GuardedType; diff --git a/front/src/Api/Events/IframeEvent.ts b/front/src/Api/Events/IframeEvent.ts index 0590939b..ed723241 100644 --- a/front/src/Api/Events/IframeEvent.ts +++ b/front/src/Api/Events/IframeEvent.ts @@ -9,6 +9,7 @@ import type { OpenCoWebSiteEvent } from "./OpenCoWebSiteEvent"; import type { OpenPopupEvent } from "./OpenPopupEvent"; import type { OpenTabEvent } from "./OpenTabEvent"; import type { UserInputChatEvent } from "./UserInputChatEvent"; +import type { MapDataEvent } from "./MapDataEvent"; import type { LayerEvent } from "./LayerEvent"; import type { SetPropertyEvent } from "./setPropertyEvent"; import type { LoadSoundEvent } from "./LoadSoundEvent"; @@ -21,8 +22,17 @@ import type { SetVariableEvent } from "./SetVariableEvent"; import { isGameStateEvent } from "./GameStateEvent"; import { isMapDataEvent } from "./MapDataEvent"; import { isSetVariableEvent } from "./SetVariableEvent"; +import type { EmbeddedWebsite } from "../iframe/Room/EmbeddedWebsite"; +import { isCreateEmbeddedWebsiteEvent } from "./EmbeddedWebsiteEvent"; import type { LoadTilesetEvent } from "./LoadTilesetEvent"; import { isLoadTilesetEvent } from "./LoadTilesetEvent"; +import type { + MessageReferenceEvent, + removeActionMessage, + triggerActionMessage, + TriggerActionMessageEvent, +} from "./ui/TriggerActionMessageEvent"; +import { isMessageReferenceEvent, isTriggerActionMessageEvent } from "./ui/TriggerActionMessageEvent"; export interface TypedMessageEvent extends MessageEvent { data: T; @@ -55,6 +65,7 @@ export type IframeEventMap = { loadTileset: LoadTilesetEvent; registerMenuCommand: MenuItemRegisterEvent; setTiles: SetTilesEvent; + modifyEmbeddedWebsite: Partial; // Note: name should be compulsory in fact }; export interface IframeEvent { type: T; @@ -73,6 +84,7 @@ export interface IframeResponseEventMap { hasPlayerMoved: HasPlayerMovedEvent; menuItemClicked: MenuItemClickedEvent; setVariable: SetVariableEvent; + messageTriggered: MessageReferenceEvent; } export interface IframeResponseEvent { type: T; @@ -105,6 +117,26 @@ export const iframeQueryMapTypeGuards = { query: isLoadTilesetEvent, answer: tg.isNumber, }, + triggerActionMessage: { + query: isTriggerActionMessageEvent, + answer: tg.isUndefined, + }, + removeActionMessage: { + query: isMessageReferenceEvent, + answer: tg.isUndefined, + }, + getEmbeddedWebsite: { + query: tg.isString, + answer: isCreateEmbeddedWebsiteEvent, + }, + deleteEmbeddedWebsite: { + query: tg.isString, + answer: tg.isUndefined, + }, + createEmbeddedWebsite: { + query: isCreateEmbeddedWebsiteEvent, + answer: tg.isUndefined, + }, }; type GuardedType = T extends (x: unknown) => x is infer T ? T : never; @@ -141,7 +173,12 @@ export const isIframeQuery = (event: any): event is IframeQuery; + +export const isTriggerActionMessageEvent = new tg.IsInterface() + .withProperties({ + message: tg.isString, + uuid: tg.isString, + type: isActionMessageType, + }) + .get(); + +export type TriggerActionMessageEvent = 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..fb64c742 --- /dev/null +++ b/front/src/Api/Events/ui/TriggerMessageEventHandler.ts @@ -0,0 +1,24 @@ +import { + isMessageReferenceEvent, + isTriggerActionMessageEvent, + removeActionMessage, + triggerActionMessage, +} from './TriggerActionMessageEvent'; + +import * as tg from 'generic-type-guard'; + +const isTriggerMessageEventObject = new tg.IsInterface() + .withProperties({ + type: tg.isSingletonString(triggerActionMessage), + data: isTriggerActionMessageEvent, + }) + .get(); + +const isTriggerMessageRemoveEventObject = new tg.IsInterface() + .withProperties({ + type: tg.isSingletonString(removeActionMessage), + data: isMessageReferenceEvent, + }) + .get(); + +export const isTriggerMessageHandlerEvent = tg.isUnion(isTriggerMessageEventObject, isTriggerMessageRemoveEventObject); diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index c72e9c9f..7ce6b32d 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -1,4 +1,5 @@ import { Subject } from "rxjs"; +import type * as tg from "generic-type-guard"; import { ChatEvent, isChatEvent } from "./Events/ChatEvent"; import { HtmlUtils } from "../WebRtc/HtmlUtils"; import type { EnterLeaveEvent } from "./Events/EnterLeaveEvent"; @@ -31,6 +32,8 @@ import { isLoadPageEvent } from "./Events/LoadPageEvent"; import { handleMenuItemRegistrationEvent, isMenuItemRegisterIframeEvent } from "./Events/ui/MenuItemRegisterEvent"; import { SetTilesEvent, isSetTilesEvent } from "./Events/SetTilesEvent"; import type { SetVariableEvent } from "./Events/SetVariableEvent"; +import { ModifyEmbeddedWebsiteEvent, isEmbeddedWebsiteEvent } from "./Events/EmbeddedWebsiteEvent"; +import { EmbeddedWebsite } from "./iframe/Room/EmbeddedWebsite"; type AnswererCallback = ( query: IframeQueryMap[T]["query"], @@ -111,6 +114,9 @@ class IframeListener { private readonly _setTilesStream: Subject = new Subject(); public readonly setTilesStream = this._setTilesStream.asObservable(); + private readonly _modifyEmbeddedWebsiteStream: Subject = new Subject(); + public readonly modifyEmbeddedWebsiteStream = this._modifyEmbeddedWebsiteStream.asObservable(); + private readonly iframes = new Set(); private readonly iframeCloseCallbacks = new Map void)[]>(); private readonly scripts = new Map(); @@ -124,7 +130,7 @@ class IframeListener { init() { window.addEventListener( "message", - (message: TypedMessageEvent>) => { + (message: MessageEvent) => { // Do we trust the sender of this message? // Let's only accept messages from the iframe that are allowed. // Note: maybe we could restrict on the domain too for additional security (in case the iframe goes to another domain). @@ -266,6 +272,8 @@ class IframeListener { handleMenuItemRegistrationEvent(payload.data); } else if (payload.type == "setTiles" && isSetTilesEvent(payload.data)) { this._setTilesStream.next(payload.data); + } else if (payload.type == "modifyEmbeddedWebsite" && isEmbeddedWebsiteEvent(payload.data)) { + this._modifyEmbeddedWebsiteStream.next(payload.data); } } }, @@ -423,6 +431,15 @@ class IframeListener { }); } + sendActionMessageTriggered(uuid: string): void { + this.postMessage({ + type: "messageTriggered", + data: { + uuid, + }, + }); + } + /** * Sends the message... to all allowed iframes. */ diff --git a/front/src/Api/iframe/IframeApiContribution.ts b/front/src/Api/iframe/IframeApiContribution.ts index e4ba089e..96548d5e 100644 --- a/front/src/Api/iframe/IframeApiContribution.ts +++ b/front/src/Api/iframe/IframeApiContribution.ts @@ -1,51 +1,66 @@ import type * as tg from "generic-type-guard"; import type { IframeEvent, - IframeEventMap, IframeQuery, + IframeEventMap, + IframeQuery, IframeQueryMap, - IframeResponseEventMap -} from '../Events/IframeEvent'; -import type {IframeQueryWrapper} from "../Events/IframeEvent"; + IframeResponseEventMap, +} from "../Events/IframeEvent"; +import type { IframeQueryWrapper } from "../Events/IframeEvent"; export function sendToWorkadventure(content: IframeEvent) { - window.parent.postMessage(content, "*") + window.parent.postMessage(content, "*"); } let queryNumber = 0; -export const answerPromises = new Map)) => void, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - reject: (reason?: any) => void -}>(); +export const answerPromises = new Map< + number, + { + resolve: ( + value: + | IframeQueryMap[keyof IframeQueryMap]["answer"] + | PromiseLike + ) => void; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + reject: (reason?: any) => void; + } +>(); -export function queryWorkadventure(content: IframeQuery): Promise { - return new Promise((resolve, reject) => { - window.parent.postMessage({ - id: queryNumber, - query: content - } as IframeQueryWrapper, "*"); +export function queryWorkadventure( + content: IframeQuery +): Promise { + return new Promise((resolve, reject) => { + window.parent.postMessage( + { + id: queryNumber, + query: content, + } as IframeQueryWrapper, + "*" + ); answerPromises.set(queryNumber, { resolve, - reject + reject, }); queryNumber++; }); } -type GuardedType> = Guard extends tg.TypeGuard ? T : never +type GuardedType> = Guard extends tg.TypeGuard ? T : never; -export interface IframeCallback> { - - typeChecker: Guard, - callback: (payloadData: T) => void +export interface IframeCallback< + Key extends keyof IframeResponseEventMap, + T = IframeResponseEventMap[Key], + Guard = tg.TypeGuard +> { + typeChecker: Guard; + callback: (payloadData: T) => void; } export interface IframeCallbackContribution extends IframeCallback { - - type: Key + type: Key; } /** @@ -54,9 +69,10 @@ export interface IframeCallbackContribution>, -}> { - - abstract callbacks: T["callbacks"] +export abstract class IframeApiContribution< + T extends { + callbacks: Array>; + } +> { + abstract callbacks: T["callbacks"]; } diff --git a/front/src/Api/iframe/Room/EmbeddedWebsite.ts b/front/src/Api/iframe/Room/EmbeddedWebsite.ts new file mode 100644 index 00000000..7b16890e --- /dev/null +++ b/front/src/Api/iframe/Room/EmbeddedWebsite.ts @@ -0,0 +1,90 @@ +import { sendToWorkadventure } from "../IframeApiContribution"; +import type { + CreateEmbeddedWebsiteEvent, + ModifyEmbeddedWebsiteEvent, + Rectangle, +} from "../../Events/EmbeddedWebsiteEvent"; + +export class EmbeddedWebsite { + public readonly name: string; + private _url: string; + private _visible: boolean; + private _allow: string; + private _allowApi: boolean; + private _position: Rectangle; + + constructor(private config: CreateEmbeddedWebsiteEvent) { + this.name = config.name; + this._url = config.url; + this._visible = config.visible ?? true; + this._allow = config.allow ?? ""; + this._allowApi = config.allowApi ?? false; + this._position = config.position; + } + + public set url(url: string) { + this._url = url; + sendToWorkadventure({ + type: "modifyEmbeddedWebsite", + data: { + name: this.name, + url: this._url, + }, + }); + } + + public set visible(visible: boolean) { + this._visible = visible; + sendToWorkadventure({ + type: "modifyEmbeddedWebsite", + data: { + name: this.name, + visible: this._visible, + }, + }); + } + + public set x(x: number) { + this._position.x = x; + sendToWorkadventure({ + type: "modifyEmbeddedWebsite", + data: { + name: this.name, + x: this._position.x, + }, + }); + } + + public set y(y: number) { + this._position.y = y; + sendToWorkadventure({ + type: "modifyEmbeddedWebsite", + data: { + name: this.name, + y: this._position.y, + }, + }); + } + + public set width(width: number) { + this._position.width = width; + sendToWorkadventure({ + type: "modifyEmbeddedWebsite", + data: { + name: this.name, + width: this._position.width, + }, + }); + } + + public set height(height: number) { + this._position.height = height; + sendToWorkadventure({ + type: "modifyEmbeddedWebsite", + data: { + name: this.name, + height: this._position.height, + }, + }); + } +} diff --git a/front/src/Api/iframe/Ui/ActionMessage.ts b/front/src/Api/iframe/Ui/ActionMessage.ts new file mode 100644 index 00000000..912603b9 --- /dev/null +++ b/front/src/Api/iframe/Ui/ActionMessage.ts @@ -0,0 +1,56 @@ +import { + ActionMessageType, + MessageReferenceEvent, + removeActionMessage, + triggerActionMessage, + TriggerActionMessageEvent, +} from "../../Events/ui/TriggerActionMessageEvent"; +import { queryWorkadventure } from "../IframeApiContribution"; +import type { ActionMessageOptions } from "../ui"; +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 class ActionMessage { + public readonly uuid: string; + private readonly type: ActionMessageType; + private readonly message: string; + private readonly callback: () => void; + + constructor(actionMessageOptions: ActionMessageOptions, private onRemove: () => void) { + this.uuid = uuidv4(); + this.message = actionMessageOptions.message; + this.type = actionMessageOptions.type ?? "message"; + this.callback = actionMessageOptions.callback; + this.create(); + } + + private async create() { + await queryWorkadventure({ + type: triggerActionMessage, + data: { + message: this.message, + type: this.type, + uuid: this.uuid, + } as TriggerActionMessageEvent, + }); + } + + async remove() { + await queryWorkadventure({ + type: removeActionMessage, + data: { + uuid: this.uuid, + } as MessageReferenceEvent, + }); + this.onRemove(); + } + + triggerCallback() { + this.callback(); + } +} diff --git a/front/src/Api/iframe/room.ts b/front/src/Api/iframe/room.ts index 9c0be9be..22df49c9 100644 --- a/front/src/Api/iframe/room.ts +++ b/front/src/Api/iframe/room.ts @@ -6,6 +6,8 @@ import { IframeApiContribution, queryWorkadventure, sendToWorkadventure } from " import { apiCallback } from "./registeredCallbacks"; import type { ITiledMap } from "../../Phaser/Map/ITiledMap"; +import type { WorkadventureRoomWebsiteCommands } from "./website"; +import website from "./website"; const enterStreams: Map> = new Map>(); const leaveStreams: Map> = new Map>(); @@ -105,6 +107,7 @@ export class WorkadventureRoomCommands extends IframeApiContribution { return await queryWorkadventure({ type: "loadTileset", @@ -113,6 +116,10 @@ export class WorkadventureRoomCommands extends IframeApiContribution = new Map(); @@ -14,6 +15,7 @@ const popupCallbacks: Map> = new Map< >(); const menuCallbacks: Map void> = new Map(); +const actionMessages = new Map(); interface ZonedPopupOptions { zone: string; @@ -23,6 +25,12 @@ interface ZonedPopupOptions { popupOptions: Array; } +export interface ActionMessageOptions { + message: string; + type?: "message" | "warning"; + callback: () => void; +} + export class WorkAdventureUiCommands extends IframeApiContribution { callbacks = [ apiCallback({ @@ -50,6 +58,16 @@ export class WorkAdventureUiCommands extends IframeApiContribution { + const actionMessage = actionMessages.get(event.uuid); + if (actionMessage) { + actionMessage.triggerCallback(); + } + }, + }), ]; openPopup(targetObject: string, message: string, buttons: ButtonDescriptor[], input: boolean = false): Popup { @@ -105,6 +123,14 @@ export class WorkAdventureUiCommands extends IframeApiContribution { + actionMessages.delete(actionMessage.uuid); + }); + actionMessages.set(actionMessage.uuid, actionMessage); + return actionMessage; + } } export default new WorkAdventureUiCommands(); diff --git a/front/src/Api/iframe/website.ts b/front/src/Api/iframe/website.ts new file mode 100644 index 00000000..28abb19a --- /dev/null +++ b/front/src/Api/iframe/website.ts @@ -0,0 +1,38 @@ +import type { LoadSoundEvent } from "../Events/LoadSoundEvent"; +import type { PlaySoundEvent } from "../Events/PlaySoundEvent"; +import type { StopSoundEvent } from "../Events/StopSoundEvent"; +import { IframeApiContribution, queryWorkadventure, sendToWorkadventure } from "./IframeApiContribution"; +import { Sound } from "./Sound/Sound"; +import { EmbeddedWebsite } from "./Room/EmbeddedWebsite"; +import type { CreateEmbeddedWebsiteEvent } from "../Events/EmbeddedWebsiteEvent"; + +export class WorkadventureRoomWebsiteCommands extends IframeApiContribution { + callbacks = []; + + async get(objectName: string): Promise { + const websiteEvent = await queryWorkadventure({ + type: "getEmbeddedWebsite", + data: objectName, + }); + return new EmbeddedWebsite(websiteEvent); + } + + create(createEmbeddedWebsiteEvent: CreateEmbeddedWebsiteEvent): EmbeddedWebsite { + queryWorkadventure({ + type: "createEmbeddedWebsite", + data: createEmbeddedWebsiteEvent, + }).catch((e) => { + console.error(e); + }); + return new EmbeddedWebsite(createEmbeddedWebsiteEvent); + } + + async delete(objectName: string): Promise { + return await queryWorkadventure({ + type: "deleteEmbeddedWebsite", + data: objectName, + }); + } +} + +export default new WorkadventureRoomWebsiteCommands(); diff --git a/front/src/Components/App.svelte b/front/src/Components/App.svelte index 56f20e9a..ec644c93 100644 --- a/front/src/Components/App.svelte +++ b/front/src/Components/App.svelte @@ -33,6 +33,8 @@ import {textMessageVisibleStore} from "../Stores/TypeMessageStore/TextMessageStore"; import {warningContainerStore} from "../Stores/MenuStore"; import WarningContainer from "./WarningContainer/WarningContainer.svelte"; + import {layoutManagerVisibilityStore} from "../Stores/LayoutManagerStore"; + import LayoutManager from "./LayoutManager/LayoutManager.svelte"; export let game: Game; @@ -79,6 +81,11 @@ {/if} + {#if $layoutManagerVisibilityStore} +
+ +
+ {/if} {#if $gameOverlayVisibilityStore}
diff --git a/front/src/Components/LayoutManager/LayoutManager.svelte b/front/src/Components/LayoutManager/LayoutManager.svelte new file mode 100644 index 00000000..5bc6e097 --- /dev/null +++ b/front/src/Components/LayoutManager/LayoutManager.svelte @@ -0,0 +1,57 @@ + + + +
+ {#each $layoutManagerActionStore as action} +
onClick(action.callback)}> +

{action.message}

+
+ {/each} +
+ + + diff --git a/front/src/Phaser/Game/EmbeddedWebsiteManager.ts b/front/src/Phaser/Game/EmbeddedWebsiteManager.ts new file mode 100644 index 00000000..21a38ee5 --- /dev/null +++ b/front/src/Phaser/Game/EmbeddedWebsiteManager.ts @@ -0,0 +1,198 @@ +import type { GameScene } from "./GameScene"; +import { iframeListener } from "../../Api/IframeListener"; +import type { Subscription } from "rxjs"; +import type { CreateEmbeddedWebsiteEvent, ModifyEmbeddedWebsiteEvent } from "../../Api/Events/EmbeddedWebsiteEvent"; +import DOMElement = Phaser.GameObjects.DOMElement; + +type EmbeddedWebsite = CreateEmbeddedWebsiteEvent & { iframe: HTMLIFrameElement; phaserObject: DOMElement }; + +export class EmbeddedWebsiteManager { + private readonly embeddedWebsites = new Map(); + private readonly subscription: Subscription; + + constructor(private gameScene: GameScene) { + iframeListener.registerAnswerer("getEmbeddedWebsite", (name: string) => { + const website = this.embeddedWebsites.get(name); + if (website === undefined) { + throw new Error('Cannot find embedded website with name "' + name + '"'); + } + const rect = website.iframe.getBoundingClientRect(); + return { + url: website.url, + name: website.name, + visible: website.visible, + allowApi: website.allowApi, + allow: website.allow, + position: { + x: website.phaserObject.x, + y: website.phaserObject.y, + width: rect["width"], + height: rect["height"], + }, + }; + }); + + iframeListener.registerAnswerer("deleteEmbeddedWebsite", (name: string) => { + const website = this.embeddedWebsites.get(name); + if (!website) { + throw new Error('Could not find website to delete with the name "' + name + '" in your map'); + } + + website.iframe.remove(); + website.phaserObject.destroy(); + this.embeddedWebsites.delete(name); + }); + + iframeListener.registerAnswerer( + "createEmbeddedWebsite", + (createEmbeddedWebsiteEvent: CreateEmbeddedWebsiteEvent) => { + if (this.embeddedWebsites.has(createEmbeddedWebsiteEvent.name)) { + throw new Error('An embedded website with the name "' + name + '" already exists in your map'); + } + + this.createEmbeddedWebsite( + createEmbeddedWebsiteEvent.name, + createEmbeddedWebsiteEvent.url, + createEmbeddedWebsiteEvent.position.x, + createEmbeddedWebsiteEvent.position.y, + createEmbeddedWebsiteEvent.position.width, + createEmbeddedWebsiteEvent.position.height, + createEmbeddedWebsiteEvent.visible ?? true, + createEmbeddedWebsiteEvent.allowApi ?? false, + createEmbeddedWebsiteEvent.allow ?? "" + ); + } + ); + + this.subscription = iframeListener.modifyEmbeddedWebsiteStream.subscribe( + (embeddedWebsiteEvent: ModifyEmbeddedWebsiteEvent) => { + const website = this.embeddedWebsites.get(embeddedWebsiteEvent.name); + if (!website) { + throw new Error( + 'Could not find website with the name "' + embeddedWebsiteEvent.name + '" in your map' + ); + } + + gameScene.markDirty(); + + if (embeddedWebsiteEvent.url !== undefined) { + website.url = embeddedWebsiteEvent.url; + const absoluteUrl = new URL(embeddedWebsiteEvent.url, this.gameScene.MapUrlFile).toString(); + website.iframe.src = absoluteUrl; + } + + if (embeddedWebsiteEvent.visible !== undefined) { + website.visible = embeddedWebsiteEvent.visible; + website.phaserObject.visible = embeddedWebsiteEvent.visible; + } + + if (embeddedWebsiteEvent.allowApi !== undefined) { + website.allowApi = embeddedWebsiteEvent.allowApi; + if (embeddedWebsiteEvent.allowApi) { + iframeListener.registerIframe(website.iframe); + } else { + iframeListener.unregisterIframe(website.iframe); + } + } + + if (embeddedWebsiteEvent.allow !== undefined) { + website.allow = embeddedWebsiteEvent.allow; + website.iframe.allow = embeddedWebsiteEvent.allow; + } + + if (embeddedWebsiteEvent?.x !== undefined) { + website.phaserObject.x = embeddedWebsiteEvent.x; + } + if (embeddedWebsiteEvent?.y !== undefined) { + website.phaserObject.y = embeddedWebsiteEvent.y; + } + if (embeddedWebsiteEvent?.width !== undefined) { + website.iframe.style.width = embeddedWebsiteEvent.width + "px"; + } + if (embeddedWebsiteEvent?.height !== undefined) { + website.iframe.style.height = embeddedWebsiteEvent.height + "px"; + } + } + ); + } + + public createEmbeddedWebsite( + name: string, + url: string, + x: number, + y: number, + width: number, + height: number, + visible: boolean, + allowApi: boolean, + allow: string + ): void { + if (this.embeddedWebsites.has(name)) { + throw new Error('An embedded website with the name "' + name + '" already exists in your map'); + } + + const embeddedWebsiteEvent: CreateEmbeddedWebsiteEvent = { + name, + url, + /*x, + y, + width, + height,*/ + allow, + allowApi, + visible, + position: { + x, + y, + width, + height, + }, + }; + + const embeddedWebsite = this.doCreateEmbeddedWebsite(embeddedWebsiteEvent, visible); + + this.embeddedWebsites.set(name, embeddedWebsite); + } + + private doCreateEmbeddedWebsite( + embeddedWebsiteEvent: CreateEmbeddedWebsiteEvent, + visible: boolean + ): EmbeddedWebsite { + const absoluteUrl = new URL(embeddedWebsiteEvent.url, this.gameScene.MapUrlFile).toString(); + + const iframe = document.createElement("iframe"); + iframe.src = absoluteUrl; + iframe.style.width = embeddedWebsiteEvent.position.width + "px"; + iframe.style.height = embeddedWebsiteEvent.position.height + "px"; + iframe.style.margin = "0"; + iframe.style.padding = "0"; + iframe.style.border = "none"; + + const embeddedWebsite = { + ...embeddedWebsiteEvent, + phaserObject: this.gameScene.add + .dom(embeddedWebsiteEvent.position.x, embeddedWebsiteEvent.position.y, iframe) + .setVisible(visible) + .setOrigin(0, 0), + iframe: iframe, + }; + if (embeddedWebsiteEvent.allowApi) { + iframeListener.registerIframe(iframe); + } + + return embeddedWebsite; + } + + close(): void { + for (const [key, website] of this.embeddedWebsites) { + if (website.allowApi) { + iframeListener.unregisterIframe(website.iframe); + } + } + + this.subscription.unsubscribe(); + iframeListener.unregisterAnswerer("getEmbeddedWebsite"); + iframeListener.unregisterAnswerer("deleteEmbeddedWebsite"); + iframeListener.unregisterAnswerer("createEmbeddedWebsite"); + } +} diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 85f8eef1..5824cff8 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -20,7 +20,6 @@ import { AUDIO_VOLUME_PROPERTY, Box, JITSI_MESSAGE_PROPERTIES, - layoutManager, ON_ACTION_TRIGGER_BUTTON, TRIGGER_JITSI_PROPERTIES, TRIGGER_WEBSITE_PROPERTIES, @@ -85,8 +84,12 @@ import { biggestAvailableAreaStore } from "../../Stores/BiggestAvailableAreaStor import { SharedVariablesManager } from "./SharedVariablesManager"; import { playersStore } from "../../Stores/PlayersStore"; import { chatVisibilityStore } from "../../Stores/ChatStore"; +import { PropertyUtils } from "../Map/PropertyUtils"; import Tileset = Phaser.Tilemaps.Tileset; import { userIsAdminStore } from "../../Stores/GameStore"; +import { layoutManagerActionStore } from "../../Stores/LayoutManagerStore"; +import { get } from "svelte/store"; +import { EmbeddedWebsiteManager } from "./EmbeddedWebsiteManager"; export interface GameSceneInitInterface { initPosition: PointInterface | null; @@ -197,6 +200,8 @@ export class GameScene extends DirtyScene { private preloading: boolean = true; private startPositionCalculator!: StartPositionCalculator; private sharedVariablesManager!: SharedVariablesManager; + private objectsByType = new Map(); + private embeddedWebsiteManager!: EmbeddedWebsiteManager; constructor(private room: Room, MapUrlFile: string, customKey?: string | undefined) { super({ @@ -336,27 +341,27 @@ export class GameScene extends DirtyScene { }); // Scan the object layers for objects to load and load them. - const objects = new Map(); + this.objectsByType = new Map(); for (const layer of this.mapFile.layers) { if (layer.type === "objectgroup") { for (const object of layer.objects) { let objectsOfType: ITiledMapObject[] | undefined; - if (!objects.has(object.type)) { + if (!this.objectsByType.has(object.type)) { objectsOfType = new Array(); } else { - objectsOfType = objects.get(object.type); + objectsOfType = this.objectsByType.get(object.type); if (objectsOfType === undefined) { throw new Error("Unexpected object type not found"); } } objectsOfType.push(object); - objects.set(object.type, objectsOfType); + this.objectsByType.set(object.type, objectsOfType); } } } - for (const [itemType, objectsOfType] of objects) { + for (const [itemType, objectsOfType] of this.objectsByType) { // FIXME: we would ideally need for the loader to WAIT for the import to be performed, which means writing our own loader plugin. let itemFactory: ItemFactoryInterface; @@ -456,6 +461,8 @@ export class GameScene extends DirtyScene { //permit to set bound collision this.physics.world.setBounds(0, 0, this.Map.widthInPixels, this.Map.heightInPixels); + this.embeddedWebsiteManager = new EmbeddedWebsiteManager(this); + //add layer on map this.gameMap = new GameMap(this.mapFile, this.Map, this.Terrains); for (const layer of this.gameMap.flatLayers) { @@ -476,6 +483,28 @@ export class GameScene extends DirtyScene { if (object.text) { TextUtils.createTextFromITiledMapObject(this, object); } + if (object.type === "website") { + // Let's load iframes in the map + const url = PropertyUtils.mustFindStringProperty( + "url", + object.properties, + 'in the "' + object.name + '" object of type "website"' + ); + const allowApi = PropertyUtils.findBooleanProperty("allowApi", object.properties); + + // TODO: add a "allow" property to iframe + this.embeddedWebsiteManager.createEmbeddedWebsite( + object.name, + url, + object.x, + object.y, + object.width, + object.height, + object.visible, + allowApi ?? false, + "" + ); + } } } } @@ -791,7 +820,7 @@ export class GameScene extends DirtyScene { }); this.gameMap.onPropertyChange("openWebsite", (newValue, oldValue, allProps) => { if (newValue === undefined) { - layoutManager.removeActionButton("openWebsite", this.userInputManager); + layoutManagerActionStore.removeAction("openWebsite"); coWebsiteManager.closeCoWebsite(); } else { const openWebsiteFunction = () => { @@ -801,7 +830,7 @@ export class GameScene extends DirtyScene { allProps.get("openWebsiteAllowApi") as boolean | undefined, allProps.get("openWebsitePolicy") as string | undefined ); - layoutManager.removeActionButton("openWebsite", this.userInputManager); + layoutManagerActionStore.removeAction("openWebsite"); }; const openWebsiteTriggerValue = allProps.get(TRIGGER_WEBSITE_PROPERTIES); @@ -810,14 +839,13 @@ export class GameScene extends DirtyScene { if (message === undefined) { message = "Press SPACE or touch here to open web site"; } - layoutManager.addActionButton( - "openWebsite", - message.toString(), - () => { - openWebsiteFunction(); - }, - this.userInputManager - ); + layoutManagerActionStore.addAction({ + uuid: "openWebsite", + type: "message", + message: message, + callback: () => openWebsiteFunction(), + userInputManager: this.userInputManager, + }); } else { openWebsiteFunction(); } @@ -825,7 +853,7 @@ export class GameScene extends DirtyScene { }); this.gameMap.onPropertyChange("jitsiRoom", (newValue, oldValue, allProps) => { if (newValue === undefined) { - layoutManager.removeActionButton("jitsiRoom", this.userInputManager); + layoutManagerActionStore.removeAction("jitsi"); this.stopJitsi(); } else { const openJitsiRoomFunction = () => { @@ -838,7 +866,7 @@ export class GameScene extends DirtyScene { } else { this.startJitsi(roomName, undefined); } - layoutManager.removeActionButton("jitsiRoom", this.userInputManager); + layoutManagerActionStore.removeAction("jitsi"); }; const jitsiTriggerValue = allProps.get(TRIGGER_JITSI_PROPERTIES); @@ -847,14 +875,13 @@ export class GameScene extends DirtyScene { if (message === undefined) { message = "Press SPACE or touch here to enter Jitsi Meet room"; } - layoutManager.addActionButton( - "jitsiRoom", - message.toString(), - () => { - openJitsiRoomFunction(); - }, - this.userInputManager - ); + layoutManagerActionStore.addAction({ + uuid: "jitsi", + type: "message", + message: message, + callback: () => openJitsiRoomFunction(), + userInputManager: this.userInputManager, + }); } else { openJitsiRoomFunction(); } @@ -1183,6 +1210,44 @@ export class GameScene extends DirtyScene { }); }); }); + + iframeListener.registerAnswerer("triggerActionMessage", (message) => + layoutManagerActionStore.addAction({ + uuid: message.uuid, + type: "message", + message: message.message, + callback: () => { + layoutManagerActionStore.removeAction(message.uuid); + iframeListener.sendActionMessageTriggered(message.uuid); + }, + userInputManager: this.userInputManager, + }) + ); + + iframeListener.registerAnswerer("removeActionMessage", (message) => { + layoutManagerActionStore.removeAction(message.uuid); + }); + + this.iframeSubscriptionList.push( + iframeListener.modifyEmbeddedWebsiteStream.subscribe((embeddedWebsite) => { + // TODO + // TODO + // TODO + // TODO + // TODO + // TODO + // TODO + // TODO + // TODO + // TODO + // TODO + // TODO + // TODO + // TODO + // TODO + // TODO + }) + ); } private setPropertyLayer( @@ -1250,7 +1315,7 @@ export class GameScene extends DirtyScene { let targetRoom: Room; try { targetRoom = await Room.createRoom(roomUrl); - } catch (e) { + } catch (e /*: unknown*/) { console.error('Error while fetching new room "' + roomUrl.toString() + '"', e); this.mapTransitioning = false; return; @@ -1305,7 +1370,12 @@ export class GameScene extends DirtyScene { this.biggestAvailableAreaStoreUnsubscribe(); iframeListener.unregisterAnswerer("getState"); iframeListener.unregisterAnswerer("loadTileset"); + iframeListener.unregisterAnswerer("getMapData"); + iframeListener.unregisterAnswerer("getState"); + iframeListener.unregisterAnswerer("triggerActionMessage"); + iframeListener.unregisterAnswerer("removeActionMessage"); this.sharedVariablesManager?.close(); + this.embeddedWebsiteManager?.close(); mediaManager.hideGameOverlay(); @@ -1377,7 +1447,7 @@ export class GameScene extends DirtyScene { try { const room = await Room.createRoom(exitRoomPath); return gameManager.loadMap(room, this.scene); - } catch (e) { + } catch (e /*: unknown*/) { console.warn('Error while pre-loading exit room "' + exitRoomPath.toString() + '"', e); } } diff --git a/front/src/Phaser/Map/PropertyUtils.ts b/front/src/Phaser/Map/PropertyUtils.ts new file mode 100644 index 00000000..b9aff8d7 --- /dev/null +++ b/front/src/Phaser/Map/PropertyUtils.ts @@ -0,0 +1,53 @@ +import type { ITiledMapProperty } from "./ITiledMap"; + +export class PropertyUtils { + public static findProperty( + name: string, + properties: ITiledMapProperty[] | undefined + ): string | boolean | number | undefined { + return properties?.find((property) => property.name === name)?.value; + } + + public static findBooleanProperty( + name: string, + properties: ITiledMapProperty[] | undefined, + context?: string + ): boolean | undefined { + const property = PropertyUtils.findProperty(name, properties); + if (property === undefined) { + return undefined; + } + if (typeof property !== "boolean") { + throw new Error( + 'Expected property "' + name + '" to be a boolean. ' + (context ? " (" + context + ")" : "") + ); + } + return property; + } + + public static mustFindProperty( + name: string, + properties: ITiledMapProperty[] | undefined, + context?: string + ): string | boolean | number { + const property = PropertyUtils.findProperty(name, properties); + if (property === undefined) { + throw new Error('Could not find property "' + name + '"' + (context ? " (" + context + ")" : "")); + } + return property; + } + + public static mustFindStringProperty( + name: string, + properties: ITiledMapProperty[] | undefined, + context?: string + ): string { + const property = PropertyUtils.mustFindProperty(name, properties, context); + if (typeof property !== "string") { + throw new Error( + 'Expected property "' + name + '" to be a string. ' + (context ? " (" + context + ")" : "") + ); + } + return property; + } +} diff --git a/front/src/Stores/LayoutManagerStore.ts b/front/src/Stores/LayoutManagerStore.ts new file mode 100644 index 00000000..e92cd3c4 --- /dev/null +++ b/front/src/Stores/LayoutManagerStore.ts @@ -0,0 +1,56 @@ +import { derived, writable } from "svelte/store"; +import type { UserInputManager } from "../Phaser/UserInput/UserInputManager"; + +export interface LayoutManagerAction { + uuid: string; + type: "warning" | "message"; + message: string | number | boolean | undefined; + callback: () => void; + userInputManager: UserInputManager | undefined; +} + +function createLayoutManagerAction() { + const { subscribe, set, update } = writable([]); + + return { + subscribe, + addAction: (newAction: LayoutManagerAction): void => { + update((list: LayoutManagerAction[]) => { + let found = false; + for (const action of list) { + if (action.uuid === newAction.uuid) { + found = true; + } + } + + if (!found) { + list.push(newAction); + newAction.userInputManager?.addSpaceEventListner(newAction.callback); + } + + return list; + }); + }, + removeAction: (uuid: string): void => { + update((list: LayoutManagerAction[]) => { + const index = list.findIndex((action) => action.uuid === uuid); + + if (index !== -1) { + list[index].userInputManager?.removeSpaceEventListner(list[index].callback); + list.splice(index, 1); + } + + return list; + }); + }, + clearActions: (): void => { + set([]); + }, + }; +} + +export const layoutManagerActionStore = createLayoutManagerAction(); + +export const layoutManagerVisibilityStore = derived(layoutManagerActionStore, ($layoutManagerActionStore) => { + return !!$layoutManagerActionStore.length; +}); diff --git a/front/src/WebRtc/LayoutManager.ts b/front/src/WebRtc/LayoutManager.ts index fd3bfef5..0d9a4ba9 100644 --- a/front/src/WebRtc/LayoutManager.ts +++ b/front/src/WebRtc/LayoutManager.ts @@ -1,7 +1,3 @@ -import type { UserInputManager } from "../Phaser/UserInput/UserInputManager"; -import { HtmlUtils } from "./HtmlUtils"; -const sanitizeHtml = require('sanitize-html'); - export enum LayoutMode { // All videos are displayed on the right side of the screen. If there is a screen sharing, it is displayed in the middle. Presentation = "Presentation", @@ -28,87 +24,3 @@ export const AUDIO_VOLUME_PROPERTY = "audioVolume"; export const AUDIO_LOOP_PROPERTY = "audioLoop"; export type Box = { xStart: number; yStart: number; xEnd: number; yEnd: number }; - -class LayoutManager { - private actionButtonTrigger: Map = new Map(); - private actionButtonInformation: Map = new Map(); - - public addActionButton(id: string, text: string, callBack: Function, userInputManager: UserInputManager) { - //delete previous element - this.removeActionButton(id, userInputManager); - - //create div and text html component - const p = document.createElement('p'); - p.classList.add('action-body'); - p.classList.add('nes-btn'); - p.classList.add('is-dark'); - p.innerHTML = sanitizeHtml(text); - - const div = document.createElement("div"); - div.classList.add("action"); - div.id = id; - div.appendChild(p); - - this.actionButtonInformation.set(id, div); - - const mainContainer = HtmlUtils.getElementByIdOrFail("main-container"); - mainContainer.appendChild(div); - - //add trigger action - div.onpointerdown = () => callBack(); - this.actionButtonTrigger.set(id, callBack); - userInputManager.addSpaceEventListner(callBack); - } - - public removeActionButton(id: string, userInputManager?: UserInputManager) { - //delete previous element - const previousDiv = this.actionButtonInformation.get(id); - if (previousDiv) { - previousDiv.remove(); - this.actionButtonInformation.delete(id); - } - const previousEventCallback = this.actionButtonTrigger.get(id); - if (previousEventCallback && userInputManager) { - userInputManager.removeSpaceEventListner(previousEventCallback); - } - } - - public addInformation(id: string, text: string, callBack?: Function, userInputManager?: UserInputManager) { - //delete previous element - for (const [key, value] of this.actionButtonInformation) { - this.removeActionButton(key, userInputManager); - } - - //create div and text html component - const p = document.createElement("p"); - p.classList.add("action-body"); - p.innerText = text; - - const div = document.createElement("div"); - div.classList.add("action"); - div.classList.add(id); - div.id = id; - div.appendChild(p); - - this.actionButtonInformation.set(id, div); - - const mainContainer = HtmlUtils.getElementByIdOrFail("main-container"); - mainContainer.appendChild(div); - //add trigger action - if (callBack) { - div.onpointerdown = () => { - callBack(); - this.removeActionButton(id, userInputManager); - }; - } - - //remove it after 10 sec - setTimeout(() => { - this.removeActionButton(id, userInputManager); - }, 10000); - } -} - -const layoutManager = new LayoutManager(); - -export { layoutManager }; diff --git a/front/src/WebRtc/MediaManager.ts b/front/src/WebRtc/MediaManager.ts index d7e9f514..a7a73ecb 100644 --- a/front/src/WebRtc/MediaManager.ts +++ b/front/src/WebRtc/MediaManager.ts @@ -1,4 +1,3 @@ -import { layoutManager } from "./LayoutManager"; import { HtmlUtils } from "./HtmlUtils"; import type { UserInputManager } from "../Phaser/UserInput/UserInputManager"; import { localStreamStore } from "../Stores/MediaStore"; @@ -10,6 +9,8 @@ export type StopScreenSharingCallback = (media: MediaStream) => void; import { cowebsiteCloseButtonId } from "./CoWebsiteManager"; import { gameOverlayVisibilityStore } from "../Stores/GameOverlayStoreVisibility"; +import { layoutManagerActionStore, layoutManagerVisibilityStore } from "../Stores/LayoutManagerStore"; +import { get } from "svelte/store"; export class MediaManager { startScreenSharingCallBacks: Set = new Set(); @@ -23,14 +24,19 @@ export class MediaManager { localStreamStore.subscribe((result) => { if (result.type === "error") { console.error(result.error); - layoutManager.addInformation( - "warning", - "Camera access denied. Click here and check your browser permissions.", - () => { + layoutManagerActionStore.addAction({ + uuid: "cameraAccessDenied", + type: "warning", + message: "Camera access denied. Click here and check your browser permissions.", + callback: () => { helpCameraSettingsVisibleStore.set(true); }, - this.userInputManager - ); + userInputManager: this.userInputManager, + }); + //remove it after 10 sec + setTimeout(() => { + layoutManagerActionStore.removeAction("cameraAccessDenied"); + }, 10000); return; } }); @@ -38,14 +44,19 @@ export class MediaManager { screenSharingLocalStreamStore.subscribe((result) => { if (result.type === "error") { console.error(result.error); - layoutManager.addInformation( - "warning", - "Screen sharing denied. Click here and check your browser permissions.", - () => { + layoutManagerActionStore.addAction({ + uuid: "screenSharingAccessDenied", + type: "warning", + message: "Screen sharing denied. Click here and check your browser permissions.", + callback: () => { helpCameraSettingsVisibleStore.set(true); }, - this.userInputManager - ); + userInputManager: this.userInputManager, + }); + //remove it after 10 sec + setTimeout(() => { + layoutManagerActionStore.removeAction("screenSharingAccessDenied"); + }, 10000); return; } }); diff --git a/front/style/style.scss b/front/style/style.scss index 0a48358b..6cafb81d 100644 --- a/front/style/style.scss +++ b/front/style/style.scss @@ -380,6 +380,10 @@ body { #game { position: relative; /* Position relative is needed for the game-overlay. */ + + iframe { + pointer-events: all; + } } .audioplayer:first-child { diff --git a/maps/tests/EmbeddedWebsite/integrated_website_1.html b/maps/tests/EmbeddedWebsite/integrated_website_1.html new file mode 100644 index 00000000..e85f5e78 --- /dev/null +++ b/maps/tests/EmbeddedWebsite/integrated_website_1.html @@ -0,0 +1,10 @@ + + + + +

Hello world

+

This is a webpage integrated in your map

+ +

Here is a form, right in your map!

+ + diff --git a/maps/tests/EmbeddedWebsite/website_in_map.json b/maps/tests/EmbeddedWebsite/website_in_map.json new file mode 100644 index 00000000..59ada784 --- /dev/null +++ b/maps/tests/EmbeddedWebsite/website_in_map.json @@ -0,0 +1,99 @@ +{ "compressionlevel":-1, + "height":30, + "infinite":false, + "layers":[ + { + "data":[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + "height":30, + "id":1, + "name":"floor", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":30, + "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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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":30, + "id":2, + "name":"start", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":30, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":3, + "name":"floorLayer", + "objects":[ + { + "height":83.6666666666666, + "id":1, + "name":"", + "rotation":0, + "text": + { + "fontfamily":"Sans Serif", + "pixelsize":13, + "text":"Test:\nWalk around the map\nResult:\nYou should see a transparent website at the center of the map, that is \"fixed\" on the map floor.", + "wrap":true + }, + "type":"", + "visible":true, + "width":315.4375, + "x":68.4021076998051, + "y":8.73391812865529 + }, + { + "height":419.805068226121, + "id":2, + "name":"demo website", + "properties":[ + { + "name":"url", + "type":"string", + "value":"integrated_website_1.html" + }], + "rotation":0, + "type":"website", + "visible":true, + "width":637.504873294347, + "x":189.005847953216, + "y":156.569200779727 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":6, + "nextobjectid":3, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"2021.03.23", + "tileheight":32, + "tilesets":[ + { + "columns":11, + "firstgid":1, + "image":"..\/tileset1.png", + "imageheight":352, + "imagewidth":352, + "margin":0, + "name":"tileset1", + "spacing":0, + "tilecount":121, + "tileheight":32, + "tilewidth":32 + }], + "tilewidth":32, + "type":"map", + "version":1.5, + "width":30 +} \ No newline at end of file diff --git a/maps/tests/EmbeddedWebsite/website_in_map_script.html b/maps/tests/EmbeddedWebsite/website_in_map_script.html new file mode 100644 index 00000000..1180a2de --- /dev/null +++ b/maps/tests/EmbeddedWebsite/website_in_map_script.html @@ -0,0 +1,85 @@ + + + + + + +X:
+Y:
+width:
+height:
+URL:
+Visible:
+ + + + + + + diff --git a/maps/tests/EmbeddedWebsite/website_in_map_script.json b/maps/tests/EmbeddedWebsite/website_in_map_script.json new file mode 100644 index 00000000..00ce95cb --- /dev/null +++ b/maps/tests/EmbeddedWebsite/website_in_map_script.json @@ -0,0 +1,93 @@ +{ "compressionlevel":-1, + "height":30, + "infinite":false, + "layers":[ + { + "data":[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + "height":30, + "id":1, + "name":"floor", + "opacity":1, + "properties":[ + { + "name":"openWebsite", + "type":"string", + "value":"website_in_map_script.html" + }, + { + "name":"openWebsiteAllowApi", + "type":"bool", + "value":true + }], + "type":"tilelayer", + "visible":true, + "width":30, + "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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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":30, + "id":2, + "name":"start", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":30, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":3, + "name":"floorLayer", + "objects":[ + { + "height":393, + "id":1, + "name":"", + "rotation":0, + "text": + { + "fontfamily":"Sans Serif", + "pixelsize":13, + "text":"Test:\nClick the \"create\" button.\n\nResult:\nA website should appear.\n\nTest:\nUse the fields to modify settings.\n\nResult:\nThe iframe is modified accordingly.\n\nTest:\nClick the \"delete\" button.\n\nResult:\nThe iframe is deleted\n\nTest:\nClick the \"create\" button.\n\nResult:\nA website should appear.\n", + "wrap":true + }, + "type":"", + "visible":true, + "width":315.4375, + "x":68.4021076998051, + "y":8.73391812865529 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":6, + "nextobjectid":3, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"2021.03.23", + "tileheight":32, + "tilesets":[ + { + "columns":11, + "firstgid":1, + "image":"..\/tileset1.png", + "imageheight":352, + "imagewidth":352, + "margin":0, + "name":"tileset1", + "spacing":0, + "tilecount":121, + "tileheight":32, + "tilewidth":32 + }], + "tilewidth":32, + "type":"map", + "version":1.5, + "width":30 +} \ No newline at end of file diff --git a/maps/tests/TriggerMessageApi/script.js b/maps/tests/TriggerMessageApi/script.js new file mode 100644 index 00000000..9ab02ec2 --- /dev/null +++ b/maps/tests/TriggerMessageApi/script.js @@ -0,0 +1,16 @@ +WA.onInit().then(() => { + let message; + + WA.room.onEnterZone("carpet", () => { + message = WA.ui.displayActionMessage({ + message: "This is a test message. Press space to display a chat message. Walk out to hide the message.", + callback: () => { + WA.chat.sendChatMessage("Hello world!", "The bot"); + } + }); + }); + + WA.room.onLeaveZone("carpet", () => { + message && message.remove(); + }); +}); diff --git a/maps/tests/TriggerMessageApi/triggerMessage.json b/maps/tests/TriggerMessageApi/triggerMessage.json new file mode 100644 index 00000000..1e077741 --- /dev/null +++ b/maps/tests/TriggerMessageApi/triggerMessage.json @@ -0,0 +1,106 @@ +{ "compressionlevel":-1, + "height":10, + "infinite":false, + "layers":[ + { + "data":[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + "height":10, + "id":1, + "name":"floor", + "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, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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":2, + "name":"start", + "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, 34, 34, 0, 0, 0, 0, 0, 0, 0, 0, 34, 34, 0, 0, 0, 0, 0, 0, 0, 0, 34, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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":8, + "name":"carpet", + "opacity":1, + "properties":[ + { + "name":"zone", + "type":"string", + "value":"carpet" + }], + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":3, + "name":"floorLayer", + "objects":[ + { + "height":304.037037037037, + "id":3, + "name":"", + "rotation":0, + "text": + { + "fontfamily":"Sans Serif", + "pixelsize":11, + "text":"Test:\nWalk on the carpet\n\nResult:\nA message is displayed at the bottom of the screen\n\nTest:\nPress space\n\nResult:\nA chat message is displayed\n\n\nTest:\nWalk out of the carpet\n\nResult:\nThe message is hidden\n", + "wrap":true + }, + "type":"", + "visible":true, + "width":252.4375, + "x":2.78125, + "y":2.5 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":9, + "nextobjectid":11, + "orientation":"orthogonal", + "properties":[ + { + "name":"script", + "type":"string", + "value":"script.js" + }], + "renderorder":"right-down", + "tiledversion":"2021.03.23", + "tileheight":32, + "tilesets":[ + { + "columns":11, + "firstgid":1, + "image":"..\/tileset1.png", + "imageheight":352, + "imagewidth":352, + "margin":0, + "name":"tileset1", + "spacing":0, + "tilecount":121, + "tileheight":32, + "tilewidth":32 + }], + "tilewidth":32, + "type":"map", + "version":1.5, + "width":10 +} \ No newline at end of file diff --git a/maps/tests/index.html b/maps/tests/index.html index fbff09e5..332875b9 100644 --- a/maps/tests/index.html +++ b/maps/tests/index.html @@ -202,6 +202,30 @@ Testing shared scripting variables + + + Success Failure Pending + + + Testing trigger message API + + + + + Success Failure Pending + + + Testing websites inside a map + + + + + Success Failure Pending + + + Testing scripting API for websites inside a map + +