diff --git a/.github/workflows/push-to-npm.yml b/.github/workflows/push-to-npm.yml index 798e2530..3b90e846 100644 --- a/.github/workflows/push-to-npm.yml +++ b/.github/workflows/push-to-npm.yml @@ -13,10 +13,6 @@ jobs: node-version: '14.x' registry-url: 'https://registry.npmjs.org' - - name: Edit tsconfig.json to add declarations - run: "sed -i 's/\"declaration\": false/\"declaration\": true/g' tsconfig.json" - working-directory: "front" - - name: Replace version number run: 'sed -i "s#VERSION_PLACEHOLDER#${GITHUB_REF/refs\/tags\//}#g" package.json' working-directory: "front/packages/iframe-api-typings" @@ -47,9 +43,9 @@ jobs: working-directory: "front" - name: "Build" - run: yarn run build + run: yarn run build-typings env: - API_URL: "localhost:8080" + PUSHER_URL: "//localhost:8080" working-directory: "front" # We build the front to generate the typings of iframe_api, then we copy those typings in a separate package. diff --git a/CHANGELOG.md b/CHANGELOG.md index 41f72510..fe8ef7dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,18 @@ - Use `WA.room.getCurrentRoom(): Promise` to get the ID, JSON map file, url of the map of the current room and the layer where the current player started - Use `WA.ui.registerMenuCommand(): void` to add a custom menu +## Version 1.4.3 - 1.4.4 - 1.4.5 + +## Bugfixes + +- Fixing the generation of @workadventure/iframe-api-typings + +## Version 1.4.2 + +## Updates + +- A script in an iframe opened by another script can use the IFrame API. + ## Version 1.4.1 ### Bugfixes diff --git a/docs/maps/api-nav.md b/docs/maps/api-nav.md index 29323632..f5721063 100644 --- a/docs/maps/api-nav.md +++ b/docs/maps/api-nav.md @@ -52,11 +52,11 @@ WA.nav.goToRoom("/_/global/.json#start-layer-2") ### Opening/closing a web page in an iFrame ``` -WA.nav.openCoWebSite(url: string): void +WA.nav.openCoWebSite(url: string, allowApi: boolean = false, allowPolicy: string = ""): void WA.nav.closeCoWebSite(): void ``` -Opens the webpage at "url" in an iFrame (on the right side of the screen) or close that iFrame. +Opens the webpage at "url" in an iFrame (on the right side of the screen) or close that iFrame. `allowApi` allows the webpage to use the "IFrame API" and execute script (it is equivalent to putting the `openWebsiteAllowApi` property in the map). `allowPolicy` grants additional access rights to the iFrame. The `allowPolicy` parameter is turned into an [`allow` feature policy in the iFrame](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-allow). Example: @@ -65,4 +65,3 @@ WA.nav.openCoWebSite('https://www.wikipedia.org/'); // ... WA.nav.closeCoWebSite(); ``` - diff --git a/docs/maps/api-reference.md b/docs/maps/api-reference.md index 30a11b2a..8c8205d8 100644 --- a/docs/maps/api-reference.md +++ b/docs/maps/api-reference.md @@ -9,4 +9,4 @@ - [Sound functions](api-sound.md) - [Controls functions](api-controls.md) -- [List of deprecated functions](api-deprecated.md) \ No newline at end of file +- [List of deprecated functions](api-deprecated.md) diff --git a/front/package.json b/front/package.json index 3414e261..2f8f75d4 100644 --- a/front/package.json +++ b/front/package.json @@ -63,6 +63,7 @@ "templater": "cross-env ./templater.sh", "serve": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" webpack serve --open", "build": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" NODE_ENV=production webpack", + "build-typings": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" NODE_ENV=production NODE_ENV=production BUILD_TYPINGS=1 webpack", "test": "TS_NODE_PROJECT=\"tsconfig-for-jasmine.json\" ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json", "lint": "node_modules/.bin/eslint src/ . --ext .ts", "fix": "node_modules/.bin/eslint --fix src/ . --ext .ts", diff --git a/front/src/Api/Events/OpenCoWebSiteEvent.ts b/front/src/Api/Events/OpenCoWebSiteEvent.ts index 0fbc0ce2..d2937405 100644 --- a/front/src/Api/Events/OpenCoWebSiteEvent.ts +++ b/front/src/Api/Events/OpenCoWebSiteEvent.ts @@ -5,6 +5,8 @@ import * as tg from "generic-type-guard"; export const isOpenCoWebsite = new tg.IsInterface().withProperties({ url: tg.isString, + allowApi: tg.isBoolean, + allowPolicy: tg.isString, }).get(); /** diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index 63829a57..735d35c9 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -138,6 +138,8 @@ class IframeListener { return; } + foundSrc = this.getBaseUrl(foundSrc, message.source); + if (isIframeEventWrapper(payload)) { if (payload.type === 'showLayer' && isLayerEvent(payload.data)) { this._showLayerStream.next(payload.data); @@ -171,7 +173,7 @@ class IframeListener { this._loadSoundStream.next(payload.data); } else if (payload.type === 'openCoWebSite' && isOpenCoWebsite(payload.data)) { - scriptUtils.openCoWebsite(payload.data.url, foundSrc); + scriptUtils.openCoWebsite(payload.data.url, foundSrc, payload.data.allowApi, payload.data.allowPolicy); } else if (payload.type === 'closeCoWebSite') { @@ -286,6 +288,15 @@ class IframeListener { } + private getBaseUrl(src: string, source: MessageEventSource | null): string{ + for (const script of this.scripts) { + if (script[1].contentWindow === source) { + return script[0]; + } + } + return src; + } + private static getIFrameId(scriptUrl: string): string { return 'script' + btoa(scriptUrl); } diff --git a/front/src/Api/ScriptUtils.ts b/front/src/Api/ScriptUtils.ts index e1c94507..75a18dc0 100644 --- a/front/src/Api/ScriptUtils.ts +++ b/front/src/Api/ScriptUtils.ts @@ -11,8 +11,8 @@ class ScriptUtils { } - public openCoWebsite(url: string, base: string) { - coWebsiteManager.loadCoWebsite(url, base); + public openCoWebsite(url: string, base: string, api: boolean, policy: string) { + coWebsiteManager.loadCoWebsite(url, base, api, policy); } public closeCoWebSite(){ diff --git a/front/src/Api/iframe/nav.ts b/front/src/Api/iframe/nav.ts index 0d31c1ea..7c7d38b4 100644 --- a/front/src/Api/iframe/nav.ts +++ b/front/src/Api/iframe/nav.ts @@ -36,11 +36,13 @@ class WorkadventureNavigationCommands extends IframeApiContribution import type { Game } from "../../Phaser/Game/Game"; import {CustomizeScene, CustomizeSceneName} from "../../Phaser/Login/CustomizeScene"; + import {activeRowStore} from "../../Stores/CustomCharacterStore"; export let game: Game; const customCharacterScene = game.scene.getScene(CustomizeSceneName) as CustomizeScene; - let activeRow = customCharacterScene.activeRow; function selectLeft() { customCharacterScene.moveCursorHorizontally(-1); @@ -17,12 +17,10 @@ function selectUp() { customCharacterScene.moveCursorVertically(-1); - activeRow = customCharacterScene.activeRow; } function selectDown() { customCharacterScene.moveCursorVertically(1); - activeRow = customCharacterScene.activeRow; } function previousScene() { @@ -44,16 +42,16 @@
- {#if activeRow === 0} + {#if $activeRowStore === 0} {/if} - {#if activeRow !== 0} + {#if $activeRowStore !== 0} {/if} - {#if activeRow === 5} + {#if $activeRowStore === 5} {/if} - {#if activeRow !== 5} + {#if $activeRowStore !== 5} {/if}
diff --git a/front/src/Phaser/Entity/PlayerTexturesLoadingManager.ts b/front/src/Phaser/Entity/PlayerTexturesLoadingManager.ts index 95f00a9e..d2a659ec 100644 --- a/front/src/Phaser/Entity/PlayerTexturesLoadingManager.ts +++ b/front/src/Phaser/Entity/PlayerTexturesLoadingManager.ts @@ -1,90 +1,124 @@ import LoaderPlugin = Phaser.Loader.LoaderPlugin; -import type {CharacterTexture} from "../../Connexion/LocalUser"; -import {BodyResourceDescriptionInterface, LAYERS, PLAYER_RESOURCES} from "./PlayerTextures"; +import type { CharacterTexture } from "../../Connexion/LocalUser"; +import { BodyResourceDescriptionInterface, LAYERS, PLAYER_RESOURCES } from "./PlayerTextures"; export interface FrameConfig { - frameWidth: number, - frameHeight: number, + frameWidth: number; + frameHeight: number; } export const loadAllLayers = (load: LoaderPlugin): BodyResourceDescriptionInterface[][] => { - const returnArray:BodyResourceDescriptionInterface[][] = []; - LAYERS.forEach(layer => { - const layerArray:BodyResourceDescriptionInterface[] = []; + const returnArray: BodyResourceDescriptionInterface[][] = []; + LAYERS.forEach((layer) => { + const layerArray: BodyResourceDescriptionInterface[] = []; Object.values(layer).forEach((textureDescriptor) => { layerArray.push(textureDescriptor); - load.spritesheet(textureDescriptor.name,textureDescriptor.img,{frameWidth: 32, frameHeight: 32}); - }) - returnArray.push(layerArray) + load.spritesheet(textureDescriptor.name, textureDescriptor.img, { frameWidth: 32, frameHeight: 32 }); + }); + returnArray.push(layerArray); }); return returnArray; -} +}; export const loadAllDefaultModels = (load: LoaderPlugin): BodyResourceDescriptionInterface[] => { const returnArray = Object.values(PLAYER_RESOURCES); returnArray.forEach((playerResource: BodyResourceDescriptionInterface) => { - load.spritesheet(playerResource.name, playerResource.img, {frameWidth: 32, frameHeight: 32}); + load.spritesheet(playerResource.name, playerResource.img, { frameWidth: 32, frameHeight: 32 }); }); return returnArray; -} +}; -export const loadCustomTexture = (loaderPlugin: LoaderPlugin, texture: CharacterTexture) : Promise => { - const name = 'customCharacterTexture'+texture.id; - const playerResourceDescriptor: BodyResourceDescriptionInterface = {name, img: texture.url, level: texture.level} +export const loadCustomTexture = ( + loaderPlugin: LoaderPlugin, + texture: CharacterTexture +): Promise => { + const name = "customCharacterTexture" + texture.id; + const playerResourceDescriptor: BodyResourceDescriptionInterface = { name, img: texture.url, level: texture.level }; return createLoadingPromise(loaderPlugin, playerResourceDescriptor, { frameWidth: 32, - frameHeight: 32 + frameHeight: 32, }); -} +}; -export const lazyLoadPlayerCharacterTextures = (loadPlugin: LoaderPlugin, texturekeys:Array): Promise => { - const promisesList:Promise[] = []; - texturekeys.forEach((textureKey: string|BodyResourceDescriptionInterface) => { +export const lazyLoadPlayerCharacterTextures = ( + loadPlugin: LoaderPlugin, + texturekeys: Array +): Promise => { + const promisesList: Promise[] = []; + texturekeys.forEach((textureKey: string | BodyResourceDescriptionInterface) => { try { //TODO refactor const playerResourceDescriptor = getRessourceDescriptor(textureKey); if (playerResourceDescriptor && !loadPlugin.textureManager.exists(playerResourceDescriptor.name)) { - promisesList.push(createLoadingPromise(loadPlugin, playerResourceDescriptor, { - frameWidth: 32, - frameHeight: 32 - })); + promisesList.push( + createLoadingPromise(loadPlugin, playerResourceDescriptor, { + frameWidth: 32, + frameHeight: 32, + }) + ); } - }catch (err){ + } catch (err) { console.error(err); } }); - let returnPromise:Promise>; + let returnPromise: Promise>; if (promisesList.length > 0) { loadPlugin.start(); returnPromise = Promise.all(promisesList).then(() => texturekeys); } else { returnPromise = Promise.resolve(texturekeys); } - return returnPromise.then((keys) => keys.map((key) => { - return typeof key !== 'string' ? key.name : key; - })) -} -export const getRessourceDescriptor = (textureKey: string|BodyResourceDescriptionInterface): BodyResourceDescriptionInterface => { - if (typeof textureKey !== 'string' && textureKey.img) { + //If the loading fail, we render the default model instead. + return returnPromise + .then((keys) => + keys.map((key) => { + return typeof key !== "string" ? key.name : key; + }) + ) + .catch(() => lazyLoadPlayerCharacterTextures(loadPlugin, ["color_22", "eyes_23"])); +}; + +export const getRessourceDescriptor = ( + textureKey: string | BodyResourceDescriptionInterface +): BodyResourceDescriptionInterface => { + if (typeof textureKey !== "string" && textureKey.img) { return textureKey; } - const textureName:string = typeof textureKey === 'string' ? textureKey : textureKey.name; + const textureName: string = typeof textureKey === "string" ? textureKey : textureKey.name; const playerResource = PLAYER_RESOURCES[textureName]; if (playerResource !== undefined) return playerResource; - for (let i=0; i { - return new Promise((res) => { +export const createLoadingPromise = ( + loadPlugin: LoaderPlugin, + playerResourceDescriptor: BodyResourceDescriptionInterface, + frameConfig: FrameConfig +) => { + return new Promise((res, rej) => { + console.log("count", loadPlugin.listenerCount("loaderror")); if (loadPlugin.textureManager.exists(playerResourceDescriptor.name)) { return res(playerResourceDescriptor); } loadPlugin.spritesheet(playerResourceDescriptor.name, playerResourceDescriptor.img, frameConfig); - loadPlugin.once('filecomplete-spritesheet-' + playerResourceDescriptor.name, () => res(playerResourceDescriptor)); + const errorCallback = (file: { src: string }) => { + if (file.src !== playerResourceDescriptor.img) return; + console.error("failed loading player ressource: ", playerResourceDescriptor); + rej(playerResourceDescriptor); + loadPlugin.off("filecomplete-spritesheet-" + playerResourceDescriptor.name, successCallback); + loadPlugin.off("loaderror", errorCallback); + }; + const successCallback = () => { + loadPlugin.off("loaderror", errorCallback); + res(playerResourceDescriptor); + }; + + loadPlugin.once("filecomplete-spritesheet-" + playerResourceDescriptor.name, successCallback); + loadPlugin.on("loaderror", errorCallback); }); -} +}; diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index f34098b8..1a4071da 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -1,4 +1,4 @@ -import { Queue } from 'queue-typescript'; +import { Queue } from "queue-typescript"; import type { Subscription } from "rxjs"; import { GlobalMessageManager } from "../../Administration/GlobalMessageManager"; import { userMessageManager } from "../../Administration/UserMessageManager"; @@ -12,18 +12,13 @@ import type { OnConnectInterface, PointInterface, PositionInterface, - RoomJoinedMessageInterface + RoomJoinedMessageInterface, } from "../../Connexion/ConnexionModels"; import { localUserStore } from "../../Connexion/LocalUserStore"; import { Room } from "../../Connexion/Room"; import type { RoomConnection } from "../../Connexion/RoomConnection"; import { worldFullMessageStream } from "../../Connexion/WorldFullMessageStream"; -import { - DEBUG_MODE, - JITSI_PRIVATE_MODE, - MAX_PER_GROUP, - POSITION_DELAY -} from "../../Enum/EnvironmentVariable"; +import { DEBUG_MODE, JITSI_PRIVATE_MODE, MAX_PER_GROUP, POSITION_DELAY } from "../../Enum/EnvironmentVariable"; import { TextureError } from "../../Exception/TextureError"; import type { UserMovedMessage } from "../../Messages/generated/messages_pb"; import { ProtobufClientUtils } from "../../Network/ProtobufClientUtils"; @@ -34,14 +29,15 @@ import { coWebsiteManager } from "../../WebRtc/CoWebsiteManager"; import { HtmlUtils } from "../../WebRtc/HtmlUtils"; import { jitsiFactory } from "../../WebRtc/JitsiFactory"; import { - AUDIO_LOOP_PROPERTY, AUDIO_VOLUME_PROPERTY, + AUDIO_LOOP_PROPERTY, + AUDIO_VOLUME_PROPERTY, Box, JITSI_MESSAGE_PROPERTIES, layoutManager, ON_ACTION_TRIGGER_BUTTON, TRIGGER_JITSI_PROPERTIES, TRIGGER_WEBSITE_PROPERTIES, - WEBSITE_MESSAGE_PROPERTIES + WEBSITE_MESSAGE_PROPERTIES, } from "../../WebRtc/LayoutManager"; import { mediaManager } from "../../WebRtc/MediaManager"; import { SimplePeer, UserSimplePeerInterface } from "../../WebRtc/SimplePeer"; @@ -63,8 +59,9 @@ import type { ITiledMapLayerProperty, ITiledMapObject, ITiledMapTileLayer, - ITiledTileSet } from "../Map/ITiledMap"; -import { MenuScene, MenuSceneName } from '../Menu/MenuScene'; + ITiledTileSet, +} from "../Map/ITiledMap"; +import { MenuScene, MenuSceneName } from "../Menu/MenuScene"; import { PlayerAnimationDirections } from "../Player/Animation"; import { hasMovedEventName, Player, requestEmoteEventName } from "../Player/Player"; import { ErrorSceneName } from "../Reconnecting/ErrorScene"; @@ -86,53 +83,53 @@ 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 EVENT_TYPE = Phaser.Scenes.Events +import EVENT_TYPE = Phaser.Scenes.Events; import RenderTexture = Phaser.GameObjects.RenderTexture; import Tilemap = Phaser.Tilemaps.Tilemap; -import type { HasPlayerMovedEvent } from '../../Api/Events/HasPlayerMovedEvent'; +import type { HasPlayerMovedEvent } from "../../Api/Events/HasPlayerMovedEvent"; import AnimatedTiles from "phaser-animated-tiles"; -import {soundManager} from "./SoundManager"; -import {peerStore, screenSharingPeerStore} from "../../Stores/PeerStore"; -import {videoFocusStore} from "../../Stores/VideoFocusStore"; -import {biggestAvailableAreaStore} from "../../Stores/BiggestAvailableAreaStore"; +import { soundManager } from "./SoundManager"; +import { peerStore, screenSharingPeerStore } from "../../Stores/PeerStore"; +import { videoFocusStore } from "../../Stores/VideoFocusStore"; +import { biggestAvailableAreaStore } from "../../Stores/BiggestAvailableAreaStore"; export interface GameSceneInitInterface { - initPosition: PointInterface | null, - reconnecting: boolean + initPosition: PointInterface | null; + reconnecting: boolean; } interface InitUserPositionEventInterface { - type: 'InitUserPositionEvent' - event: MessageUserPositionInterface[] + type: "InitUserPositionEvent"; + event: MessageUserPositionInterface[]; } interface AddPlayerEventInterface { - type: 'AddPlayerEvent' - event: AddPlayerInterface + type: "AddPlayerEvent"; + event: AddPlayerInterface; } interface RemovePlayerEventInterface { - type: 'RemovePlayerEvent' - userId: number + type: "RemovePlayerEvent"; + userId: number; } interface UserMovedEventInterface { - type: 'UserMovedEvent' - event: MessageUserMovedInterface + type: "UserMovedEvent"; + event: MessageUserMovedInterface; } interface GroupCreatedUpdatedEventInterface { - type: 'GroupCreatedUpdatedEvent' - event: GroupCreatedUpdatedMessageInterface + type: "GroupCreatedUpdatedEvent"; + event: GroupCreatedUpdatedMessageInterface; } interface DeleteGroupEventInterface { - type: 'DeleteGroupEvent' - groupId: number + type: "DeleteGroupEvent"; + groupId: number; } -const defaultStartLayerName = 'start'; +const defaultStartLayerName = "start"; export class GameScene extends DirtyScene { Terrains: Array; @@ -148,14 +145,30 @@ export class GameScene extends DirtyScene { startY!: number; circleTexture!: CanvasTexture; circleRedTexture!: CanvasTexture; - pendingEvents: Queue = new Queue(); + pendingEvents: Queue< + | InitUserPositionEventInterface + | AddPlayerEventInterface + | RemovePlayerEventInterface + | UserMovedEventInterface + | GroupCreatedUpdatedEventInterface + | DeleteGroupEventInterface + > = new Queue< + | InitUserPositionEventInterface + | AddPlayerEventInterface + | RemovePlayerEventInterface + | UserMovedEventInterface + | GroupCreatedUpdatedEventInterface + | DeleteGroupEventInterface + >(); private initPosition: PositionInterface | null = null; private playersPositionInterpolator = new PlayersPositionInterpolator(); public connection: RoomConnection | undefined; private simplePeer!: SimplePeer; private GlobalMessageManager!: GlobalMessageManager; private connectionAnswerPromise: Promise; - private connectionAnswerPromiseResolve!: (value: RoomJoinedMessageInterface | PromiseLike) => void; + private connectionAnswerPromiseResolve!: ( + value: RoomJoinedMessageInterface | PromiseLike + ) => void; // A promise that will resolve when the "create" method is called (signaling loading is ended) private createPromise: Promise; private createPromiseResolve!: (value?: void | PromiseLike) => void; @@ -169,11 +182,11 @@ export class GameScene extends DirtyScene { currentTick!: number; lastSentTick!: number; // The last tick at which a position was sent. lastMoveEventSent: HasPlayerMovedEvent = { - direction: '', + direction: "", moving: false, x: -1000, - y: -1000 - } + y: -1000, + }; private gameMap!: GameMap; private actionableItems: Map = new Map(); @@ -192,16 +205,16 @@ export class GameScene extends DirtyScene { private pinchManager: PinchManager | undefined; private mapTransitioning: boolean = false; //used to prevent transitions happenning at the same time. private emoteManager!: EmoteManager; + private preloading: boolean = true; constructor(private room: Room, MapUrlFile: string, customKey?: string | undefined) { super({ - key: customKey ?? room.id + key: customKey ?? room.id, }); this.Terrains = []; this.groups = new Map(); this.instance = room.getInstance(); - this.MapUrlFile = MapUrlFile; this.RoomId = room.id; @@ -223,50 +236,70 @@ export class GameScene extends DirtyScene { } } - this.load.image(openChatIconName, 'resources/objects/talk.png'); + this.load.image(openChatIconName, "resources/objects/talk.png"); if (touchScreenManager.supportTouchScreen) { this.load.image(joystickBaseKey, joystickBaseImg); this.load.image(joystickThumbKey, joystickThumbImg); } - this.load.audio('audio-webrtc-in', '/resources/objects/webrtc-in.mp3'); - this.load.audio('audio-webrtc-out', '/resources/objects/webrtc-out.mp3'); + this.load.audio("audio-webrtc-in", "/resources/objects/webrtc-in.mp3"); + this.load.audio("audio-webrtc-out", "/resources/objects/webrtc-out.mp3"); //this.load.audio('audio-report-message', '/resources/objects/report-message.mp3'); this.sound.pauseOnBlur = false; this.load.on(FILE_LOAD_ERROR, (file: { src: string }) => { // If we happen to be in HTTP and we are trying to load a URL in HTTPS only... (this happens only in dev environments) - if (window.location.protocol === 'http:' && file.src === this.MapUrlFile && file.src.startsWith('http:') && this.originalMapUrl === undefined) { + if ( + window.location.protocol === "http:" && + file.src === this.MapUrlFile && + file.src.startsWith("http:") && + this.originalMapUrl === undefined + ) { this.originalMapUrl = this.MapUrlFile; - this.MapUrlFile = this.MapUrlFile.replace('http://', 'https://'); + this.MapUrlFile = this.MapUrlFile.replace("http://", "https://"); this.load.tilemapTiledJSON(this.MapUrlFile, this.MapUrlFile); - this.load.on('filecomplete-tilemapJSON-' + this.MapUrlFile, (key: string, type: string, data: unknown) => { - this.onMapLoad(data); - }); + this.load.on( + "filecomplete-tilemapJSON-" + this.MapUrlFile, + (key: string, type: string, data: unknown) => { + this.onMapLoad(data); + } + ); return; } // 127.0.0.1, localhost and *.localhost are considered secure, even on HTTP. // So if we are in https, we can still try to load a HTTP local resource (can be useful for testing purposes) // See https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts#when_is_a_context_considered_secure const url = new URL(file.src); - const host = url.host.split(':')[0]; - if (window.location.protocol === 'https:' && file.src === this.MapUrlFile && (host === '127.0.0.1' || host === 'localhost' || host.endsWith('.localhost')) && this.originalMapUrl === undefined) { + const host = url.host.split(":")[0]; + if ( + window.location.protocol === "https:" && + file.src === this.MapUrlFile && + (host === "127.0.0.1" || host === "localhost" || host.endsWith(".localhost")) && + this.originalMapUrl === undefined + ) { this.originalMapUrl = this.MapUrlFile; - this.MapUrlFile = this.MapUrlFile.replace('https://', 'http://'); + this.MapUrlFile = this.MapUrlFile.replace("https://", "http://"); this.load.tilemapTiledJSON(this.MapUrlFile, this.MapUrlFile); - this.load.on('filecomplete-tilemapJSON-' + this.MapUrlFile, (key: string, type: string, data: unknown) => { - this.onMapLoad(data); - }); + this.load.on( + "filecomplete-tilemapJSON-" + this.MapUrlFile, + (key: string, type: string, data: unknown) => { + this.onMapLoad(data); + } + ); return; } - this.scene.start(ErrorSceneName, { - title: 'Network error', - subTitle: 'An error occurred while loading resource:', - message: this.originalMapUrl ?? file.src - }); + //once preloading is over, we don't want loading errors to crash the game, so we need to disable this behavior after preloading. + console.error("Error when loading: ", file); + if (this.preloading) { + this.scene.start(ErrorSceneName, { + title: "Network error", + subTitle: "An error occurred while loading resource:", + message: this.originalMapUrl ?? file.src, + }); + } }); - this.load.scenePlugin('AnimatedTiles', AnimatedTiles, 'animatedTiles', 'animatedTiles'); - this.load.on('filecomplete-tilemapJSON-' + this.MapUrlFile, (key: string, type: string, data: unknown) => { + this.load.scenePlugin("AnimatedTiles", AnimatedTiles, "animatedTiles", "animatedTiles"); + this.load.on("filecomplete-tilemapJSON-" + this.MapUrlFile, (key: string, type: string, data: unknown) => { this.onMapLoad(data); }); //TODO strategy to add access token @@ -278,13 +311,13 @@ export class GameScene extends DirtyScene { this.onMapLoad(data); } - this.load.bitmapFont('main_font', 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml'); + this.load.bitmapFont("main_font", "resources/fonts/arcade.png", "resources/fonts/arcade.xml"); //eslint-disable-next-line @typescript-eslint/no-explicit-any (this.load as any).rexWebFont({ custom: { - families: ['Press Start 2P'], - urls: ['/resources/fonts/fonts.css'], - testString: 'abcdefg' + families: ["Press Start 2P"], + urls: ["/resources/fonts/fonts.css"], + testString: "abcdefg", }, }); @@ -298,21 +331,21 @@ export class GameScene extends DirtyScene { // Triggered when the map is loaded // Load tiles attached to the map recursively this.mapFile = data.data; - const url = this.MapUrlFile.substr(0, this.MapUrlFile.lastIndexOf('/')); + const url = this.MapUrlFile.substr(0, this.MapUrlFile.lastIndexOf("/")); this.mapFile.tilesets.forEach((tileset) => { - if (typeof tileset.name === 'undefined' || typeof tileset.image === 'undefined') { - console.warn("Don't know how to handle tileset ", tileset) + if (typeof tileset.name === "undefined" || typeof tileset.image === "undefined") { + console.warn("Don't know how to handle tileset ", tileset); return; } //TODO strategy to add access token this.load.image(`${url}/${tileset.image}`, `${url}/${tileset.image}`); - }) + }); // Scan the object layers for objects to load and load them. const objects = new Map(); for (const layer of this.mapFile.layers) { - if (layer.type === 'objectgroup') { + if (layer.type === "objectgroup") { for (const object of layer.objects) { let objectsOfType: ITiledMapObject[] | undefined; if (!objects.has(object.type)) { @@ -320,7 +353,7 @@ export class GameScene extends DirtyScene { } else { objectsOfType = objects.get(object.type); if (objectsOfType === undefined) { - throw new Error('Unexpected object type not found'); + throw new Error("Unexpected object type not found"); } } objectsOfType.push(object); @@ -335,8 +368,8 @@ export class GameScene extends DirtyScene { let itemFactory: ItemFactoryInterface; switch (itemType) { - case 'computer': { - const module = await import('../Items/Computer/computer'); + case "computer": { + const module = await import("../Items/Computer/computer"); itemFactory = module.default; break; } @@ -348,7 +381,7 @@ export class GameScene extends DirtyScene { itemFactory.preload(this.load); this.load.start(); // Let's manually start the loader because the import might be over AFTER the loading ends. - this.load.on('complete', () => { + this.load.on("complete", () => { // FIXME: the factory might fail because the resources might not be loaded yet... // We would need to add a loader ended event in addition to the createPromise this.createPromise.then(async () => { @@ -388,6 +421,7 @@ export class GameScene extends DirtyScene { //hook create scene create(): void { + this.preloading = false; this.trackDirtyAnims(); gameManager.gameSceneIsCreated(this); @@ -398,11 +432,13 @@ export class GameScene extends DirtyScene { this.pinchManager = new PinchManager(this); } - this.messageSubscription = worldFullMessageStream.stream.subscribe((message) => this.showWorldFullError(message)) + this.messageSubscription = worldFullMessageStream.stream.subscribe((message) => + this.showWorldFullError(message) + ); const playerName = gameManager.getPlayerName(); if (!playerName) { - throw 'playerName is not set'; + throw "playerName is not set"; } this.playerName = playerName; this.characterLayers = gameManager.getCharacterLayers(); @@ -410,9 +446,18 @@ export class GameScene extends DirtyScene { //initalise map this.Map = this.add.tilemap(this.MapUrlFile); - const mapDirUrl = this.MapUrlFile.substr(0, this.MapUrlFile.lastIndexOf('/')); + const mapDirUrl = this.MapUrlFile.substr(0, this.MapUrlFile.lastIndexOf("/")); this.mapFile.tilesets.forEach((tileset: ITiledTileSet) => { - this.Terrains.push(this.Map.addTilesetImage(tileset.name, `${mapDirUrl}/${tileset.image}`, tileset.tilewidth, tileset.tileheight, tileset.margin, tileset.spacing/*, tileset.firstgid*/)); + this.Terrains.push( + this.Map.addTilesetImage( + tileset.name, + `${mapDirUrl}/${tileset.image}`, + tileset.tilewidth, + tileset.tileheight, + tileset.margin, + tileset.spacing /*, tileset.firstgid*/ + ) + ); }); //permit to set bound collision @@ -421,8 +466,7 @@ export class GameScene extends DirtyScene { //add layer on map this.gameMap = new GameMap(this.mapFile, this.Map, this.Terrains); for (const layer of this.gameMap.flatLayers) { - if (layer.type === 'tilelayer') { - + if (layer.type === "tilelayer") { const exitSceneUrl = this.getExitSceneUrl(layer); if (exitSceneUrl !== undefined) { this.loadNextGame(exitSceneUrl); @@ -432,7 +476,7 @@ export class GameScene extends DirtyScene { this.loadNextGame(exitUrl); } } - if (layer.type === 'objectgroup') { + if (layer.type === "objectgroup") { for (const object of layer.objects) { if (object.text) { TextUtils.createTextFromITiledMapObject(this, object); @@ -441,9 +485,9 @@ export class GameScene extends DirtyScene { } } - this.gameMap.exitUrls.forEach(exitUrl => { - this.loadNextGame(exitUrl) - }) + this.gameMap.exitUrls.forEach((exitUrl) => { + this.loadNextGame(exitUrl); + }); this.initStartXAndStartY(); @@ -453,13 +497,12 @@ export class GameScene extends DirtyScene { //initialise list of other player this.MapPlayers = this.physics.add.group({ immovable: true }); - //create input to move this.userInputManager = new UserInputManager(this); mediaManager.setUserInputManager(this.userInputManager); if (localUserStore.getFullscreen()) { - document.querySelector('body')?.requestFullscreen(); + document.querySelector("body")?.requestFullscreen(); } //notify game manager can to create currentUser in map @@ -469,7 +512,7 @@ export class GameScene extends DirtyScene { this.initCamera(); this.animatedTiles.init(this.Map); - this.events.on('tileanimationupdate', () => this.dirty = true); + this.events.on("tileanimationupdate", () => (this.dirty = true)); this.initCirclesCanvas(); @@ -497,17 +540,18 @@ export class GameScene extends DirtyScene { this.outlinedItem?.activate(); }); - this.openChatIcon = new OpenChatIcon(this, 2, this.game.renderer.height - 2) + this.openChatIcon = new OpenChatIcon(this, 2, this.game.renderer.height - 2); this.reposition(); // From now, this game scene will be notified of reposition events - this.biggestAvailableAreaStoreUnsubscribe = biggestAvailableAreaStore.subscribe((box) => this.updateCameraOffset(box)); + this.biggestAvailableAreaStoreUnsubscribe = biggestAvailableAreaStore.subscribe((box) => + this.updateCameraOffset(box) + ); this.triggerOnMapLayerPropertyChange(); this.listenToIframeEvents(); - if (!this.room.isDisconnected()) { this.connect(); } @@ -518,12 +562,12 @@ export class GameScene extends DirtyScene { this.peerStoreUnsubscribe = peerStore.subscribe((peers) => { const newPeerNumber = peers.size; if (newPeerNumber > oldPeerNumber) { - this.sound.play('audio-webrtc-in', { - volume: 0.2 + this.sound.play("audio-webrtc-in", { + volume: 0.2, }); } else if (newPeerNumber < oldPeerNumber) { - this.sound.play('audio-webrtc-out', { - volume: 0.2 + this.sound.play("audio-webrtc-out", { + volume: 0.2, }); } oldPeerNumber = newPeerNumber; @@ -536,221 +580,236 @@ export class GameScene extends DirtyScene { private connect(): void { const camera = this.cameras.main; - connectionManager.connectToRoomSocket( - this.RoomId, - this.playerName, - this.characterLayers, - { - x: this.startX, - y: this.startY - }, - { - left: camera.scrollX, - top: camera.scrollY, - right: camera.scrollX + camera.width, - bottom: camera.scrollY + camera.height, - }, - this.companion - ).then((onConnect: OnConnectInterface) => { - this.connection = onConnect.connection; + connectionManager + .connectToRoomSocket( + this.RoomId, + this.playerName, + this.characterLayers, + { + x: this.startX, + y: this.startY, + }, + { + left: camera.scrollX, + top: camera.scrollY, + right: camera.scrollX + camera.width, + bottom: camera.scrollY + camera.height, + }, + this.companion + ) + .then((onConnect: OnConnectInterface) => { + this.connection = onConnect.connection; - this.connection.onUserJoins((message: MessageUserJoined) => { - const userMessage: AddPlayerInterface = { - userId: message.userId, - characterLayers: message.characterLayers, - name: message.name, - position: message.position, - visitCardUrl: message.visitCardUrl, - companion: message.companion - } - this.addPlayer(userMessage); - }); + this.connection.onUserJoins((message: MessageUserJoined) => { + const userMessage: AddPlayerInterface = { + userId: message.userId, + characterLayers: message.characterLayers, + name: message.name, + position: message.position, + visitCardUrl: message.visitCardUrl, + companion: message.companion, + }; + this.addPlayer(userMessage); + }); - this.connection.onUserMoved((message: UserMovedMessage) => { - const position = message.getPosition(); - if (position === undefined) { - throw new Error('Position missing from UserMovedMessage'); - } + this.connection.onUserMoved((message: UserMovedMessage) => { + const position = message.getPosition(); + if (position === undefined) { + throw new Error("Position missing from UserMovedMessage"); + } - const messageUserMoved: MessageUserMovedInterface = { - userId: message.getUserid(), - position: ProtobufClientUtils.toPointInterface(position) - } + const messageUserMoved: MessageUserMovedInterface = { + userId: message.getUserid(), + position: ProtobufClientUtils.toPointInterface(position), + }; - this.updatePlayerPosition(messageUserMoved); - }); + this.updatePlayerPosition(messageUserMoved); + }); - this.connection.onUserLeft((userId: number) => { - this.removePlayer(userId); - }); + this.connection.onUserLeft((userId: number) => { + this.removePlayer(userId); + }); - this.connection.onGroupUpdatedOrCreated((groupPositionMessage: GroupCreatedUpdatedMessageInterface) => { - this.shareGroupPosition(groupPositionMessage); - }) + this.connection.onGroupUpdatedOrCreated((groupPositionMessage: GroupCreatedUpdatedMessageInterface) => { + this.shareGroupPosition(groupPositionMessage); + }); - this.connection.onGroupDeleted((groupId: number) => { - try { - this.deleteGroup(groupId); - } catch (e) { - console.error(e); - } - }) + this.connection.onGroupDeleted((groupId: number) => { + try { + this.deleteGroup(groupId); + } catch (e) { + console.error(e); + } + }); - this.connection.onServerDisconnected(() => { - console.log('Player disconnected from server. Reloading scene.'); - this.cleanupClosingScene(); + this.connection.onServerDisconnected(() => { + console.log("Player disconnected from server. Reloading scene."); + this.cleanupClosingScene(); - const gameSceneKey = 'somekey' + Math.round(Math.random() * 10000); - const game: Phaser.Scene = new GameScene(this.room, this.MapUrlFile, gameSceneKey); - this.scene.add(gameSceneKey, game, true, - { + const gameSceneKey = "somekey" + Math.round(Math.random() * 10000); + const game: Phaser.Scene = new GameScene(this.room, this.MapUrlFile, gameSceneKey); + this.scene.add(gameSceneKey, game, true, { initPosition: { x: this.CurrentPlayer.x, - y: this.CurrentPlayer.y + y: this.CurrentPlayer.y, }, - reconnecting: true + reconnecting: true, }); - this.scene.stop(this.scene.key); - this.scene.remove(this.scene.key); - }) + this.scene.stop(this.scene.key); + this.scene.remove(this.scene.key); + }); - this.connection.onActionableEvent((message => { - const item = this.actionableItems.get(message.itemId); - if (item === undefined) { - console.warn('Received an event about object "' + message.itemId + '" but cannot find this item on the map.'); - return; - } - item.fire(message.event, message.state, message.parameters); - })); - - /** - * Triggered when we receive the JWT token to connect to Jitsi - */ - this.connection.onStartJitsiRoom((jwt, room) => { - this.startJitsi(room, jwt); - }); - - // When connection is performed, let's connect SimplePeer - this.simplePeer = new SimplePeer(this.connection, !this.room.isPublic, this.playerName); - peerStore.connectToSimplePeer(this.simplePeer); - screenSharingPeerStore.connectToSimplePeer(this.simplePeer); - videoFocusStore.connectToSimplePeer(this.simplePeer); - this.GlobalMessageManager = new GlobalMessageManager(this.connection); - userMessageManager.setReceiveBanListener(this.bannedUser.bind(this)); - - const self = this; - this.simplePeer.registerPeerConnectionListener({ - onConnect(peer) { - self.openChatIcon.setVisible(true); - audioManager.decreaseVolume(); - }, - onDisconnect(userId: number) { - if (self.simplePeer.getNbConnections() === 0) { - self.openChatIcon.setVisible(false); - audioManager.restoreVolume(); + this.connection.onActionableEvent((message) => { + const item = this.actionableItems.get(message.itemId); + if (item === undefined) { + console.warn( + 'Received an event about object "' + + message.itemId + + '" but cannot find this item on the map.' + ); + return; } - } - }) + item.fire(message.event, message.state, message.parameters); + }); - //listen event to share position of user - this.CurrentPlayer.on(hasMovedEventName, this.pushPlayerPosition.bind(this)) - this.CurrentPlayer.on(hasMovedEventName, this.outlineItem.bind(this)) - this.CurrentPlayer.on(hasMovedEventName, (event: HasPlayerMovedEvent) => { - this.gameMap.setPosition(event.x, event.y); - }) + /** + * Triggered when we receive the JWT token to connect to Jitsi + */ + this.connection.onStartJitsiRoom((jwt, room) => { + this.startJitsi(room, jwt); + }); - //this.initUsersPosition(roomJoinedMessage.users); - this.connectionAnswerPromiseResolve(onConnect.room); - // Analyze tags to find if we are admin. If yes, show console. + // When connection is performed, let's connect SimplePeer + this.simplePeer = new SimplePeer(this.connection, !this.room.isPublic, this.playerName); + peerStore.connectToSimplePeer(this.simplePeer); + screenSharingPeerStore.connectToSimplePeer(this.simplePeer); + videoFocusStore.connectToSimplePeer(this.simplePeer); + this.GlobalMessageManager = new GlobalMessageManager(this.connection); + userMessageManager.setReceiveBanListener(this.bannedUser.bind(this)); + const self = this; + this.simplePeer.registerPeerConnectionListener({ + onConnect(peer) { + self.openChatIcon.setVisible(true); + audioManager.decreaseVolume(); + }, + onDisconnect(userId: number) { + if (self.simplePeer.getNbConnections() === 0) { + self.openChatIcon.setVisible(false); + audioManager.restoreVolume(); + } + }, + }); - this.scene.wake(); - this.scene.stop(ReconnectingSceneName); + //listen event to share position of user + this.CurrentPlayer.on(hasMovedEventName, this.pushPlayerPosition.bind(this)); + this.CurrentPlayer.on(hasMovedEventName, this.outlineItem.bind(this)); + this.CurrentPlayer.on(hasMovedEventName, (event: HasPlayerMovedEvent) => { + this.gameMap.setPosition(event.x, event.y); + }); - //init user position and play trigger to check layers properties - this.gameMap.setPosition(this.CurrentPlayer.x, this.CurrentPlayer.y); - }); + //this.initUsersPosition(roomJoinedMessage.users); + this.connectionAnswerPromiseResolve(onConnect.room); + // Analyze tags to find if we are admin. If yes, show console. + + this.scene.wake(); + this.scene.stop(ReconnectingSceneName); + + //init user position and play trigger to check layers properties + this.gameMap.setPosition(this.CurrentPlayer.x, this.CurrentPlayer.y); + }); } //todo: into dedicated classes private initCirclesCanvas(): void { // Let's generate the circle for the group delimiter - let circleElement = Object.values(this.textures.list).find((object: Texture) => object.key === 'circleSprite-white'); + let circleElement = Object.values(this.textures.list).find( + (object: Texture) => object.key === "circleSprite-white" + ); if (circleElement) { - this.textures.remove('circleSprite-white'); + this.textures.remove("circleSprite-white"); } - circleElement = Object.values(this.textures.list).find((object: Texture) => object.key === 'circleSprite-red'); + circleElement = Object.values(this.textures.list).find((object: Texture) => object.key === "circleSprite-red"); if (circleElement) { - this.textures.remove('circleSprite-red'); + this.textures.remove("circleSprite-red"); } //create white circle canvas use to create sprite - this.circleTexture = this.textures.createCanvas('circleSprite-white', 96, 96); + this.circleTexture = this.textures.createCanvas("circleSprite-white", 96, 96); const context = this.circleTexture.context; context.beginPath(); context.arc(48, 48, 48, 0, 2 * Math.PI, false); // context.lineWidth = 5; - context.strokeStyle = '#ffffff'; + context.strokeStyle = "#ffffff"; context.stroke(); this.circleTexture.refresh(); //create red circle canvas use to create sprite - this.circleRedTexture = this.textures.createCanvas('circleSprite-red', 96, 96); + this.circleRedTexture = this.textures.createCanvas("circleSprite-red", 96, 96); const contextRed = this.circleRedTexture.context; contextRed.beginPath(); contextRed.arc(48, 48, 48, 0, 2 * Math.PI, false); //context.lineWidth = 5; - contextRed.strokeStyle = '#ff0000'; + contextRed.strokeStyle = "#ff0000"; contextRed.stroke(); this.circleRedTexture.refresh(); } - private safeParseJSONstring(jsonString: string | undefined, propertyName: string) { try { return jsonString ? JSON.parse(jsonString) : {}; } catch (e) { console.warn('Invalid JSON found in property "' + propertyName + '" of the map:' + jsonString, e); - return {} + return {}; } } private triggerOnMapLayerPropertyChange() { - this.gameMap.onPropertyChange('exitSceneUrl', (newValue, oldValue) => { + this.gameMap.onPropertyChange("exitSceneUrl", (newValue, oldValue) => { if (newValue) this.onMapExit(newValue as string); }); - this.gameMap.onPropertyChange('exitUrl', (newValue, oldValue) => { + this.gameMap.onPropertyChange("exitUrl", (newValue, oldValue) => { if (newValue) this.onMapExit(newValue as string); }); - this.gameMap.onPropertyChange('openWebsite', (newValue, oldValue, allProps) => { + this.gameMap.onPropertyChange("openWebsite", (newValue, oldValue, allProps) => { if (newValue === undefined) { - layoutManager.removeActionButton('openWebsite', this.userInputManager); + layoutManager.removeActionButton("openWebsite", this.userInputManager); coWebsiteManager.closeCoWebsite(); } else { const openWebsiteFunction = () => { - coWebsiteManager.loadCoWebsite(newValue as string, this.MapUrlFile, allProps.get('openWebsiteAllowApi') as boolean | undefined, allProps.get('openWebsitePolicy') as string | undefined); - layoutManager.removeActionButton('openWebsite', this.userInputManager); + coWebsiteManager.loadCoWebsite( + newValue as string, + this.MapUrlFile, + allProps.get("openWebsiteAllowApi") as boolean | undefined, + allProps.get("openWebsitePolicy") as string | undefined + ); + layoutManager.removeActionButton("openWebsite", this.userInputManager); }; const openWebsiteTriggerValue = allProps.get(TRIGGER_WEBSITE_PROPERTIES); if (openWebsiteTriggerValue && openWebsiteTriggerValue === ON_ACTION_TRIGGER_BUTTON) { let message = allProps.get(WEBSITE_MESSAGE_PROPERTIES); if (message === undefined) { - message = 'Press SPACE or touch here to open web site'; + message = "Press SPACE or touch here to open web site"; } - layoutManager.addActionButton('openWebsite', message.toString(), () => { - openWebsiteFunction(); - }, this.userInputManager); + layoutManager.addActionButton( + "openWebsite", + message.toString(), + () => { + openWebsiteFunction(); + }, + this.userInputManager + ); } else { openWebsiteFunction(); } } }); - this.gameMap.onPropertyChange('jitsiRoom', (newValue, oldValue, allProps) => { + this.gameMap.onPropertyChange("jitsiRoom", (newValue, oldValue, allProps) => { if (newValue === undefined) { - layoutManager.removeActionButton('jitsiRoom', this.userInputManager); + layoutManager.removeActionButton("jitsiRoom", this.userInputManager); this.stopJitsi(); } else { const openJitsiRoomFunction = () => { @@ -763,44 +822,52 @@ export class GameScene extends DirtyScene { } else { this.startJitsi(roomName, undefined); } - layoutManager.removeActionButton('jitsiRoom', this.userInputManager); - } + layoutManager.removeActionButton("jitsiRoom", this.userInputManager); + }; const jitsiTriggerValue = allProps.get(TRIGGER_JITSI_PROPERTIES); if (jitsiTriggerValue && jitsiTriggerValue === ON_ACTION_TRIGGER_BUTTON) { let message = allProps.get(JITSI_MESSAGE_PROPERTIES); if (message === undefined) { - message = 'Press SPACE or touch here to enter Jitsi Meet room'; + message = "Press SPACE or touch here to enter Jitsi Meet room"; } - layoutManager.addActionButton('jitsiRoom', message.toString(), () => { - openJitsiRoomFunction(); - }, this.userInputManager); + layoutManager.addActionButton( + "jitsiRoom", + message.toString(), + () => { + openJitsiRoomFunction(); + }, + this.userInputManager + ); } else { openJitsiRoomFunction(); } } }); - this.gameMap.onPropertyChange('silent', (newValue, oldValue) => { - if (newValue === undefined || newValue === false || newValue === '') { + this.gameMap.onPropertyChange("silent", (newValue, oldValue) => { + if (newValue === undefined || newValue === false || newValue === "") { this.connection?.setSilent(false); } else { this.connection?.setSilent(true); } }); - this.gameMap.onPropertyChange('playAudio', (newValue, oldValue, allProps) => { + this.gameMap.onPropertyChange("playAudio", (newValue, oldValue, allProps) => { const volume = allProps.get(AUDIO_VOLUME_PROPERTY) as number | undefined; const loop = allProps.get(AUDIO_LOOP_PROPERTY) as boolean | undefined; - newValue === undefined ? audioManager.unloadAudio() : audioManager.playAudio(newValue, this.getMapDirUrl(), volume, loop); + newValue === undefined + ? audioManager.unloadAudio() + : audioManager.playAudio(newValue, this.getMapDirUrl(), volume, loop); }); // TODO: This legacy property should be removed at some point - this.gameMap.onPropertyChange('playAudioLoop', (newValue, oldValue) => { - newValue === undefined ? audioManager.unloadAudio() : audioManager.playAudio(newValue, this.getMapDirUrl(), undefined, true); + 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 === '') { + this.gameMap.onPropertyChange("zone", (newValue, oldValue) => { + if (newValue === undefined || newValue === false || newValue === "") { iframeListener.sendLeaveEvent(oldValue as string); - } else { iframeListener.sendEnterEvent(newValue as string); } @@ -809,123 +876,195 @@ export class GameScene extends DirtyScene { private listenToIframeEvents(): void { this.iframeSubscriptionList = []; - this.iframeSubscriptionList.push(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 = `