import type { Subscription } from "rxjs"; import { userMessageManager } from "../../Administration/UserMessageManager"; import { iframeListener } from "../../Api/IframeListener"; import { connectionManager } from "../../Connexion/ConnectionManager"; import type { GroupCreatedUpdatedMessageInterface, MessageUserJoined, MessageUserMovedInterface, MessageUserPositionInterface, OnConnectInterface, PointInterface, PositionInterface, RoomJoinedMessageInterface, } from "../../Connexion/ConnexionModels"; import { DEBUG_MODE, JITSI_PRIVATE_MODE, MAX_PER_GROUP, POSITION_DELAY } from "../../Enum/EnvironmentVariable"; import { Queue } from "queue-typescript"; import { Box, ON_ACTION_TRIGGER_BUTTON } from "../../WebRtc/LayoutManager"; import { CoWebsite, coWebsiteManager } from "../../WebRtc/CoWebsiteManager"; import type { UserMovedMessage } from "../../Messages/generated/messages_pb"; import { ProtobufClientUtils } from "../../Network/ProtobufClientUtils"; import type { RoomConnection } from "../../Connexion/RoomConnection"; import { Room } from "../../Connexion/Room"; import { jitsiFactory } from "../../WebRtc/JitsiFactory"; import { urlManager } from "../../Url/UrlManager"; import { TextureError } from "../../Exception/TextureError"; import { localUserStore } from "../../Connexion/LocalUserStore"; import { HtmlUtils } from "../../WebRtc/HtmlUtils"; import { mediaManager } from "../../WebRtc/MediaManager"; import { SimplePeer } from "../../WebRtc/SimplePeer"; import { Loader } from "../Components/Loader"; import { lazyLoadPlayerCharacterTextures, loadCustomTexture } from "../Entity/PlayerTexturesLoadingManager"; import { RemotePlayer } from "../Entity/RemotePlayer"; import type { ActionableItem } from "../Items/ActionableItem"; import type { ItemFactoryInterface } from "../Items/ItemFactoryInterface"; import { SelectCharacterScene, SelectCharacterSceneName } from "../Login/SelectCharacterScene"; import type { ITiledMap, ITiledMapLayer, ITiledMapProperty, ITiledMapObject, ITiledTileSet } from "../Map/ITiledMap"; import { PlayerAnimationDirections } from "../Player/Animation"; import { hasMovedEventName, Player, requestEmoteEventName } from "../Player/Player"; import { ErrorSceneName } from "../Reconnecting/ErrorScene"; import { ReconnectingSceneName } from "../Reconnecting/ReconnectingScene"; import { UserInputManager } from "../UserInput/UserInputManager"; import type { AddPlayerInterface } from "./AddPlayerInterface"; import { gameManager } from "./GameManager"; import { GameMap } from "./GameMap"; import { PlayerMovement } from "./PlayerMovement"; import { PlayersPositionInterpolator } from "./PlayersPositionInterpolator"; 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 { worldFullMessageStream } from "../../Connexion/WorldFullMessageStream"; import { lazyLoadCompanionResource } from "../Companion/CompanionTexturesLoadingManager"; import { DirtyScene } from "./DirtyScene"; import { TextUtils } from "../Components/TextUtils"; import { touchScreenManager } from "../../Touch/TouchScreenManager"; import { PinchManager } from "../UserInput/PinchManager"; import { joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey } from "../Components/MobileJoystick"; import { waScaleManager } from "../Services/WaScaleManager"; import { EmoteManager } from "./EmoteManager"; import { CameraManager } from "./CameraManager"; import EVENT_TYPE = Phaser.Scenes.Events; import type { HasPlayerMovedEvent } from "../../Api/Events/HasPlayerMovedEvent"; import AnimatedTiles from "phaser-animated-tiles"; import { StartPositionCalculator } from "./StartPositionCalculator"; import { soundManager } from "./SoundManager"; import { peerStore, screenSharingPeerStore } from "../../Stores/PeerStore"; import { videoFocusStore } from "../../Stores/VideoFocusStore"; import { biggestAvailableAreaStore } from "../../Stores/BiggestAvailableAreaStore"; import { SharedVariablesManager } from "./SharedVariablesManager"; import { playersStore } from "../../Stores/PlayersStore"; import { chatVisibilityStore } from "../../Stores/ChatStore"; import { emoteStore, emoteMenuStore } from "../../Stores/EmoteStore"; import { audioManagerFileStore, audioManagerVisibilityStore, audioManagerVolumeStore, } from "../../Stores/AudioManagerStore"; import { PropertyUtils } from "../Map/PropertyUtils"; import Tileset = Phaser.Tilemaps.Tileset; import { userIsAdminStore } from "../../Stores/GameStore"; import { layoutManagerActionStore } from "../../Stores/LayoutManagerStore"; import { EmbeddedWebsiteManager } from "./EmbeddedWebsiteManager"; import { GameMapPropertiesListener } from "./GameMapPropertiesListener"; import { analyticsClient } from "../../Administration/AnalyticsClient"; import { get } from "svelte/store"; import { contactPageStore } from "../../Stores/MenuStore"; import { GameMapProperties } from "./GameMapProperties"; import SpriteSheetFile = Phaser.Loader.FileTypes.SpriteSheetFile; export interface GameSceneInitInterface { initPosition: PointInterface | null; reconnecting: boolean; } interface InitUserPositionEventInterface { type: "InitUserPositionEvent"; event: MessageUserPositionInterface[]; } interface AddPlayerEventInterface { type: "AddPlayerEvent"; event: AddPlayerInterface; } interface RemovePlayerEventInterface { type: "RemovePlayerEvent"; userId: number; } interface UserMovedEventInterface { type: "UserMovedEvent"; event: MessageUserMovedInterface; } interface GroupCreatedUpdatedEventInterface { type: "GroupCreatedUpdatedEvent"; event: GroupCreatedUpdatedMessageInterface; } interface DeleteGroupEventInterface { type: "DeleteGroupEvent"; groupId: number; } export class GameScene extends DirtyScene { Terrains: Array; CurrentPlayer!: Player; MapPlayers!: Phaser.Physics.Arcade.Group; MapPlayersByKey: Map = new Map(); Map!: Phaser.Tilemaps.Tilemap; Objects!: Array; mapFile!: ITiledMap; animatedTiles!: AnimatedTiles; groups: Map; circleTexture!: CanvasTexture; circleRedTexture!: CanvasTexture; 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 connectionAnswerPromise: Promise; 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; private iframeSubscriptionList!: Array; private peerStoreUnsubscribe!: () => void; private emoteUnsubscribe!: () => void; private emoteMenuUnsubscribe!: () => void; private biggestAvailableAreaStoreUnsubscribe!: () => void; MapUrlFile: string; roomUrl: string; instance: string; currentTick!: number; lastSentTick!: number; // The last tick at which a position was sent. lastMoveEventSent: HasPlayerMovedEvent = { direction: "", moving: false, x: -1000, y: -1000, oldX: -1000, oldY: -1000, }; private gameMap!: GameMap; private actionableItems: Map = new Map(); // The item that can be selected by pressing the space key. private outlinedItem: ActionableItem | null = null; public userInputManager!: UserInputManager; private isReconnecting: boolean | undefined = undefined; private playerName!: string; private characterLayers!: string[]; private companion!: string | null; private messageSubscription: Subscription | null = null; private popUpElements: Map = new Map(); private originalMapUrl: string | undefined; private pinchManager: PinchManager | undefined; private mapTransitioning: boolean = false; //used to prevent transitions happening at the same time. private emoteManager!: EmoteManager; private cameraManager!: CameraManager; private preloading: boolean = true; private startPositionCalculator!: StartPositionCalculator; private sharedVariablesManager!: SharedVariablesManager; private objectsByType = new Map(); private embeddedWebsiteManager!: EmbeddedWebsiteManager; private loader: Loader; constructor(private room: Room, MapUrlFile: string, customKey?: string | undefined) { super({ key: customKey ?? room.key, }); this.Terrains = []; this.groups = new Map(); this.instance = room.getInstance(); this.MapUrlFile = MapUrlFile; this.roomUrl = room.key; this.createPromise = new Promise((resolve, reject): void => { this.createPromiseResolve = resolve; }); this.connectionAnswerPromise = new Promise((resolve, reject): void => { this.connectionAnswerPromiseResolve = resolve; }); this.loader = new Loader(this); } //hook preload scene preload(): void { //initialize frame event of scripting API this.listenToIframeEvents(); const localUser = localUserStore.getLocalUser(); const textures = localUser?.textures; if (textures) { for (const texture of textures) { loadCustomTexture(this.load, texture); } } 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-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 ) { this.originalMapUrl = this.MapUrlFile; 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); } ); 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 ) { this.originalMapUrl = this.MapUrlFile; 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); } ); // If the map has already been loaded as part of another GameScene, the "on load" event will not be triggered. // In this case, we check in the cache to see if the map is here and trigger the event manually. if (this.cache.tilemap.exists(this.MapUrlFile)) { const data = this.cache.tilemap.get(this.MapUrlFile); this.onMapLoad(data); } return; } //once preloading is over, we don't want loading errors to crash the game, so we need to disable this behavior after preloading. //if SpriteSheetFile (WOKA file) don't display error and give an access for user if (this.preloading && !(file instanceof SpriteSheetFile)) { //remove loader in progress this.loader.removeLoader(); //display an error scene 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.onMapLoad(data); }); //TODO strategy to add access token this.load.tilemapTiledJSON(this.MapUrlFile, this.MapUrlFile); // If the map has already been loaded as part of another GameScene, the "on load" event will not be triggered. // In this case, we check in the cache to see if the map is here and trigger the event manually. if (this.cache.tilemap.exists(this.MapUrlFile)) { const data = this.cache.tilemap.get(this.MapUrlFile); this.onMapLoad(data); } 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", }, }); //this function must stay at the end of preload function this.loader.addLoader(); } // FIXME: we need to put a "unknown" instead of a "any" and validate the structure of the JSON we are receiving. // eslint-disable-next-line @typescript-eslint/no-explicit-any private async onMapLoad(data: any): Promise { // 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("/")); this.mapFile.tilesets.forEach((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. this.objectsByType = new Map(); for (const layer of this.mapFile.layers) { if (layer.type === "objectgroup") { for (const object of layer.objects) { let objectsOfType: ITiledMapObject[] | undefined; if (!this.objectsByType.has(object.type)) { objectsOfType = new Array(); } else { objectsOfType = this.objectsByType.get(object.type); if (objectsOfType === undefined) { throw new Error("Unexpected object type not found"); } } objectsOfType.push(object); this.objectsByType.set(object.type, objectsOfType); } } } for (const [itemType, objectsOfType] of this.objectsByType) { // FIXME: we would ideally need for the loader to WAIT for the import to be performed, which means writing our own loader plugin. let itemFactory: ItemFactoryInterface; switch (itemType) { case "computer": { const module = await import("../Items/Computer/computer"); itemFactory = module.default; break; } default: continue; //throw new Error('Unsupported object type: "'+ itemType +'"'); } 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", () => { // 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 () => { itemFactory.create(this); const roomJoinedAnswer = await this.connectionAnswerPromise; for (const object of objectsOfType) { // TODO: we should pass here a factory to create sprites (maybe?) // Do we have a state for this object? const state = roomJoinedAnswer.items[object.id]; const actionableItem = itemFactory.factory(this, object, state); this.actionableItems.set(actionableItem.getId(), actionableItem); } }); }); } } //hook initialisation init(initData: GameSceneInitInterface) { if (initData.initPosition !== undefined) { this.initPosition = initData.initPosition; //todo: still used? } if (initData.initPosition !== undefined) { this.isReconnecting = initData.reconnecting; } } //hook create scene create(): void { this.preloading = false; this.trackDirtyAnims(); gameManager.gameSceneIsCreated(this); urlManager.pushRoomIdToUrl(this.room); analyticsClient.enteredRoom(this.room.id, this.room.group); contactPageStore.set(this.room.contactPage); if (touchScreenManager.supportTouchScreen) { this.pinchManager = new PinchManager(this); } this.messageSubscription = worldFullMessageStream.stream.subscribe((message) => this.showWorldFullError(message) ); const playerName = gameManager.getPlayerName(); if (!playerName) { throw "playerName is not set"; } this.playerName = playerName; this.characterLayers = gameManager.getCharacterLayers(); this.companion = gameManager.getCompanion(); //initialise map this.Map = this.add.tilemap(this.MapUrlFile); 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*/ ) ); }); //permit to set bound collision this.physics.world.setBounds(0, 0, this.Map.widthInPixels, this.Map.heightInPixels); this.embeddedWebsiteManager = new EmbeddedWebsiteManager(this); //add layer on map this.gameMap = new GameMap(this.mapFile, this.Map, this.Terrains); for (const layer of this.gameMap.flatLayers) { if (layer.type === "tilelayer") { const exitSceneUrl = this.getExitSceneUrl(layer); if (exitSceneUrl !== undefined) { this.loadNextGame( Room.getRoomPathFromExitSceneUrl(exitSceneUrl, window.location.toString(), this.MapUrlFile) ); } const exitUrl = this.getExitUrl(layer); if (exitUrl !== undefined) { this.loadNextGameFromExitUrl(exitUrl); } } if (layer.type === "objectgroup") { for (const object of layer.objects) { if (object.text) { TextUtils.createTextFromITiledMapObject(this, object); } if (object.type === "website") { // Let's load iframes in the map const url = PropertyUtils.mustFindStringProperty( GameMapProperties.URL, object.properties, 'in the "' + object.name + '" object of type "website"' ); const allowApi = PropertyUtils.findBooleanProperty( GameMapProperties.ALLOW_API, object.properties ); // TODO: add a "allow" property to iframe this.embeddedWebsiteManager.createEmbeddedWebsite( object.name, url, object.x, object.y, object.width, object.height, object.visible, allowApi ?? false, "" ); } } } } this.gameMap.exitUrls.forEach((exitUrl) => { this.loadNextGameFromExitUrl(exitUrl); }); this.startPositionCalculator = new StartPositionCalculator( this.gameMap, this.mapFile, this.initPosition, urlManager.getStartLayerNameFromUrl() ); //add entities this.Objects = new Array(); //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(); } //notify game manager can to create currentUser in map this.createCurrentPlayer(); this.removeAllRemotePlayers(); //cleanup the list of remote players in case the scene was rebooted this.cameraManager = new CameraManager( this, { x: this.Map.widthInPixels, y: this.Map.heightInPixels }, waScaleManager ); biggestAvailableAreaStore.recompute(); this.cameraManager.startFollow(this.CurrentPlayer); this.animatedTiles.init(this.Map); this.events.on("tileanimationupdate", () => (this.dirty = true)); this.initCirclesCanvas(); // Let's pause the scene if the connection is not established yet if (!this.room.isDisconnected()) { if (this.isReconnecting) { setTimeout(() => { this.scene.sleep(); this.scene.launch(ReconnectingSceneName); }, 0); } else if (this.connection === undefined) { // Let's wait 1 second before printing the "connecting" screen to avoid blinking setTimeout(() => { if (this.connection === undefined) { this.scene.sleep(); this.scene.launch(ReconnectingSceneName); } }, 1000); } } this.createPromiseResolve(); // Now, let's load the script, if any const scripts = this.getScriptUrls(this.mapFile); const scriptPromises = []; for (const script of scripts) { scriptPromises.push(iframeListener.registerScript(script)); } this.userInputManager.spaceEvent(() => { this.outlinedItem?.activate(); }); this.reposition(); // From now, this game scene will be notified of reposition events this.biggestAvailableAreaStoreUnsubscribe = biggestAvailableAreaStore.subscribe((box) => this.cameraManager.updateCameraOffset(box) ); new GameMapPropertiesListener(this, this.gameMap).register(); this.triggerOnMapLayerPropertyChange(); if (!this.room.isDisconnected()) { this.scene.sleep(); this.connect(); } this.emoteManager = new EmoteManager(this); let oldPeerNumber = 0; this.peerStoreUnsubscribe = peerStore.subscribe((peers) => { const newPeerNumber = peers.size; if (newPeerNumber > oldPeerNumber) { this.sound.play("audio-webrtc-in", { volume: 0.2, }); } else if (newPeerNumber < oldPeerNumber) { this.sound.play("audio-webrtc-out", { volume: 0.2, }); } oldPeerNumber = newPeerNumber; }); this.emoteUnsubscribe = emoteStore.subscribe((emote) => { if (emote) { this.CurrentPlayer?.playEmote(emote.url); this.connection?.emitEmoteEvent(emote.url); emoteStore.set(null); } }); this.emoteMenuUnsubscribe = emoteMenuStore.subscribe((emoteMenu) => { if (emoteMenu) { this.userInputManager.disableControls(); } else { this.userInputManager.restoreControls(); } }); Promise.all([this.connectionAnswerPromise as Promise, ...scriptPromises]).then(() => { this.scene.wake(); }); } /** * Initializes the connection to Pusher. */ private connect(): void { const camera = this.cameraManager.getCamera(); connectionManager .connectToRoomSocket( this.roomUrl, this.playerName, this.characterLayers, { ...this.startPositionCalculator.startPosition, }, { 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; playersStore.connectToRoomConnection(this.connection); userIsAdminStore.set(this.connection.hasTag("admin")); 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, userUuid: message.userUuid, }; this.addPlayer(userMessage); }); 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), }; this.updatePlayerPosition(messageUserMoved); }); this.connection.onUserLeft((userId: number) => { this.removePlayer(userId); }); this.connection.onGroupUpdatedOrCreated((groupPositionMessage: GroupCreatedUpdatedMessageInterface) => { this.shareGroupPosition(groupPositionMessage); }); 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.createSuccessorGameScene(true, true); }); 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); userMessageManager.setReceiveBanListener(this.bannedUser.bind(this)); //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); }); // Set up variables manager this.sharedVariablesManager = new SharedVariablesManager( this.connection, this.gameMap, onConnect.room.variables ); //this.initUsersPosition(roomJoinedMessage.users); this.connectionAnswerPromiseResolve(onConnect.room); // Analyze tags to find if we are admin. If yes, show console. if (this.scene.isSleeping()) { this.scene.stop(ReconnectingSceneName); } //init user position and play trigger to check layers properties this.gameMap.setPosition(this.CurrentPlayer.x, this.CurrentPlayer.y); // Init layer change listener this.gameMap.onEnterLayer((layers) => { layers.forEach((layer) => { iframeListener.sendEnterLayerEvent(layer.name); }); }); this.gameMap.onLeaveLayer((layers) => { layers.forEach((layer) => { iframeListener.sendLeaveLayerEvent(layer.name); }); }); // P.H. TODO: Send those events to the iframe? this.gameMap.onEnterZone((zones) => { for (const zone of zones) { for (const property of zone.properties ?? []) { if (property.name === "focusable" && property.value === true) { this.cameraManager.enterFocusMode(zone); break; } } } // zones.forEach((zone) => { // iframeListener.sendEnterLayerEvent(zone.name); // }); }); this.gameMap.onLeaveZone((zones) => { for (const zone of zones) { for (const property of zone.properties ?? []) { if (property.name === "focusable" && property.value === true) { this.cameraManager.leaveFocusMode(this.CurrentPlayer); break; } } } // zones.forEach((zone) => { // iframeListener.sendEnterLayerEvent(zone.name); // }); }); // this.gameMap.onLeaveLayer((layers) => { // layers.forEach((layer) => { // iframeListener.sendLeaveLayerEvent(layer.name); // }); // }); }); } //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" ); if (circleElement) { this.textures.remove("circleSprite-white"); } circleElement = Object.values(this.textures.list).find((object: Texture) => object.key === "circleSprite-red"); if (circleElement) { this.textures.remove("circleSprite-red"); } //create white circle canvas use to create sprite 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.stroke(); this.circleTexture.refresh(); //create red circle canvas use to create sprite 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.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 {}; } } private triggerOnMapLayerPropertyChange() { this.gameMap.onPropertyChange(GameMapProperties.EXIT_SCENE_URL, (newValue, oldValue) => { if (newValue) { this.onMapExit( Room.getRoomPathFromExitSceneUrl(newValue as string, window.location.toString(), this.MapUrlFile) ); } else { setTimeout(() => { layoutManagerActionStore.removeAction("roomAccessDenied"); }, 2000); } }); this.gameMap.onPropertyChange(GameMapProperties.EXIT_URL, (newValue, oldValue) => { if (newValue) { this.onMapExit(Room.getRoomPathFromExitUrl(newValue as string, window.location.toString())); } else { setTimeout(() => { layoutManagerActionStore.removeAction("roomAccessDenied"); }, 2000); } }); this.gameMap.onPropertyChange(GameMapProperties.JITSI_ROOM, (newValue, oldValue, allProps) => { if (newValue === undefined) { layoutManagerActionStore.removeAction("jitsi"); this.stopJitsi(); } else { const openJitsiRoomFunction = () => { const roomName = jitsiFactory.getRoomName(newValue.toString(), this.instance); const jitsiUrl = allProps.get(GameMapProperties.JITSI_URL) as string | undefined; if (JITSI_PRIVATE_MODE && !jitsiUrl) { const adminTag = allProps.get(GameMapProperties.JITSI_ADMIN_ROOM_TAG) as string | undefined; this.connection?.emitQueryJitsiJwtMessage(roomName, adminTag); } else { this.startJitsi(roomName, undefined); } layoutManagerActionStore.removeAction("jitsi"); }; const jitsiTriggerValue = allProps.get(GameMapProperties.JITSI_TRIGGER); if (jitsiTriggerValue && jitsiTriggerValue === ON_ACTION_TRIGGER_BUTTON) { let message = allProps.get(GameMapProperties.JITSI_TRIGGER_MESSAGE); if (message === undefined) { message = "Press SPACE or touch here to enter Jitsi Meet room"; } layoutManagerActionStore.addAction({ uuid: "jitsi", type: "message", message: message, callback: () => openJitsiRoomFunction(), userInputManager: this.userInputManager, }); } else { openJitsiRoomFunction(); } } }); this.gameMap.onPropertyChange(GameMapProperties.SILENT, (newValue, oldValue) => { if (newValue === undefined || newValue === false || newValue === "") { this.connection?.setSilent(false); this.CurrentPlayer.noSilent(); } else { this.connection?.setSilent(true); this.CurrentPlayer.isSilent(); } }); this.gameMap.onPropertyChange(GameMapProperties.PLAY_AUDIO, (newValue, oldValue, allProps) => { const volume = allProps.get(GameMapProperties.AUDIO_VOLUME) as number | undefined; const loop = allProps.get(GameMapProperties.AUDIO_LOOP) as boolean | undefined; newValue === undefined ? audioManagerFileStore.unloadAudio() : audioManagerFileStore.playAudio(newValue, this.getMapDirUrl(), volume, loop); audioManagerVisibilityStore.set(!(newValue === undefined)); }); // TODO: This legacy property should be removed at some point this.gameMap.onPropertyChange(GameMapProperties.PLAY_AUDIO_LOOP, (newValue, oldValue) => { newValue === undefined ? audioManagerFileStore.unloadAudio() : audioManagerFileStore.playAudio(newValue, this.getMapDirUrl(), undefined, true); audioManagerVisibilityStore.set(!(newValue === undefined)); }); // TODO: Legacy functionnality replace by layer change this.gameMap.onPropertyChange(GameMapProperties.ZONE, (newValue, oldValue) => { if (oldValue) { iframeListener.sendLeaveEvent(oldValue as string); } if (newValue) { iframeListener.sendEnterEvent(newValue as string); } }); } 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 = '