diff --git a/back/src/Model/GameRoom.ts b/back/src/Model/GameRoom.ts index e907fcb7..5667146a 100644 --- a/back/src/Model/GameRoom.ts +++ b/back/src/Model/GameRoom.ts @@ -473,7 +473,22 @@ export class GameRoom { const variablesManager = new VariablesManager(this.roomUrl, null); return variablesManager.init(); } else { - throw e; + // An error occurred while loading the map + // Right now, let's bypass the error. In the future, we should make sure the user is aware of that + // and that he/she will act on it to fix the problem. + + // Note: we run this message inside a setTimeout so that the room listeners can have time to connect. + setTimeout(() => { + for (const roomListener of this.roomListeners) { + emitErrorOnRoomSocket( + roomListener, + "Your map does not seem accessible from the WorkAdventure servers. Is it behind a firewall or a proxy? Your map should be accessible from the WorkAdventure servers. If you use the scripting API in this map, please be aware that server-side checks and variable persistence is disabled." + ); + } + }, 1000); + + const variablesManager = new VariablesManager(this.roomUrl, null); + return variablesManager.init(); } }); } 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/dist/resources/service-worker.js b/front/dist/service-worker.js similarity index 97% rename from front/dist/resources/service-worker.js rename to front/dist/service-worker.js index e496f7fc..92c381ea 100644 --- a/front/dist/resources/service-worker.js +++ b/front/dist/service-worker.js @@ -8,7 +8,6 @@ self.addEventListener('install', function(event) { event.waitUntil( caches.open(CACHE_NAME) .then(function(cache) { - console.log('Opened cache'); return cache.addAll(urlsToCache); }) ); diff --git a/front/dist/static/images/favicons/apple-icon-114x114.png b/front/dist/static/images/favicons/apple-icon-114x114.png index 6c994217..3de5f8c4 100644 Binary files a/front/dist/static/images/favicons/apple-icon-114x114.png and b/front/dist/static/images/favicons/apple-icon-114x114.png differ diff --git a/front/dist/static/images/favicons/apple-icon-120x120.png b/front/dist/static/images/favicons/apple-icon-120x120.png index b20bf1e2..90ff4b18 100644 Binary files a/front/dist/static/images/favicons/apple-icon-120x120.png and b/front/dist/static/images/favicons/apple-icon-120x120.png differ diff --git a/front/dist/static/images/favicons/apple-icon-144x144.png b/front/dist/static/images/favicons/apple-icon-144x144.png index 59a7c4ce..1fc655bb 100644 Binary files a/front/dist/static/images/favicons/apple-icon-144x144.png and b/front/dist/static/images/favicons/apple-icon-144x144.png differ diff --git a/front/dist/static/images/favicons/apple-icon-152x152.png b/front/dist/static/images/favicons/apple-icon-152x152.png index fc905367..ca3a85ba 100644 Binary files a/front/dist/static/images/favicons/apple-icon-152x152.png and b/front/dist/static/images/favicons/apple-icon-152x152.png differ diff --git a/front/dist/static/images/favicons/apple-icon-180x180.png b/front/dist/static/images/favicons/apple-icon-180x180.png index 8f18bd51..64190d74 100644 Binary files a/front/dist/static/images/favicons/apple-icon-180x180.png and b/front/dist/static/images/favicons/apple-icon-180x180.png differ diff --git a/front/dist/static/images/favicons/apple-icon-57x57.png b/front/dist/static/images/favicons/apple-icon-57x57.png index 62c8ab07..747c5c11 100644 Binary files a/front/dist/static/images/favicons/apple-icon-57x57.png and b/front/dist/static/images/favicons/apple-icon-57x57.png differ diff --git a/front/dist/static/images/favicons/apple-icon-60x60.png b/front/dist/static/images/favicons/apple-icon-60x60.png index 68c5b74b..61a9c874 100644 Binary files a/front/dist/static/images/favicons/apple-icon-60x60.png and b/front/dist/static/images/favicons/apple-icon-60x60.png differ diff --git a/front/dist/static/images/favicons/apple-icon-72x72.png b/front/dist/static/images/favicons/apple-icon-72x72.png index b04f1b25..da043d44 100644 Binary files a/front/dist/static/images/favicons/apple-icon-72x72.png and b/front/dist/static/images/favicons/apple-icon-72x72.png differ diff --git a/front/dist/static/images/favicons/apple-icon-76x76.png b/front/dist/static/images/favicons/apple-icon-76x76.png index a58cf3ed..5e51fe2b 100644 Binary files a/front/dist/static/images/favicons/apple-icon-76x76.png and b/front/dist/static/images/favicons/apple-icon-76x76.png differ diff --git a/front/dist/static/images/favicons/apple-icon.png b/front/dist/static/images/favicons/apple-icon.png index e1a4b3ed..69d255b1 100644 Binary files a/front/dist/static/images/favicons/apple-icon.png and b/front/dist/static/images/favicons/apple-icon.png differ diff --git a/front/dist/static/images/favicons/icon-512x512.png b/front/dist/static/images/favicons/icon-512x512.png index 86cb7477..41116386 100644 Binary files a/front/dist/static/images/favicons/icon-512x512.png and b/front/dist/static/images/favicons/icon-512x512.png differ diff --git a/front/dist/static/images/favicons/manifest.json b/front/dist/static/images/favicons/manifest.json index 30d08769..1cf2a835 100644 --- a/front/dist/static/images/favicons/manifest.json +++ b/front/dist/static/images/favicons/manifest.json @@ -48,41 +48,10 @@ "type": "image\/png" }, { - "src": "/static/images/favicons/android-icon-36x36.png", - "sizes": "36x36", + "src": "/static/images/favicons/apple-icon.png", + "sizes": "192x192", "type": "image\/png", - "density": "0.75" - }, - { - "src": "/static/images/favicons/android-icon-48x48.png", - "sizes": "48x48", - "type": "image\/png", - "density": "1.0" - }, - { - "src": "/static/images/favicons/android-icon-72x72.png", - "sizes": "72x72", - "type": "image\/png", - "density": "1.5" - }, - - { - "src": "/static/images/favicons/favicon-16x16.png", - "sizes": "16x16", - "type": "image\/png", - "density": "1" - }, - { - "src": "/static/images/favicons/favicon-32x32.png", - "sizes": "32x32", - "type": "image\/png", - "density": "1.5" - }, - { - "src": "/static/images/favicons/favicon-96x96.png", - "sizes": "96x96", - "type": "image\/png", - "density": "2.0" + "purpose": "any" }, { @@ -122,6 +91,25 @@ "density": "4.0", "purpose": "any maskable" }, + + { + "src": "/static/images/favicons/favicon-16x16.png", + "sizes": "16x16", + "type": "image\/png", + "density": "1" + }, + { + "src": "/static/images/favicons/favicon-32x32.png", + "sizes": "32x32", + "type": "image\/png", + "density": "1.5" + }, + { + "src": "/static/images/favicons/favicon-96x96.png", + "sizes": "96x96", + "type": "image\/png", + "density": "2.0" + }, { "src": "/static/images/favicons/icon-512x512.png", "sizes": "512x512", @@ -132,6 +120,7 @@ "background_color": "#000000", "display_override": ["window-control-overlay", "minimal-ui"], "display": "standalone", + "orientation": "portrait-primary", "scope": "/", "lang": "en", "theme_color": "#000000", 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..f7da0ad2 --- /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 d9286ef0..4dde1b7d 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"], @@ -108,6 +111,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(); @@ -121,7 +127,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). @@ -263,6 +269,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); } } }, @@ -416,6 +424,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({ @@ -49,6 +57,16 @@ export class WorkAdventureUiCommands extends IframeApiContribution { + const actionMessage = actionMessages.get(event.uuid); + if (actionMessage) { + actionMessage.triggerCallback(); + } + }, + }), ]; openPopup(targetObject: string, message: string, buttons: ButtonDescriptor[]): Popup { @@ -103,6 +121,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 be7afc3c..d896cece 100644 --- a/front/src/Components/App.svelte +++ b/front/src/Components/App.svelte @@ -36,6 +36,8 @@ import WarningContainer from "./WarningContainer/WarningContainer.svelte"; import {layoutManagerVisibilityStore} from "../Stores/LayoutManagerStore"; import LayoutManager from "./LayoutManager/LayoutManager.svelte"; + import {audioManagerVisibilityStore} from "../Stores/AudioManagerStore"; + import AudioManager from "./AudioManager/AudioManager.svelte" export let game: Game; @@ -82,6 +84,11 @@ {/if} + {#if $audioManagerVisibilityStore} +
+ +
+ {/if} {#if $layoutManagerVisibilityStore}
diff --git a/front/src/Components/AudioManager/AudioManager.svelte b/front/src/Components/AudioManager/AudioManager.svelte new file mode 100644 index 00000000..a78b4bde --- /dev/null +++ b/front/src/Components/AudioManager/AudioManager.svelte @@ -0,0 +1,119 @@ + + + +
+
+ player volume + +
+
+ +
+ +
+
+
+ + + diff --git a/front/src/Components/LayoutManager/LayoutManager.svelte b/front/src/Components/LayoutManager/LayoutManager.svelte index ef90a4e3..5bc6e097 100644 --- a/front/src/Components/LayoutManager/LayoutManager.svelte +++ b/front/src/Components/LayoutManager/LayoutManager.svelte @@ -1,26 +1,5 @@ + + +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 + +