diff --git a/deeployer.libsonnet b/deeployer.libsonnet
index 07f5f491..52cea293 100644
--- a/deeployer.libsonnet
+++ b/deeployer.libsonnet
@@ -88,8 +88,6 @@
"JITSI_URL": env.JITSI_URL,
"SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
"TURN_SERVER": "turn:coturn.workadventu.re:443,turns:coturn.workadventu.re:443",
- "TURN_USER": "workadventure",
- "TURN_PASSWORD": "WorkAdventure123",
"JITSI_PRIVATE_MODE": if env.SECRET_JITSI_KEY != '' then "true" else "false",
"START_ROOM_URL": "/_/global/maps."+url+"/Floor0/floor0.json"
//"GA_TRACKING_ID": "UA-10196481-11"
diff --git a/docker-compose.yaml b/docker-compose.yaml
index fdb74d6a..9ea637a3 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -43,7 +43,7 @@ services:
- ./front:/usr/src/app
labels:
- "traefik.http.routers.front.rule=Host(`play.workadventure.localhost`)"
- - "traefik.http.routers.front.entryPoints=web,traefik"
+ - "traefik.http.routers.front.entryPoints=web"
- "traefik.http.services.front.loadbalancer.server.port=8080"
- "traefik.http.routers.front-ssl.rule=Host(`play.workadventure.localhost`)"
- "traefik.http.routers.front-ssl.entryPoints=websecure"
diff --git a/front/Dockerfile b/front/Dockerfile
index b0d17877..51734535 100644
--- a/front/Dockerfile
+++ b/front/Dockerfile
@@ -8,6 +8,11 @@ FROM thecodingmachine/nodejs:14-apache
COPY --chown=docker:docker front .
COPY --from=builder --chown=docker:docker /var/www/messages/generated /var/www/html/src/Messages/generated
+
+# Removing the iframe.html file from the final image as this adds a XSS attack.
+# iframe.html is only in dev mode to circumvent a limitation
+RUN rm dist/iframe.html
+
RUN yarn install
ENV NODE_ENV=production
diff --git a/front/dist/.gitignore b/front/dist/.gitignore
index 05c474ec..785f2eb9 100644
--- a/front/dist/.gitignore
+++ b/front/dist/.gitignore
@@ -1,3 +1,4 @@
index.html
index.tmpl.html.tmp
-style.*.css
\ No newline at end of file
+/js/
+style.*.css
diff --git a/front/dist/iframe.html b/front/dist/iframe.html
new file mode 100644
index 00000000..c8fafb4b
--- /dev/null
+++ b/front/dist/iframe.html
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
diff --git a/front/dist/index.tmpl.html b/front/dist/index.tmpl.html
index 55adcebc..88efa80d 100644
--- a/front/dist/index.tmpl.html
+++ b/front/dist/index.tmpl.html
@@ -29,6 +29,9 @@
+
+
+
WorkAdventure
diff --git a/front/dist/resources/style/style.css b/front/dist/resources/style/style.css
index fda0c9dd..ba5b6c07 100644
--- a/front/dist/resources/style/style.css
+++ b/front/dist/resources/style/style.css
@@ -1123,6 +1123,31 @@ div.action p.action-body{
margin-left: calc(50% - 75px);
border-radius: 15px;
}
+.popUpElement{
+ font-family: 'Press Start 2P';
+ text-align: left;
+ color: white;
+}
+.popUpElement div {
+ font-family: 'Press Start 2P';
+ font-size: 10px;
+ background-color: #727678;
+}
+
+.popUpElement button {
+ position: relative;
+ font-size: 10px;
+ border-image-repeat: revert;
+ margin-right: 5px;
+}
+
+.popUpElement .buttonContainer {
+ float: right;
+ background-color: inherit;
+
+}
+
+
@keyframes mymove {
0% {bottom: 40px;}
50% {bottom: 30px;}
diff --git a/front/src/Administration/ConsoleGlobalMessageManager.ts b/front/src/Administration/ConsoleGlobalMessageManager.ts
index 7800e332..3aed39ac 100644
--- a/front/src/Administration/ConsoleGlobalMessageManager.ts
+++ b/front/src/Administration/ConsoleGlobalMessageManager.ts
@@ -336,7 +336,7 @@ export class ConsoleGlobalMessageManager {
}
active(){
- this.userInputManager.clearAllKeys();
+ this.userInputManager.disableControls();
this.divMainConsole.style.top = '0';
this.activeConsole = true;
}
diff --git a/front/src/Api/Events/ButtonClickedEvent.ts b/front/src/Api/Events/ButtonClickedEvent.ts
new file mode 100644
index 00000000..de807037
--- /dev/null
+++ b/front/src/Api/Events/ButtonClickedEvent.ts
@@ -0,0 +1,11 @@
+import * as tg from "generic-type-guard";
+
+export const isButtonClickedEvent =
+ new tg.IsInterface().withProperties({
+ popupId: tg.isNumber,
+ buttonId: tg.isNumber,
+ }).get();
+/**
+ * A message sent from the game to the iFrame when a user enters or leaves a zone marked with the "zone" property.
+ */
+export type ButtonClickedEvent = tg.GuardedType;
diff --git a/front/src/Api/Events/ChatEvent.ts b/front/src/Api/Events/ChatEvent.ts
new file mode 100644
index 00000000..5729a120
--- /dev/null
+++ b/front/src/Api/Events/ChatEvent.ts
@@ -0,0 +1,11 @@
+import * as tg from "generic-type-guard";
+
+export const isChatEvent =
+ new tg.IsInterface().withProperties({
+ message: tg.isString,
+ author: tg.isString,
+ }).get();
+/**
+ * A message sent from the iFrame to the game to add a message in the chat.
+ */
+export type ChatEvent = tg.GuardedType;
diff --git a/front/src/Api/Events/ClosePopupEvent.ts b/front/src/Api/Events/ClosePopupEvent.ts
new file mode 100644
index 00000000..83b09c96
--- /dev/null
+++ b/front/src/Api/Events/ClosePopupEvent.ts
@@ -0,0 +1,11 @@
+import * as tg from "generic-type-guard";
+
+export const isClosePopupEvent =
+ new tg.IsInterface().withProperties({
+ popupId: tg.isNumber,
+ }).get();
+
+/**
+ * A message sent from the iFrame to the game to add a message in the chat.
+ */
+export type ClosePopupEvent = tg.GuardedType;
diff --git a/front/src/Api/Events/EnterLeaveEvent.ts b/front/src/Api/Events/EnterLeaveEvent.ts
new file mode 100644
index 00000000..0c0cb4ff
--- /dev/null
+++ b/front/src/Api/Events/EnterLeaveEvent.ts
@@ -0,0 +1,10 @@
+import * as tg from "generic-type-guard";
+
+export const isEnterLeaveEvent =
+ new tg.IsInterface().withProperties({
+ name: tg.isString,
+ }).get();
+/**
+ * A message sent from the game to the iFrame when a user enters or leaves a zone marked with the "zone" property.
+ */
+export type EnterLeaveEvent = tg.GuardedType;
diff --git a/front/src/Api/Events/GoToPageEvent.ts b/front/src/Api/Events/GoToPageEvent.ts
new file mode 100644
index 00000000..cb258b03
--- /dev/null
+++ b/front/src/Api/Events/GoToPageEvent.ts
@@ -0,0 +1,13 @@
+import * as tg from "generic-type-guard";
+
+
+
+export const isGoToPageEvent =
+ new tg.IsInterface().withProperties({
+ url: tg.isString,
+ }).get();
+
+/**
+ * A message sent from the iFrame to the game to add a message in the chat.
+ */
+export type GoToPageEvent = tg.GuardedType;
diff --git a/front/src/Api/Events/IframeEvent.ts b/front/src/Api/Events/IframeEvent.ts
new file mode 100644
index 00000000..883f50fc
--- /dev/null
+++ b/front/src/Api/Events/IframeEvent.ts
@@ -0,0 +1,7 @@
+export interface IframeEvent {
+ type: string;
+ data: unknown;
+}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export const isIframeEventWrapper = (event: any): event is IframeEvent => typeof event.type === 'string';
diff --git a/front/src/Api/Events/OpenCoWebSiteEvent.ts b/front/src/Api/Events/OpenCoWebSiteEvent.ts
new file mode 100644
index 00000000..0fbc0ce2
--- /dev/null
+++ b/front/src/Api/Events/OpenCoWebSiteEvent.ts
@@ -0,0 +1,13 @@
+import * as tg from "generic-type-guard";
+
+
+
+export const isOpenCoWebsite =
+ new tg.IsInterface().withProperties({
+ url: tg.isString,
+ }).get();
+
+/**
+ * A message sent from the iFrame to the game to add a message in the chat.
+ */
+export type OpenCoWebSiteEvent = tg.GuardedType;
diff --git a/front/src/Api/Events/OpenPopupEvent.ts b/front/src/Api/Events/OpenPopupEvent.ts
new file mode 100644
index 00000000..094ba555
--- /dev/null
+++ b/front/src/Api/Events/OpenPopupEvent.ts
@@ -0,0 +1,20 @@
+import * as tg from "generic-type-guard";
+
+const isButtonDescriptor =
+ new tg.IsInterface().withProperties({
+ label: tg.isString,
+ className: tg.isOptional(tg.isString)
+ }).get();
+
+export const isOpenPopupEvent =
+ new tg.IsInterface().withProperties({
+ popupId: tg.isNumber,
+ targetObject: tg.isString,
+ message: tg.isString,
+ buttons: tg.isArray(isButtonDescriptor)
+ }).get();
+
+/**
+ * A message sent from the iFrame to the game to add a message in the chat.
+ */
+export type OpenPopupEvent = tg.GuardedType;
diff --git a/front/src/Api/Events/OpenTabEvent.ts b/front/src/Api/Events/OpenTabEvent.ts
new file mode 100644
index 00000000..e510f8b6
--- /dev/null
+++ b/front/src/Api/Events/OpenTabEvent.ts
@@ -0,0 +1,13 @@
+import * as tg from "generic-type-guard";
+
+
+
+export const isOpenTabEvent =
+ new tg.IsInterface().withProperties({
+ url: tg.isString,
+ }).get();
+
+/**
+ * A message sent from the iFrame to the game to add a message in the chat.
+ */
+export type OpenTabEvent = tg.GuardedType;
diff --git a/front/src/Api/Events/UserInputChatEvent.ts b/front/src/Api/Events/UserInputChatEvent.ts
new file mode 100644
index 00000000..de21ff6e
--- /dev/null
+++ b/front/src/Api/Events/UserInputChatEvent.ts
@@ -0,0 +1,10 @@
+import * as tg from "generic-type-guard";
+
+export const isUserInputChatEvent =
+ new tg.IsInterface().withProperties({
+ message: tg.isString,
+ }).get();
+/**
+ * A message sent from the game to the iFrame when a user types a message in the chat.
+ */
+export type UserInputChatEvent = tg.GuardedType;
diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts
new file mode 100644
index 00000000..c875ebbb
--- /dev/null
+++ b/front/src/Api/IframeListener.ts
@@ -0,0 +1,238 @@
+import {Subject} from "rxjs";
+import {ChatEvent, isChatEvent} from "./Events/ChatEvent";
+import {IframeEvent, isIframeEventWrapper} from "./Events/IframeEvent";
+import {UserInputChatEvent} from "./Events/UserInputChatEvent";
+import * as crypto from "crypto";
+import {HtmlUtils} from "../WebRtc/HtmlUtils";
+import {EnterLeaveEvent} from "./Events/EnterLeaveEvent";
+import {isOpenPopupEvent, OpenPopupEvent} from "./Events/OpenPopupEvent";
+import {isOpenTabEvent, OpenTabEvent} from "./Events/OpenTabEvent";
+import {ButtonClickedEvent} from "./Events/ButtonClickedEvent";
+import {ClosePopupEvent, isClosePopupEvent} from "./Events/ClosePopupEvent";
+import {scriptUtils} from "./ScriptUtils";
+import {GoToPageEvent, isGoToPageEvent} from "./Events/GoToPageEvent";
+import {isOpenCoWebsite, OpenCoWebSiteEvent} from "./Events/OpenCoWebSiteEvent";
+
+
+/**
+ * Listens to messages from iframes and turn those messages into easy to use observables.
+ * Also allows to send messages to those iframes.
+ */
+class IframeListener {
+ private readonly _chatStream: Subject = new Subject();
+ public readonly chatStream = this._chatStream.asObservable();
+
+ private readonly _openPopupStream: Subject = new Subject();
+ public readonly openPopupStream = this._openPopupStream.asObservable();
+
+ private readonly _openTabStream: Subject = new Subject();
+ public readonly openTabStream = this._openTabStream.asObservable();
+
+ private readonly _goToPageStream: Subject = new Subject();
+ public readonly goToPageStream = this._goToPageStream.asObservable();
+
+ private readonly _openCoWebSiteStream: Subject = new Subject();
+ public readonly openCoWebSiteStream = this._openCoWebSiteStream.asObservable();
+
+ private readonly _closeCoWebSiteStream: Subject = new Subject();
+ public readonly closeCoWebSiteStream = this._closeCoWebSiteStream.asObservable();
+
+ private readonly _disablePlayerControlStream: Subject = new Subject();
+ public readonly disablePlayerControlStream = this._disablePlayerControlStream.asObservable();
+
+ private readonly _enablePlayerControlStream: Subject = new Subject();
+ public readonly enablePlayerControlStream = this._enablePlayerControlStream.asObservable();
+
+ private readonly _closePopupStream: Subject = new Subject();
+ public readonly closePopupStream = this._closePopupStream.asObservable();
+
+ private readonly _displayBubbleStream: Subject = new Subject();
+ public readonly displayBubbleStream = this._displayBubbleStream.asObservable();
+
+ private readonly _removeBubbleStream: Subject = new Subject();
+ public readonly removeBubbleStream = this._removeBubbleStream.asObservable();
+
+ private readonly iframes = new Set();
+ private readonly scripts = new Map();
+
+ init() {
+ window.addEventListener("message", (message) => {
+ // 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).
+ let found = false;
+ for (const iframe of this.iframes) {
+ if (iframe.contentWindow === message.source) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ return;
+ }
+
+ const payload = message.data;
+ if (isIframeEventWrapper(payload)) {
+ if (payload.type === 'chat' && isChatEvent(payload.data)) {
+ this._chatStream.next(payload.data);
+ } else if (payload.type === 'openPopup' && isOpenPopupEvent(payload.data)) {
+ this._openPopupStream.next(payload.data);
+ } else if (payload.type === 'closePopup' && isClosePopupEvent(payload.data)) {
+ this._closePopupStream.next(payload.data);
+ }
+ else if(payload.type === 'openTab' && isOpenTabEvent(payload.data)) {
+ scriptUtils.openTab(payload.data.url);
+ }
+ else if(payload.type === 'goToPage' && isGoToPageEvent(payload.data)) {
+ scriptUtils.goToPage(payload.data.url);
+ }
+ else if(payload.type === 'openCoWebSite' && isOpenCoWebsite(payload.data)) {
+ scriptUtils.openCoWebsite(payload.data.url);
+ }
+ else if(payload.type === 'closeCoWebSite') {
+ scriptUtils.closeCoWebSite();
+ }
+ else if (payload.type === 'disablePlayerControl'){
+ this._disablePlayerControlStream.next();
+ }
+ else if (payload.type === 'restorePlayerControl'){
+ this._enablePlayerControlStream.next();
+ }
+ else if (payload.type === 'displayBubble'){
+ this._displayBubbleStream.next();
+ }
+ else if (payload.type === 'removeBubble'){
+ this._removeBubbleStream.next();
+ }
+ }
+
+
+ }, false);
+
+ }
+
+ /**
+ * Allows the passed iFrame to send/receive messages via the API.
+ */
+ registerIframe(iframe: HTMLIFrameElement): void {
+ this.iframes.add(iframe);
+ }
+
+ unregisterIframe(iframe: HTMLIFrameElement): void {
+ this.iframes.delete(iframe);
+ }
+
+ registerScript(scriptUrl: string): void {
+ console.log('Loading map related script at ', scriptUrl)
+
+ if (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') {
+ // Using external iframe mode (
+ const iframe = document.createElement('iframe');
+ iframe.id = this.getIFrameId(scriptUrl);
+ iframe.style.display = 'none';
+ iframe.src = '/iframe.html?script='+encodeURIComponent(scriptUrl);
+
+ // We are putting a sandbox on this script because it will run in the same domain as the main website.
+ iframe.sandbox.add('allow-scripts');
+ iframe.sandbox.add('allow-top-navigation-by-user-activation');
+
+ document.body.prepend(iframe);
+
+ this.scripts.set(scriptUrl, iframe);
+ this.registerIframe(iframe);
+ } else {
+ // production code
+ const iframe = document.createElement('iframe');
+ iframe.id = this.getIFrameId(scriptUrl);
+ iframe.style.display = 'none';
+
+ // We are putting a sandbox on this script because it will run in the same domain as the main website.
+ iframe.sandbox.add('allow-scripts');
+ iframe.sandbox.add('allow-top-navigation-by-user-activation');
+
+ const html = '\n' +
+ '\n' +
+ '\n' +
+ '\n' +
+ '\n' +
+ '\n' +
+ '\n' +
+ '\n';
+
+ //iframe.src = "data:text/html;charset=utf-8," + escape(html);
+ iframe.srcdoc = html;
+
+ document.body.prepend(iframe);
+
+ this.scripts.set(scriptUrl, iframe);
+ this.registerIframe(iframe);
+ }
+
+
+ }
+
+ private getIFrameId(scriptUrl: string): string {
+ return 'script'+crypto.createHash('md5').update(scriptUrl).digest("hex");
+ }
+
+ unregisterScript(scriptUrl: string): void {
+ const iFrameId = this.getIFrameId(scriptUrl);
+ const iframe = HtmlUtils.getElementByIdOrFail(iFrameId);
+ if (!iframe) {
+ throw new Error('Unknown iframe for script "'+scriptUrl+'"');
+ }
+ this.unregisterIframe(iframe);
+ iframe.remove();
+
+ this.scripts.delete(scriptUrl);
+ }
+
+ sendUserInputChat(message: string) {
+ this.postMessage({
+ 'type': 'userInputChat',
+ 'data': {
+ 'message': message,
+ } as UserInputChatEvent
+ });
+ }
+
+ sendEnterEvent(name: string) {
+ this.postMessage({
+ 'type': 'enterEvent',
+ 'data': {
+ "name": name
+ } as EnterLeaveEvent
+ });
+ }
+
+ sendLeaveEvent(name: string) {
+ this.postMessage({
+ 'type': 'leaveEvent',
+ 'data': {
+ "name": name
+ } as EnterLeaveEvent
+ });
+ }
+
+ sendButtonClickedEvent(popupId: number, buttonId: number): void {
+ this.postMessage({
+ 'type': 'buttonClickedEvent',
+ 'data': {
+ popupId,
+ buttonId
+ } as ButtonClickedEvent
+ });
+ }
+
+ /**
+ * Sends the message... to all allowed iframes.
+ */
+ private postMessage(message: IframeEvent) {
+ for (const iframe of this.iframes) {
+ iframe.contentWindow?.postMessage(message, '*');
+ }
+ }
+
+}
+
+export const iframeListener = new IframeListener();
diff --git a/front/src/Api/ScriptUtils.ts b/front/src/Api/ScriptUtils.ts
new file mode 100644
index 00000000..1a7fed0f
--- /dev/null
+++ b/front/src/Api/ScriptUtils.ts
@@ -0,0 +1,23 @@
+import {coWebsiteManager} from "../WebRtc/CoWebsiteManager";
+
+class ScriptUtils {
+
+ public openTab(url : string){
+ window.open(url);
+ }
+
+ public goToPage(url : string){
+ window.location.href = url;
+
+ }
+
+ public openCoWebsite(url : string){
+ coWebsiteManager.loadCoWebsite(url,url);
+ }
+
+ public closeCoWebSite(){
+ coWebsiteManager.closeCoWebsite();
+ }
+}
+
+export const scriptUtils = new ScriptUtils();
diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts
index 6faaf4fa..5b2ef7f2 100644
--- a/front/src/Phaser/Game/GameScene.ts
+++ b/front/src/Phaser/Game/GameScene.ts
@@ -59,11 +59,14 @@ import {TextureError} from "../../Exception/TextureError";
import {addLoader} from "../Components/Loader";
import {ErrorSceneName} from "../Reconnecting/ErrorScene";
import {localUserStore} from "../../Connexion/LocalUserStore";
+import {iframeListener} from "../../Api/IframeListener";
+import {HtmlUtils} from "../../WebRtc/HtmlUtils";
import Texture = Phaser.Textures.Texture;
import Sprite = Phaser.GameObjects.Sprite;
import CanvasTexture = Phaser.Textures.CanvasTexture;
import GameObject = Phaser.GameObjects.GameObject;
import FILE_LOAD_ERROR = Phaser.Loader.Events.FILE_LOAD_ERROR;
+import DOMElement = Phaser.GameObjects.DOMElement;
import {Subscription} from "rxjs";
import {worldFullMessageStream} from "../../Connexion/WorldFullMessageStream";
@@ -157,6 +160,7 @@ export class GameScene extends ResizableScene implements CenterListener {
private playerName!: string;
private characterLayers!: string[];
private messageSubscription: Subscription|null = null;
+ private popUpElements : Map = new Map();
constructor(private room: Room, MapUrlFile: string, customKey?: string|undefined) {
super({
@@ -263,7 +267,8 @@ export class GameScene extends ResizableScene implements CenterListener {
break;
}
default:
- throw new Error('Unsupported object type: "'+ itemType +'"');
+ continue;
+ //throw new Error('Unsupported object type: "'+ itemType +'"');
}
itemFactory.preload(this.load);
@@ -289,6 +294,12 @@ export class GameScene extends ResizableScene implements CenterListener {
});
});
}
+
+ // Now, let's load the script, if any
+ const scripts = this.getScriptUrls(this.mapFile);
+ for (const script of scripts) {
+ iframeListener.registerScript(script);
+ }
}
//hook initialisation
@@ -306,7 +317,7 @@ export class GameScene extends ResizableScene implements CenterListener {
gameManager.gameSceneIsCreated(this);
urlManager.pushRoomIdToUrl(this.room);
this.startLayerName = urlManager.getStartLayerNameFromUrl();
-
+
this.messageSubscription = worldFullMessageStream.stream.subscribe((message) => this.showWorldFullError())
const playerName = gameManager.getPlayerName();
@@ -410,6 +421,7 @@ export class GameScene extends ResizableScene implements CenterListener {
// From now, this game scene will be notified of reposition events
layoutManager.setListener(this);
this.triggerOnMapLayerPropertyChange();
+ this.listenToIframeEvents();
const camera = this.cameras.main;
@@ -577,12 +589,12 @@ export class GameScene extends ResizableScene implements CenterListener {
const contextRed = this.circleRedTexture.context;
contextRed.beginPath();
contextRed.arc(48, 48, 48, 0, 2 * Math.PI, false);
- // context.lineWidth = 5;
+ //context.lineWidth = 5;
contextRed.strokeStyle = '#ff0000';
contextRed.stroke();
this.circleRedTexture.refresh();
}
-
+
private safeParseJSONstring(jsonString: string|undefined, propertyName: string) {
try {
@@ -606,7 +618,7 @@ export class GameScene extends ResizableScene implements CenterListener {
coWebsiteManager.closeCoWebsite();
}else{
const openWebsiteFunction = () => {
- coWebsiteManager.loadCoWebsite(newValue as string, this.MapUrlFile, allProps.get('openWebsitePolicy') as string | undefined);
+ coWebsiteManager.loadCoWebsite(newValue as string, this.MapUrlFile, allProps.get('openWebsiteAllowApi') as boolean | undefined, allProps.get('openWebsitePolicy') as string | undefined);
layoutManager.removeActionButton('openWebsite', this.userInputManager);
};
@@ -672,6 +684,103 @@ export class GameScene extends ResizableScene implements CenterListener {
this.gameMap.onPropertyChange('playAudioLoop', (newValue, oldValue) => {
newValue === undefined ? audioManager.unloadAudio() : audioManager.playAudio(newValue, this.getMapDirUrl(), undefined, true);
});
+
+ this.gameMap.onPropertyChange('zone', (newValue, oldValue) => {
+ if (newValue === undefined || newValue === false || newValue === '') {
+ iframeListener.sendLeaveEvent(oldValue as string);
+
+ } else {
+ iframeListener.sendEnterEvent(newValue as string);
+ }
+ });
+ }
+
+ private listenToIframeEvents(): void {
+ iframeListener.openPopupStream.subscribe((openPopupEvent) => {
+
+ let objectLayerSquare : ITiledMapObject;
+ const targetObjectData = this.getObjectLayerData(openPopupEvent.targetObject);
+ if (targetObjectData !== undefined){
+ objectLayerSquare = targetObjectData;
+ } else {
+ console.error("Error while opening a popup. Cannot find an object on the map with name '" + openPopupEvent.targetObject + "'. The first parameter of WA.openPopup() must be the name of a rectangle object in your map.");
+ return;
+ }
+ const escapedMessage = HtmlUtils.escapeHtml(openPopupEvent.message);
+ let html = ``;
+ const buttonContainer = `