diff --git a/back/src/Services/SocketManager.ts b/back/src/Services/SocketManager.ts index 5efae800..8989df75 100644 --- a/back/src/Services/SocketManager.ts +++ b/back/src/Services/SocketManager.ts @@ -97,6 +97,7 @@ export class SocketManager { } const roomJoinedMessage = new RoomJoinedMessage(); roomJoinedMessage.setTagList(joinRoomMessage.getTagList()); + roomJoinedMessage.setUserroomtoken(joinRoomMessage.getUserroomtoken()); for (const [itemId, item] of room.getItemsState().entries()) { const itemStateMessage = new ItemStateMessage(); diff --git a/back/src/Services/VariablesManager.ts b/back/src/Services/VariablesManager.ts index 00aac3dc..6ec1cc3a 100644 --- a/back/src/Services/VariablesManager.ts +++ b/back/src/Services/VariablesManager.ts @@ -1,12 +1,7 @@ /** * Handles variables shared between the scripting API and the server. */ -import { - ITiledMap, - ITiledMapLayer, - ITiledMapObject, - ITiledMapObjectLayer, -} from "@workadventure/tiled-map-type-guard/dist"; +import { ITiledMap, ITiledMapLayer, ITiledMapObject } from "@workadventure/tiled-map-type-guard/dist"; import { User } from "_Model/User"; import { variablesRepository } from "./Repository/VariablesRepository"; import { redisClient } from "./RedisClient"; diff --git a/docs/maps/api-player.md b/docs/maps/api-player.md index 39a13d9e..9af0b1c2 100644 --- a/docs/maps/api-player.md +++ b/docs/maps/api-player.md @@ -58,6 +58,34 @@ WA.onInit().then(() => { }) ``` +### Get the user-room token of the player + +``` +WA.player.userRoomToken: string; +``` + +The user-room token is available from the `WA.player.userRoomToken` property. + +This token can be used by third party services to authenticate a player and prove that the player is in a given room. +The token is generated by the administration panel linked to WorkAdventure. The token is a string and is depending on your implementation of the administration panel. +In WorkAdventure SAAS version, the token is a JWT token that contains information such as the player's room ID and its associated membership ID. + +If you are using the self-hosted version of WorkAdventure and you developed your own administration panel, the token can be anything. +By default, self-hosted versions of WorkAdventure don't come with an administration panel, so the token string will be empty. + +{.alert.alert-info} +A typical use-case for the user-room token is providing logo upload capabilities in a map. +The token can be used as a way to authenticate a WorkAdventure player and ensure he is indeed in the map and authorized to upload a logo. + +{.alert.alert-info} +You need to wait for the end of the initialization before accessing `WA.player.userRoomToken` + +```typescript +WA.onInit().then(() => { + console.log('Token: ', WA.player.userRoomToken); +}) +``` + ### Listen to player movement ``` WA.player.onPlayerMove(callback: HasPlayerMovedEventCallback): void; diff --git a/docs/maps/camera.md b/docs/maps/camera.md new file mode 100644 index 00000000..9e58fcad --- /dev/null +++ b/docs/maps/camera.md @@ -0,0 +1,92 @@ +{.section-title.accent.text-primary} +# Working with camera + +## Focusable Zones + +It is possible to define special regions on the map that can make the camera zoom and center on themselves. We call them "Focusable Zones". When player gets inside, his camera view will be altered - focused, zoomed and locked on defined zone, like this: + +
+ +
+ +### Adding new **Focusable Zone**: + +1. Make sure you are editing an **Object Layer** + +
+ +
+ +2. Select **Insert Rectangle** tool + +
+ +
+ +3. Define new object wherever you want. For example, you can make your chilling room event cosier! + +
+ +
+ +4. Make sure your object is of type "zone"! + +
+ +
+ +5. Edit this new object and click on **Add Property**, like this: + +
+ +
+ +6. Add a **bool** property of name *focusable*: + +
+ +
+ +7. Make sure it's checked! :) + +
+ +
+ +All should be set up now and your new **Focusable Zone** should be working fine! + +### Defining custom zoom margin: + +If you want, you can add an additional property to control how much should the camera zoom onto focusable zone. + +1. Like before, click on **Add Property** + +
+ +
+ +2. Add a **float** property of name *zoom_margin*: + +
+ +
+ +2. Define how much (in percentage value) should the zoom be decreased: + +
+ +
+ + For example, if you define your zone as a 300x200 rectangle, setting this property to 0.5 *(50%)* means the camera will try to fit within the viewport the entire zone + margin of 50% of its dimensions, so 450x300. + + - No margin defined + +
+ +
+ + - Margin set to **0.35** + +
+ +
\ No newline at end of file diff --git a/docs/maps/images/camera/0_focusable_zone.png b/docs/maps/images/camera/0_focusable_zone.png new file mode 100644 index 00000000..8b54f11f Binary files /dev/null and b/docs/maps/images/camera/0_focusable_zone.png differ diff --git a/docs/maps/images/camera/1_object_layer.png b/docs/maps/images/camera/1_object_layer.png new file mode 100644 index 00000000..6f57d0ae Binary files /dev/null and b/docs/maps/images/camera/1_object_layer.png differ diff --git a/docs/maps/images/camera/2_rectangle_zone.png b/docs/maps/images/camera/2_rectangle_zone.png new file mode 100644 index 00000000..9b0b9cda Binary files /dev/null and b/docs/maps/images/camera/2_rectangle_zone.png differ diff --git a/docs/maps/images/camera/3_define_new_zone.png b/docs/maps/images/camera/3_define_new_zone.png new file mode 100644 index 00000000..226028eb Binary files /dev/null and b/docs/maps/images/camera/3_define_new_zone.png differ diff --git a/docs/maps/images/camera/4_add_zone_type.png b/docs/maps/images/camera/4_add_zone_type.png new file mode 100644 index 00000000..0416d1e4 Binary files /dev/null and b/docs/maps/images/camera/4_add_zone_type.png differ diff --git a/docs/maps/images/camera/5_click_add_property.png b/docs/maps/images/camera/5_click_add_property.png new file mode 100644 index 00000000..9aa96a2f Binary files /dev/null and b/docs/maps/images/camera/5_click_add_property.png differ diff --git a/docs/maps/images/camera/6_add_focusable_prop.png b/docs/maps/images/camera/6_add_focusable_prop.png new file mode 100644 index 00000000..3ba1b955 Binary files /dev/null and b/docs/maps/images/camera/6_add_focusable_prop.png differ diff --git a/docs/maps/images/camera/7_make_sure_checked.png b/docs/maps/images/camera/7_make_sure_checked.png new file mode 100644 index 00000000..7fbcdb89 Binary files /dev/null and b/docs/maps/images/camera/7_make_sure_checked.png differ diff --git a/docs/maps/images/camera/8_add_zoom_margin.png b/docs/maps/images/camera/8_add_zoom_margin.png new file mode 100644 index 00000000..8e3f5256 Binary files /dev/null and b/docs/maps/images/camera/8_add_zoom_margin.png differ diff --git a/docs/maps/images/camera/9_optional_zoom_margin_defined.png b/docs/maps/images/camera/9_optional_zoom_margin_defined.png new file mode 100644 index 00000000..8b41d7d0 Binary files /dev/null and b/docs/maps/images/camera/9_optional_zoom_margin_defined.png differ diff --git a/docs/maps/images/camera/no_margin.png b/docs/maps/images/camera/no_margin.png new file mode 100644 index 00000000..b8c9dd18 Binary files /dev/null and b/docs/maps/images/camera/no_margin.png differ diff --git a/docs/maps/images/camera/with_margin.png b/docs/maps/images/camera/with_margin.png new file mode 100644 index 00000000..ffd057ea Binary files /dev/null and b/docs/maps/images/camera/with_margin.png differ diff --git a/docs/maps/menu.php b/docs/maps/menu.php index 0bf0a7f9..10a2f4c5 100644 --- a/docs/maps/menu.php +++ b/docs/maps/menu.php @@ -51,6 +51,12 @@ return [ 'markdown' => 'maps.website-in-map', 'editUrl' => 'https://github.com/thecodingmachine/workadventure/edit/develop/docs/maps/website-in-map.md', ], + [ + 'title' => 'Camera', + 'url' => '/map-building/camera.md', + 'markdown' => 'maps.camera', + 'editUrl' => 'https://github.com/thecodingmachine/workadventure/edit/develop/docs/maps/camera.md', + ], [ 'title' => 'Variables', 'url' => '/map-building/variables.md', diff --git a/docs/maps/scripting.md b/docs/maps/scripting.md index 8b11fe74..6da3ddbf 100644 --- a/docs/maps/scripting.md +++ b/docs/maps/scripting.md @@ -60,7 +60,7 @@ WA.chat.sendChatMessage('Hello world', 'Mr Robot'); The `WA` objects contains a number of useful methods enabling you to interact with the WorkAdventure game. For instance, `WA.chat.sendChatMessage` opens the chat and adds a message in it. -In your browser console, when you open the map, the chat message should be displayed right away. +The message should be displayed in the chat history as soon as you enter the room. ## Adding a script in an iFrame diff --git a/docs/maps/wa-maps.md b/docs/maps/wa-maps.md index 70581a57..6e84a251 100644 --- a/docs/maps/wa-maps.md +++ b/docs/maps/wa-maps.md @@ -98,13 +98,14 @@ The exception is the "collides" property that can only be set on tiles, but not By setting properties on the map itself, you can help visitors know more about the creators of the map. The following *map* properties are supported: -* `mapName` (string) -* `mapDescription` (string) -* `mapCopyright` (string) +* `mapName` (string): The name of your map +* `mapLink` (string): A link to your map, for example a repository +* `mapDescription` (string): A short description of your map +* `mapCopyright` (string): Copyright notice -And *each tileset* can also have a property called `tilesetCopyright` (string). +Each *tileset* can also have a property called `tilesetCopyright` (string). +If you are using audio files in your map, you can declare a layer property `audioCopyright` (string). Resulting in a "credit" page in the menu looking like this: ![](images/mapProperties.png){.document-img} - diff --git a/front/src/Api/Events/ChangeZoneEvent.ts b/front/src/Api/Events/ChangeZoneEvent.ts new file mode 100644 index 00000000..e7ca3668 --- /dev/null +++ b/front/src/Api/Events/ChangeZoneEvent.ts @@ -0,0 +1,11 @@ +import * as tg from "generic-type-guard"; + +export const isChangeZoneEvent = 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. + */ +export type ChangeZoneEvent = tg.GuardedType; diff --git a/front/src/Api/Events/GameStateEvent.ts b/front/src/Api/Events/GameStateEvent.ts index 112c2880..1f0f36ed 100644 --- a/front/src/Api/Events/GameStateEvent.ts +++ b/front/src/Api/Events/GameStateEvent.ts @@ -9,6 +9,7 @@ export const isGameStateEvent = new tg.IsInterface() startLayerName: tg.isUnion(tg.isString, tg.isNull), tags: tg.isArray(tg.isString), variables: tg.isObject, + userRoomToken: tg.isUnion(tg.isString, tg.isUndefined), }) .get(); /** diff --git a/front/src/Api/Events/IframeEvent.ts b/front/src/Api/Events/IframeEvent.ts index 081008c4..c338ddbe 100644 --- a/front/src/Api/Events/IframeEvent.ts +++ b/front/src/Api/Events/IframeEvent.ts @@ -28,6 +28,7 @@ import type { MessageReferenceEvent } from "./ui/TriggerActionMessageEvent"; import { isMessageReferenceEvent, isTriggerActionMessageEvent } from "./ui/TriggerActionMessageEvent"; import type { MenuRegisterEvent, UnregisterMenuEvent } from "./ui/MenuRegisterEvent"; import type { ChangeLayerEvent } from "./ChangeLayerEvent"; +import type { ChangeZoneEvent } from "./ChangeZoneEvent"; export interface TypedMessageEvent extends MessageEvent { data: T; @@ -76,6 +77,8 @@ export interface IframeResponseEventMap { leaveEvent: EnterLeaveEvent; enterLayerEvent: ChangeLayerEvent; leaveLayerEvent: ChangeLayerEvent; + enterZoneEvent: ChangeZoneEvent; + leaveZoneEvent: ChangeZoneEvent; buttonClickedEvent: ButtonClickedEvent; hasPlayerMoved: HasPlayerMovedEvent; menuItemClicked: MenuItemClickedEvent; diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index 3db35984..67b49344 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -31,6 +31,7 @@ import type { SetVariableEvent } from "./Events/SetVariableEvent"; import { ModifyEmbeddedWebsiteEvent, isEmbeddedWebsiteEvent } from "./Events/EmbeddedWebsiteEvent"; import { handleMenuRegistrationEvent, handleMenuUnregisterEvent } from "../Stores/MenuStore"; import type { ChangeLayerEvent } from "./Events/ChangeLayerEvent"; +import type { ChangeZoneEvent } from "./Events/ChangeZoneEvent"; type AnswererCallback = ( query: IframeQueryMap[T]["query"], @@ -414,6 +415,24 @@ class IframeListener { }); } + sendEnterZoneEvent(zoneName: string) { + this.postMessage({ + type: "enterZoneEvent", + data: { + name: zoneName, + } as ChangeZoneEvent, + }); + } + + sendLeaveZoneEvent(zoneName: string) { + this.postMessage({ + type: "leaveZoneEvent", + data: { + name: zoneName, + } as ChangeZoneEvent, + }); + } + hasPlayerMoved(event: HasPlayerMovedEvent) { if (this.sendPlayerMove) { this.postMessage({ diff --git a/front/src/Api/iframe/player.ts b/front/src/Api/iframe/player.ts index 078a1926..c46f3fbc 100644 --- a/front/src/Api/iframe/player.ts +++ b/front/src/Api/iframe/player.ts @@ -20,6 +20,12 @@ export const setTags = (_tags: string[]) => { let uuid: string | undefined; +let userRoomToken: string | undefined; + +export const setUserRoomToken = (token: string | undefined) => { + userRoomToken = token; +}; + export const setUuid = (_uuid: string | undefined) => { uuid = _uuid; }; @@ -67,6 +73,15 @@ export class WorkadventurePlayerCommands extends IframeApiContribution + import { gameManager } from "../../Phaser/Game/GameManager"; + import type { PictureStore } from "../../Stores/PictureStore"; + import { onDestroy } from "svelte"; + + export let userId: number; + export let placeholderSrc: string; + export let width: string = "62px"; + export let height: string = "62px"; + + const gameScene = gameManager.getCurrentGameScene(); + let companionWokaPictureStore: PictureStore | undefined; + if (userId === -1) { + companionWokaPictureStore = gameScene.CurrentPlayer.companion?.pictureStore; + } else { + companionWokaPictureStore = gameScene.MapPlayersByKey.getNestedStore( + userId, + (item) => item.companion?.pictureStore + ); + } + + let src = placeholderSrc; + + if (companionWokaPictureStore) { + const unsubscribe = companionWokaPictureStore.subscribe((source) => { + src = source ?? placeholderSrc; + }); + + onDestroy(unsubscribe); + } + + + + + diff --git a/front/src/Components/Menu/AboutRoomSubMenu.svelte b/front/src/Components/Menu/AboutRoomSubMenu.svelte index 666183e0..2bbb4d3c 100644 --- a/front/src/Components/Menu/AboutRoomSubMenu.svelte +++ b/front/src/Components/Menu/AboutRoomSubMenu.svelte @@ -6,11 +6,14 @@ let expandedMapCopyright = false; let expandedTilesetCopyright = false; + let expandedAudioCopyright = false; let mapName: string = ""; + let mapLink: string = ""; let mapDescription: string = ""; let mapCopyright: string = "The map creator did not declare a copyright for the map."; let tilesetCopyright: string[] = []; + let audioCopyright: string[] = []; onMount(() => { if (gameScene.mapFile.properties !== undefined) { @@ -18,6 +21,10 @@ if (propertyName !== undefined && typeof propertyName.value === "string") { mapName = propertyName.value; } + const propertyLink = gameScene.mapFile.properties.find((property) => property.name === "mapLink"); + if (propertyLink !== undefined && typeof propertyLink.value === "string") { + mapLink = propertyLink.value; + } const propertyDescription = gameScene.mapFile.properties.find( (property) => property.name === "mapDescription" ); @@ -36,7 +43,18 @@ (property) => property.name === "tilesetCopyright" ); if (propertyTilesetCopyright !== undefined && typeof propertyTilesetCopyright.value === "string") { - tilesetCopyright = [...tilesetCopyright, propertyTilesetCopyright.value]; //Assignment needed to trigger Svelte's reactivity + // Assignment needed to trigger Svelte's reactivity + tilesetCopyright = [...tilesetCopyright, propertyTilesetCopyright.value]; + } + } + } + + for (const layer of gameScene.mapFile.layers) { + if (layer.type && layer.type === "tilelayer" && layer.properties) { + const propertyAudioCopyright = layer.properties.find((property) => property.name === "audioCopyright"); + if (propertyAudioCopyright !== undefined && typeof propertyAudioCopyright.value === "string") { + // Assignment needed to trigger Svelte's reactivity + audioCopyright = [...audioCopyright, propertyAudioCopyright.value]; } } } @@ -48,6 +66,9 @@

{mapName}

{mapDescription}

+ {#if mapLink} +

> link to this map <

+ {/if}

(expandedMapCopyright = !expandedMapCopyright)}> Copyrights of the map

@@ -60,8 +81,21 @@

{copyright}

{:else}

- The map creator did not declare a copyright for the tilesets. Warning, This doesn't mean that those - tilesets have no license. + The map creator did not declare a copyright for the tilesets. This doesn't mean that those tilesets + have no license. +

+ {/each} +
+

(expandedAudioCopyright = !expandedAudioCopyright)}> + Copyrights of audio files +

+ diff --git a/front/src/Components/Menu/MenuIcon.svelte b/front/src/Components/Menu/MenuIcon.svelte index bf34658f..bb5a2df2 100644 --- a/front/src/Components/Menu/MenuIcon.svelte +++ b/front/src/Components/Menu/MenuIcon.svelte @@ -1,6 +1,6 @@ + + + + diff --git a/front/src/Connexion/RoomConnection.ts b/front/src/Connexion/RoomConnection.ts index 4a4eea6e..9e4025b1 100644 --- a/front/src/Connexion/RoomConnection.ts +++ b/front/src/Connexion/RoomConnection.ts @@ -68,6 +68,7 @@ export class RoomConnection implements RoomConnection { private static websocketFactory: null | ((url: string) => any) = null; // eslint-disable-line @typescript-eslint/no-explicit-any private closed: boolean = false; private tags: string[] = []; + private _userRoomToken: string | undefined; // eslint-disable-next-line @typescript-eslint/no-explicit-any public static setWebsocketFactory(websocketFactory: (url: string) => any): void { @@ -211,6 +212,7 @@ export class RoomConnection implements RoomConnection { this.userId = roomJoinedMessage.getCurrentuserid(); this.tags = roomJoinedMessage.getTagList(); + this._userRoomToken = roomJoinedMessage.getUserroomtoken(); this.dispatch(EventMessage.CONNECT, { connection: this, @@ -713,4 +715,8 @@ export class RoomConnection implements RoomConnection { public getAllTags(): string[] { return this.tags; } + + public get userRoomToken(): string | undefined { + return this._userRoomToken; + } } diff --git a/front/src/Phaser/Companion/Companion.ts b/front/src/Phaser/Companion/Companion.ts index 75eb844f..80b0236e 100644 --- a/front/src/Phaser/Companion/Companion.ts +++ b/front/src/Phaser/Companion/Companion.ts @@ -1,6 +1,9 @@ import Sprite = Phaser.GameObjects.Sprite; import Container = Phaser.GameObjects.Container; import { PlayerAnimationDirections, PlayerAnimationTypes } from "../Player/Animation"; +import { TexturesHelper } from "../Helpers/TexturesHelper"; +import { Writable, writable } from "svelte/store"; +import type { PictureStore } from "../../Stores/PictureStore"; export interface CompanionStatus { x: number; @@ -21,6 +24,7 @@ export class Companion extends Container { private companionName: string; private direction: PlayerAnimationDirections; private animationType: PlayerAnimationTypes; + private readonly _pictureStore: Writable; constructor(scene: Phaser.Scene, x: number, y: number, name: string, texturePromise: Promise) { super(scene, x + 14, y + 4); @@ -35,10 +39,14 @@ export class Companion extends Container { this.animationType = PlayerAnimationTypes.Idle; this.companionName = name; + this._pictureStore = writable(undefined); texturePromise.then((resource) => { this.addResource(resource); this.invisible = false; + return this.getSnapshot().then((htmlImageElementSrc) => { + this._pictureStore.set(htmlImageElementSrc); + }); }); this.scene.physics.world.enableBody(this); @@ -123,6 +131,22 @@ export class Companion extends Container { }; } + public async getSnapshot(): Promise { + const sprites = Array.from(this.sprites.values()).map((sprite) => { + return { sprite, frame: 1 }; + }); + return TexturesHelper.getSnapshot(this.scene, ...sprites).catch((reason) => { + console.warn(reason); + for (const sprite of this.sprites.values()) { + // it can be either cat or dog prefix + if (sprite.texture.key.includes("cat") || sprite.texture.key.includes("dog")) { + return this.scene.textures.getBase64(sprite.texture.key); + } + } + return "cat1"; + }); + } + private playAnimation(direction: PlayerAnimationDirections, type: PlayerAnimationTypes): void { if (this.invisible) return; @@ -220,4 +244,8 @@ export class Companion extends Container { super.destroy(); } + + public get pictureStore(): PictureStore { + return this._pictureStore; + } } diff --git a/front/src/Phaser/Entity/Character.ts b/front/src/Phaser/Entity/Character.ts index 1211a52d..2e0bd363 100644 --- a/front/src/Phaser/Entity/Character.ts +++ b/front/src/Phaser/Entity/Character.ts @@ -8,10 +8,12 @@ import { TextureError } from "../../Exception/TextureError"; import { Companion } from "../Companion/Companion"; import type { GameScene } from "../Game/GameScene"; import { DEPTH_INGAME_TEXT_INDEX } from "../Game/DepthIndexes"; -import { waScaleManager } from "../Services/WaScaleManager"; import type OutlinePipelinePlugin from "phaser3-rex-plugins/plugins/outlinepipeline-plugin.js"; import { isSilentStore } from "../../Stores/MediaStore"; -import { lazyLoadPlayerCharacterTextures } from "./PlayerTexturesLoadingManager"; +import { lazyLoadPlayerCharacterTextures, loadAllDefaultModels } from "./PlayerTexturesLoadingManager"; +import { TexturesHelper } from "../Helpers/TexturesHelper"; +import type { PictureStore } from "../../Stores/PictureStore"; +import { Writable, writable } from "svelte/store"; const playerNameY = -25; @@ -37,6 +39,7 @@ export abstract class Character extends Container { private emote: Phaser.GameObjects.DOMElement | null = null; private emoteTween: Phaser.Tweens.Tween | null = null; scene: GameScene; + private readonly _pictureStore: Writable; constructor( scene: GameScene, @@ -57,6 +60,7 @@ export abstract class Character extends Container { this.invisible = true; this.sprites = new Map(); + this._pictureStore = writable(undefined); //textures are inside a Promise in case they need to be lazyloaded before use. texturesPromise @@ -64,6 +68,9 @@ export abstract class Character extends Container { this.addTextures(textures, frame); this.invisible = false; this.playAnimation(direction, moving); + return this.getSnapshot().then((htmlImageElementSrc) => { + this._pictureStore.set(htmlImageElementSrc); + }); }) .catch(() => { return lazyLoadPlayerCharacterTextures(scene.load, ["color_22", "eyes_23"]).then((textures) => { @@ -117,8 +124,20 @@ export abstract class Character extends Container { } } - private getOutlinePlugin(): OutlinePipelinePlugin | undefined { - return this.scene.plugins.get("rexOutlinePipeline") as unknown as OutlinePipelinePlugin | undefined; + private async getSnapshot(): Promise { + const sprites = Array.from(this.sprites.values()).map((sprite) => { + return { sprite, frame: 1 }; + }); + return TexturesHelper.getSnapshot(this.scene, ...sprites).catch((reason) => { + console.warn(reason); + for (const sprite of this.sprites.values()) { + // we can be sure that either predefined woka or body texture is at this point loaded + if (sprite.texture.key.includes("color") || sprite.texture.key.includes("male")) { + return this.scene.textures.getBase64(sprite.texture.key); + } + } + return "male1"; + }); } public addCompanion(name: string, texturePromise?: Promise): void { @@ -154,6 +173,10 @@ export abstract class Character extends Container { } } + private getOutlinePlugin(): OutlinePipelinePlugin | undefined { + return this.scene.plugins.get("rexOutlinePipeline") as unknown as OutlinePipelinePlugin | undefined; + } + private getPlayerAnimations(name: string): AnimationData[] { return [ { @@ -374,4 +397,8 @@ export abstract class Character extends Container { this.emote = null; this.playerName.setVisible(true); } + + public get pictureStore(): PictureStore { + return this._pictureStore; + } } diff --git a/front/src/Phaser/Game/CameraManager.ts b/front/src/Phaser/Game/CameraManager.ts new file mode 100644 index 00000000..19c4821a --- /dev/null +++ b/front/src/Phaser/Game/CameraManager.ts @@ -0,0 +1,178 @@ +import { Easing } from "../../types"; +import { HtmlUtils } from "../../WebRtc/HtmlUtils"; +import type { Box } from "../../WebRtc/LayoutManager"; +import type { Player } from "../Player/Player"; +import type { WaScaleManager } from "../Services/WaScaleManager"; +import type { GameScene } from "./GameScene"; + +export enum CameraMode { + Free = "Free", + Follow = "Follow", + Focus = "Focus", +} + +export class CameraManager extends Phaser.Events.EventEmitter { + private scene: GameScene; + private camera: Phaser.Cameras.Scene2D.Camera; + private cameraBounds: { x: number; y: number }; + private waScaleManager: WaScaleManager; + + private cameraMode: CameraMode = CameraMode.Free; + + private restoreZoomTween?: Phaser.Tweens.Tween; + private startFollowTween?: Phaser.Tweens.Tween; + + private cameraFollowTarget?: { x: number; y: number }; + + constructor(scene: GameScene, cameraBounds: { x: number; y: number }, waScaleManager: WaScaleManager) { + super(); + this.scene = scene; + + this.camera = scene.cameras.main; + this.cameraBounds = cameraBounds; + + this.waScaleManager = waScaleManager; + + this.initCamera(); + + this.bindEventHandlers(); + } + + public destroy(): void { + this.scene.game.events.off("wa-scale-manager:refresh-focus-on-target"); + super.destroy(); + } + + public getCamera(): Phaser.Cameras.Scene2D.Camera { + return this.camera; + } + + public enterFocusMode( + focusOn: { x: number; y: number; width: number; height: number }, + margin: number = 0, + duration: number = 1000 + ): void { + this.setCameraMode(CameraMode.Focus); + this.waScaleManager.saveZoom(); + this.waScaleManager.setFocusTarget(focusOn); + + this.restoreZoomTween?.stop(); + this.startFollowTween?.stop(); + const marginMult = 1 + margin; + const targetZoomModifier = this.waScaleManager.getTargetZoomModifierFor( + focusOn.width * marginMult, + focusOn.height * marginMult + ); + const currentZoomModifier = this.waScaleManager.zoomModifier; + const zoomModifierChange = targetZoomModifier - currentZoomModifier; + this.camera.stopFollow(); + this.cameraFollowTarget = undefined; + this.camera.pan( + focusOn.x + focusOn.width * 0.5 * marginMult, + focusOn.y + focusOn.height * 0.5 * marginMult, + duration, + Easing.SineEaseOut, + true, + (camera, progress, x, y) => { + this.waScaleManager.zoomModifier = currentZoomModifier + progress * zoomModifierChange; + } + ); + } + + public leaveFocusMode(player: Player): void { + this.waScaleManager.setFocusTarget(); + this.startFollow(player, 1000); + this.restoreZoom(1000); + } + + public startFollow(target: object | Phaser.GameObjects.GameObject, duration: number = 0): void { + this.cameraFollowTarget = target as { x: number; y: number }; + this.setCameraMode(CameraMode.Follow); + if (duration === 0) { + this.camera.startFollow(target, true); + return; + } + const oldPos = { x: this.camera.scrollX, y: this.camera.scrollY }; + this.startFollowTween = this.scene.tweens.addCounter({ + from: 0, + to: 1, + duration, + ease: Easing.SineEaseOut, + onUpdate: (tween: Phaser.Tweens.Tween) => { + if (!this.cameraFollowTarget) { + return; + } + const shiftX = + (this.cameraFollowTarget.x - this.camera.worldView.width * 0.5 - oldPos.x) * tween.getValue(); + const shiftY = + (this.cameraFollowTarget.y - this.camera.worldView.height * 0.5 - oldPos.y) * tween.getValue(); + this.camera.setScroll(oldPos.x + shiftX, oldPos.y + shiftY); + }, + onComplete: () => { + this.camera.startFollow(target, true); + }, + }); + } + + /** + * Updates the offset of the character compared to the center of the screen according to the layout manager + * (tries to put the character in the center of the remaining space if there is a discussion going on. + */ + public updateCameraOffset(array: Box): void { + const xCenter = (array.xEnd - array.xStart) / 2 + array.xStart; + const yCenter = (array.yEnd - array.yStart) / 2 + array.yStart; + + const game = HtmlUtils.querySelectorOrFail("#game canvas"); + // Let's put this in Game coordinates by applying the zoom level: + + this.camera.setFollowOffset( + ((xCenter - game.offsetWidth / 2) * window.devicePixelRatio) / this.scene.scale.zoom, + ((yCenter - game.offsetHeight / 2) * window.devicePixelRatio) / this.scene.scale.zoom + ); + } + + public isCameraLocked(): boolean { + return this.cameraMode === CameraMode.Focus; + } + + private setCameraMode(mode: CameraMode): void { + if (this.cameraMode === mode) { + return; + } + this.cameraMode = mode; + } + + private restoreZoom(duration: number = 0): void { + if (duration === 0) { + this.waScaleManager.zoomModifier = this.waScaleManager.getSaveZoom(); + return; + } + this.restoreZoomTween?.stop(); + this.restoreZoomTween = this.scene.tweens.addCounter({ + from: this.waScaleManager.zoomModifier, + to: this.waScaleManager.getSaveZoom(), + duration, + ease: Easing.SineEaseOut, + onUpdate: (tween: Phaser.Tweens.Tween) => { + this.waScaleManager.zoomModifier = tween.getValue(); + }, + }); + } + + private initCamera() { + this.camera = this.scene.cameras.main; + this.camera.setBounds(0, 0, this.cameraBounds.x, this.cameraBounds.y); + } + + private bindEventHandlers(): void { + this.scene.game.events.on( + "wa-scale-manager:refresh-focus-on-target", + (focusOn: { x: number; y: number; width: number; height: number }) => { + if (!focusOn) { + return; + } + this.camera.centerOn(focusOn.x + focusOn.width * 0.5, focusOn.y + focusOn.height * 0.5); + } + ); + } +} diff --git a/front/src/Phaser/Game/GameMap.ts b/front/src/Phaser/Game/GameMap.ts index 8fe0e329..6688acb8 100644 --- a/front/src/Phaser/Game/GameMap.ts +++ b/front/src/Phaser/Game/GameMap.ts @@ -1,8 +1,15 @@ -import type { ITiledMap, ITiledMapLayer, ITiledMapProperty } from "../Map/ITiledMap"; +import type { + ITiledMap, + ITiledMapLayer, + ITiledMapObject, + ITiledMapObjectLayer, + ITiledMapProperty, +} from "../Map/ITiledMap"; import { flattenGroupLayersMap } from "../Map/LayersFlattener"; import TilemapLayer = Phaser.Tilemaps.TilemapLayer; import { DEPTH_OVERLAY_INDEX } from "./DepthIndexes"; import { GameMapProperties } from "./GameMapProperties"; +import { MathUtils } from "../../Utils/MathUtils"; export type PropertyChangeCallback = ( newValue: string | number | boolean | undefined, @@ -15,24 +22,48 @@ export type layerChangeCallback = ( allLayersOnNewPosition: Array ) => void; +export type zoneChangeCallback = ( + zonesChangedByAction: Array, + allZonesOnNewPosition: Array +) => void; + /** * A wrapper around a ITiledMap interface to provide additional capabilities. * It is used to handle layer properties. */ export class GameMap { - // oldKey is the index of the previous tile. + /** + * oldKey is the index of the previous tile. + */ private oldKey: number | undefined; - // key is the index of the current tile. + /** + * key is the index of the current tile. + */ private key: number | undefined; + /** + * oldPosition is the previous position of the player. + */ + private oldPosition: { x: number; y: number } | undefined; + /** + * position is the current position of the player. + */ + private position: { x: number; y: number } | undefined; + private lastProperties = new Map(); private propertiesChangeCallbacks = new Map>(); + private enterLayerCallbacks = Array(); private leaveLayerCallbacks = Array(); + private enterZoneCallbacks = Array(); + private leaveZoneCallbacks = Array(); + private tileNameMap = new Map(); private tileSetPropertyMap: { [tile_index: number]: Array } = {}; public readonly flatLayers: ITiledMapLayer[]; + public readonly tiledObjects: ITiledMapObject[]; public readonly phaserLayers: TilemapLayer[] = []; + public readonly zones: ITiledMapObject[] = []; public exitUrls: Array = []; @@ -44,6 +75,9 @@ export class GameMap { terrains: Array ) { this.flatLayers = flattenGroupLayersMap(map); + this.tiledObjects = this.getObjectsFromLayers(this.flatLayers); + this.zones = this.tiledObjects.filter((object) => object.type === "zone"); + let depth = -2; for (const layer of this.flatLayers) { if (layer.type === "tilelayer") { @@ -88,6 +122,10 @@ export class GameMap { * This will trigger events if properties are changing. */ public setPosition(x: number, y: number) { + this.oldPosition = this.position; + this.position = { x, y }; + this.triggerZonesChange(); + this.oldKey = this.key; const xMap = Math.floor(x / this.map.tilewidth); @@ -126,7 +164,7 @@ export class GameMap { } } - private triggerLayersChange() { + private triggerLayersChange(): void { const layersByOldKey = this.oldKey ? this.getLayersByKey(this.oldKey) : []; const layersByNewKey = this.key ? this.getLayersByKey(this.key) : []; @@ -155,6 +193,53 @@ export class GameMap { } } + /** + * We use Tiled Objects with type "zone" as zones with defined x, y, width and height for easier event triggering. + */ + private triggerZonesChange(): void { + const zonesByOldPosition = this.oldPosition + ? this.zones.filter((zone) => { + if (!this.oldPosition) { + return false; + } + return MathUtils.isOverlappingWithRectangle(this.oldPosition, zone); + }) + : []; + + const zonesByNewPosition = this.position + ? this.zones.filter((zone) => { + if (!this.position) { + return false; + } + return MathUtils.isOverlappingWithRectangle(this.position, zone); + }) + : []; + + const enterZones = new Set(zonesByNewPosition); + const leaveZones = new Set(zonesByOldPosition); + + enterZones.forEach((zone) => { + if (leaveZones.has(zone)) { + leaveZones.delete(zone); + enterZones.delete(zone); + } + }); + + if (enterZones.size > 0) { + const zonesArray = Array.from(enterZones); + for (const callback of this.enterZoneCallbacks) { + callback(zonesArray, zonesByNewPosition); + } + } + + if (leaveZones.size > 0) { + const zonesArray = Array.from(leaveZones); + for (const callback of this.leaveZoneCallbacks) { + callback(zonesArray, zonesByNewPosition); + } + } + } + public getCurrentProperties(): Map { return this.lastProperties; } @@ -251,6 +336,20 @@ export class GameMap { this.leaveLayerCallbacks.push(callback); } + /** + * Registers a callback called when the user moves inside another zone. + */ + public onEnterZone(callback: zoneChangeCallback) { + this.enterZoneCallbacks.push(callback); + } + + /** + * Registers a callback called when the user moves outside another zone. + */ + public onLeaveZone(callback: zoneChangeCallback) { + this.leaveZoneCallbacks.push(callback); + } + public findLayer(layerName: string): ITiledMapLayer | undefined { return this.flatLayers.find((layer) => layer.name === layerName); } @@ -362,4 +461,17 @@ export class GameMap { this.trigger(oldPropName, oldPropValue, undefined, emptyProps); } } + + private getObjectsFromLayers(layers: ITiledMapLayer[]): ITiledMapObject[] { + const objects: ITiledMapObject[] = []; + + const objectLayers = layers.filter((layer) => layer.type === "objectgroup"); + for (const objectLayer of objectLayers) { + if (objectLayer.type === "objectgroup") { + objects.push(...objectLayer.objects); + } + } + + return objects; + } } diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index aba64202..ae89e2c3 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -1,7 +1,54 @@ import type { Subscription } from "rxjs"; +import AnimatedTiles from "phaser-animated-tiles"; +import { Queue } from "queue-typescript"; +import { get } from "svelte/store"; + import { userMessageManager } from "../../Administration/UserMessageManager"; -import { iframeListener } from "../../Api/IframeListener"; import { connectionManager } from "../../Connexion/ConnectionManager"; +import { CoWebsite, coWebsiteManager } from "../../WebRtc/CoWebsiteManager"; +import { urlManager } from "../../Url/UrlManager"; +import { mediaManager } from "../../WebRtc/MediaManager"; +import { UserInputManager } from "../UserInput/UserInputManager"; +import { gameManager } from "./GameManager"; +import { touchScreenManager } from "../../Touch/TouchScreenManager"; +import { PinchManager } from "../UserInput/PinchManager"; +import { waScaleManager } from "../Services/WaScaleManager"; +import { EmoteManager } from "./EmoteManager"; +import { soundManager } from "./SoundManager"; +import { SharedVariablesManager } from "./SharedVariablesManager"; +import { EmbeddedWebsiteManager } from "./EmbeddedWebsiteManager"; + +import { lazyLoadPlayerCharacterTextures, loadCustomTexture } from "../Entity/PlayerTexturesLoadingManager"; +import { lazyLoadCompanionResource } from "../Companion/CompanionTexturesLoadingManager"; +import { ON_ACTION_TRIGGER_BUTTON } from "../../WebRtc/LayoutManager"; +import { iframeListener } from "../../Api/IframeListener"; +import { DEBUG_MODE, JITSI_PRIVATE_MODE, MAX_PER_GROUP, POSITION_DELAY } from "../../Enum/EnvironmentVariable"; +import { ProtobufClientUtils } from "../../Network/ProtobufClientUtils"; +import { Room } from "../../Connexion/Room"; +import { jitsiFactory } from "../../WebRtc/JitsiFactory"; +import { TextureError } from "../../Exception/TextureError"; +import { localUserStore } from "../../Connexion/LocalUserStore"; +import { HtmlUtils } from "../../WebRtc/HtmlUtils"; +import { SimplePeer } from "../../WebRtc/SimplePeer"; +import { Loader } from "../Components/Loader"; +import { RemotePlayer } from "../Entity/RemotePlayer"; +import { SelectCharacterScene, SelectCharacterSceneName } from "../Login/SelectCharacterScene"; +import { PlayerAnimationDirections } from "../Player/Animation"; +import { hasMovedEventName, Player, requestEmoteEventName } from "../Player/Player"; +import { ErrorSceneName } from "../Reconnecting/ErrorScene"; +import { ReconnectingSceneName } from "../Reconnecting/ReconnectingScene"; +import { GameMap } from "./GameMap"; +import { PlayerMovement } from "./PlayerMovement"; +import { PlayersPositionInterpolator } from "./PlayersPositionInterpolator"; +import { worldFullMessageStream } from "../../Connexion/WorldFullMessageStream"; +import { DirtyScene } from "./DirtyScene"; +import { TextUtils } from "../Components/TextUtils"; +import { joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey } from "../Components/MobileJoystick"; +import { StartPositionCalculator } from "./StartPositionCalculator"; +import { PropertyUtils } from "../Map/PropertyUtils"; +import { GameMapPropertiesListener } from "./GameMapPropertiesListener"; +import { analyticsClient } from "../../Administration/AnalyticsClient"; +import { GameMapProperties } from "./GameMapProperties"; import type { GroupCreatedUpdatedMessageInterface, MessageUserJoined, @@ -12,84 +59,35 @@ import type { 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 { CameraManager } from "./CameraManager"; +import type { HasPlayerMovedEvent } from "../../Api/Events/HasPlayerMovedEvent"; +import type { Character } from "../Entity/Character"; + +import { peerStore } from "../../Stores/PeerStore"; +import { biggestAvailableAreaStore } from "../../Stores/BiggestAvailableAreaStore"; +import { layoutManagerActionStore } from "../../Stores/LayoutManagerStore"; +import { playersStore } from "../../Stores/PlayersStore"; +import { emoteStore, emoteMenuStore } from "../../Stores/EmoteStore"; +import { userIsAdminStore } from "../../Stores/GameStore"; +import { contactPageStore } from "../../Stores/MenuStore"; +import { audioManagerFileStore, audioManagerVisibilityStore } from "../../Stores/AudioManagerStore"; + +import EVENT_TYPE = Phaser.Scenes.Events; 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 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; - +import FILE_LOAD_ERROR = Phaser.Loader.Events.FILE_LOAD_ERROR; +import { MapStore } from "../../Stores/Utils/MapStore"; export interface GameSceneInitInterface { initPosition: PointInterface | null; reconnecting: boolean; @@ -129,7 +127,7 @@ export class GameScene extends DirtyScene { Terrains: Array; CurrentPlayer!: Player; MapPlayers!: Phaser.Physics.Arcade.Group; - MapPlayersByKey: Map = new Map(); + MapPlayersByKey: MapStore = new MapStore(); Map!: Phaser.Tilemaps.Tilemap; Objects!: Array; mapFile!: ITiledMap; @@ -198,6 +196,7 @@ export class GameScene extends DirtyScene { 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; @@ -550,7 +549,13 @@ export class GameScene extends DirtyScene { this.createCurrentPlayer(); this.removeAllRemotePlayers(); //cleanup the list of remote players in case the scene was rebooted - this.initCamera(); + 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)); @@ -591,7 +596,7 @@ export class GameScene extends DirtyScene { // From now, this game scene will be notified of reposition events this.biggestAvailableAreaStoreUnsubscribe = biggestAvailableAreaStore.subscribe((box) => - this.updateCameraOffset(box) + this.cameraManager.updateCameraOffset(box) ); new GameMapPropertiesListener(this, this.gameMap).register(); @@ -644,7 +649,7 @@ export class GameScene extends DirtyScene { * Initializes the connection to Pusher. */ private connect(): void { - const camera = this.cameras.main; + const camera = this.cameraManager.getCamera(); connectionManager .connectToRoomSocket( @@ -666,7 +671,6 @@ export class GameScene extends DirtyScene { this.connection = onConnect.connection; playersStore.connectToRoomConnection(this.connection); - userIsAdminStore.set(this.connection.hasTag("admin")); this.connection.onUserJoins((message: MessageUserJoined) => { @@ -779,6 +783,42 @@ export class GameScene extends DirtyScene { iframeListener.sendLeaveLayerEvent(layer.name); }); }); + + this.gameMap.onEnterZone((zones) => { + for (const zone of zones) { + const focusable = zone.properties?.find((property) => property.name === "focusable"); + if (focusable && focusable.value === true) { + const zoomMargin = zone.properties?.find((property) => property.name === "zoom_margin"); + this.cameraManager.enterFocusMode( + zone, + zoomMargin ? Math.max(0, Number(zoomMargin.value)) : undefined + ); + break; + } + } + zones.forEach((zone) => { + iframeListener.sendEnterZoneEvent(zone.name); + }); + }); + + this.gameMap.onLeaveZone((zones) => { + for (const zone of zones) { + const focusable = zone.properties?.find((property) => property.name === "focusable"); + if (focusable && focusable.value === true) { + this.cameraManager.leaveFocusMode(this.CurrentPlayer); + break; + } + } + zones.forEach((zone) => { + iframeListener.sendLeaveZoneEvent(zone.name); + }); + }); + + // this.gameMap.onLeaveLayer((layers) => { + // layers.forEach((layer) => { + // iframeListener.sendLeaveLayerEvent(layer.name); + // }); + // }); }); } @@ -1167,6 +1207,7 @@ ${escapedMessage} roomId: this.roomUrl, tags: this.connection ? this.connection.getAllTags() : [], variables: this.sharedVariablesManager.variables, + userRoomToken: this.connection ? this.connection.userRoomToken : "", }; }); this.iframeSubscriptionList.push( @@ -1369,6 +1410,7 @@ ${escapedMessage} this.userInputManager.destroy(); this.pinchManager?.destroy(); this.emoteManager.destroy(); + this.cameraManager.destroy(); this.peerStoreUnsubscribe(); this.emoteUnsubscribe(); this.emoteMenuUnsubscribe(); @@ -1400,7 +1442,7 @@ ${escapedMessage} this.MapPlayers.remove(player); }); - this.MapPlayersByKey = new Map(); + this.MapPlayersByKey.clear(); } private getExitUrl(layer: ITiledMapLayer): string | undefined { @@ -1458,13 +1500,6 @@ ${escapedMessage} } } - //todo: in a dedicated class/function? - initCamera() { - this.cameras.main.setBounds(0, 0, this.Map.widthInPixels, this.Map.heightInPixels); - this.cameras.main.startFollow(this.CurrentPlayer, true); - biggestAvailableAreaStore.recompute(); - } - createCollisionWithPlayer() { //add collision layer for (const phaserLayer of this.gameMap.phaserLayers) { @@ -1856,23 +1891,6 @@ ${escapedMessage} biggestAvailableAreaStore.recompute(); } - /** - * Updates the offset of the character compared to the center of the screen according to the layout manager - * (tries to put the character in the center of the remaining space if there is a discussion going on. - */ - private updateCameraOffset(array: Box): void { - const xCenter = (array.xEnd - array.xStart) / 2 + array.xStart; - const yCenter = (array.yEnd - array.yStart) / 2 + array.yStart; - - const game = HtmlUtils.querySelectorOrFail("#game canvas"); - // Let's put this in Game coordinates by applying the zoom level: - - this.cameras.main.setFollowOffset( - ((xCenter - game.offsetWidth / 2) * window.devicePixelRatio) / this.scale.zoom, - ((yCenter - game.offsetHeight / 2) * window.devicePixelRatio) / this.scale.zoom - ); - } - public startJitsi(roomName: string, jwt?: string): void { const allProps = this.gameMap.getCurrentProperties(); const jitsiConfig = this.safeParseJSONstring( @@ -1941,6 +1959,9 @@ ${escapedMessage} } zoomByFactor(zoomFactor: number) { + if (this.cameraManager.isCameraLocked()) { + return; + } waScaleManager.zoomModifier *= zoomFactor; biggestAvailableAreaStore.recompute(); } diff --git a/front/src/Phaser/Game/SharedVariablesManager.ts b/front/src/Phaser/Game/SharedVariablesManager.ts index 8f913765..5b5867dc 100644 --- a/front/src/Phaser/Game/SharedVariablesManager.ts +++ b/front/src/Phaser/Game/SharedVariablesManager.ts @@ -1,7 +1,7 @@ import type { RoomConnection } from "../../Connexion/RoomConnection"; import { iframeListener } from "../../Api/IframeListener"; import type { GameMap } from "./GameMap"; -import type { ITiledMapLayer, ITiledMapObject, ITiledMapObjectLayer } from "../Map/ITiledMap"; +import type { ITiledMapLayer, ITiledMapObject } from "../Map/ITiledMap"; import { GameMapProperties } from "./GameMapProperties"; interface Variable { diff --git a/front/src/Phaser/Helpers/TexturesHelper.ts b/front/src/Phaser/Helpers/TexturesHelper.ts new file mode 100644 index 00000000..348e957a --- /dev/null +++ b/front/src/Phaser/Helpers/TexturesHelper.ts @@ -0,0 +1,34 @@ +export class TexturesHelper { + public static async getSnapshot( + scene: Phaser.Scene, + ...sprites: { sprite: Phaser.GameObjects.Sprite; frame?: string | number }[] + ): Promise { + const rt = scene.make.renderTexture({}, false); + try { + for (const { sprite, frame } of sprites) { + if (frame) { + sprite.setFrame(frame); + } + rt.draw(sprite, sprite.displayWidth * 0.5, sprite.displayHeight * 0.5); + } + return new Promise((resolve, reject) => { + try { + rt.snapshot( + (url) => { + resolve((url as HTMLImageElement).src); + rt.destroy(); + }, + "image/png", + 1 + ); + } catch (error) { + rt.destroy(); + reject(error); + } + }); + } catch (error) { + rt.destroy(); + throw new Error("Could not get the snapshot"); + } + } +} diff --git a/front/src/Phaser/Items/Computer/computer.ts b/front/src/Phaser/Items/Computer/computer.ts index 4665c546..41fb6fc4 100644 --- a/front/src/Phaser/Items/Computer/computer.ts +++ b/front/src/Phaser/Items/Computer/computer.ts @@ -1,5 +1,4 @@ import * as Phaser from "phaser"; -import { Scene } from "phaser"; import Sprite = Phaser.GameObjects.Sprite; import type { ITiledMapObject } from "../../Map/ITiledMap"; import type { ItemFactoryInterface } from "../ItemFactoryInterface"; diff --git a/front/src/Phaser/Services/HdpiManager.ts b/front/src/Phaser/Services/HdpiManager.ts index 116f6816..9c4e9af4 100644 --- a/front/src/Phaser/Services/HdpiManager.ts +++ b/front/src/Phaser/Services/HdpiManager.ts @@ -94,7 +94,7 @@ export class HdpiManager { /** * We only accept integer but we make an exception for 1.5 */ - private getOptimalZoomLevel(realPixelNumber: number): number { + public getOptimalZoomLevel(realPixelNumber: number): number { const result = Math.sqrt(realPixelNumber / this.minRecommendedGamePixelsNumber); if (1.5 <= result && result < 2) { return 1.5; diff --git a/front/src/Phaser/Services/WaScaleManager.ts b/front/src/Phaser/Services/WaScaleManager.ts index 5ceaeb71..447b6a1f 100644 --- a/front/src/Phaser/Services/WaScaleManager.ts +++ b/front/src/Phaser/Services/WaScaleManager.ts @@ -5,13 +5,15 @@ import type { Game } from "../Game/Game"; import { ResizableScene } from "../Login/ResizableScene"; import { HtmlUtils } from "../../WebRtc/HtmlUtils"; -class WaScaleManager { +export class WaScaleManager { private hdpiManager: HdpiManager; private scaleManager!: ScaleManager; private game!: Game; private actualZoom: number = 1; private _saveZoom: number = 1; + private focusTarget?: { x: number; y: number; width: number; height: number }; + public constructor(private minGamePixelsNumber: number, private absoluteMinPixelNumber: number) { this.hdpiManager = new HdpiManager(minGamePixelsNumber, absoluteMinPixelNumber); } @@ -23,18 +25,14 @@ class WaScaleManager { public applyNewSize() { const { width, height } = coWebsiteManager.getGameSize(); - - let devicePixelRatio = 1; - if (window.devicePixelRatio) { - devicePixelRatio = window.devicePixelRatio; - } - + const devicePixelRatio = window.devicePixelRatio ?? 1; const { game: gameSize, real: realSize } = this.hdpiManager.getOptimalGameSize({ width: width * devicePixelRatio, height: height * devicePixelRatio, }); this.actualZoom = realSize.width / gameSize.width / devicePixelRatio; + this.scaleManager.setZoom(realSize.width / gameSize.width / devicePixelRatio); this.scaleManager.resize(gameSize.width, gameSize.height); @@ -59,6 +57,34 @@ class WaScaleManager { this.game.markDirty(); } + /** + * Use this in case of resizing while focusing on something + */ + public refreshFocusOnTarget(): void { + if (!this.focusTarget) { + return; + } + this.zoomModifier = this.getTargetZoomModifierFor(this.focusTarget.width, this.focusTarget.height); + this.game.events.emit("wa-scale-manager:refresh-focus-on-target", this.focusTarget); + } + + public setFocusTarget(targetDimensions?: { x: number; y: number; width: number; height: number }): void { + this.focusTarget = targetDimensions; + } + + public getTargetZoomModifierFor(viewportWidth: number, viewportHeight: number) { + const { width: gameWidth, height: gameHeight } = coWebsiteManager.getGameSize(); + const devicePixelRatio = window.devicePixelRatio ?? 1; + + const { game: gameSize, real: realSize } = this.hdpiManager.getOptimalGameSize({ + width: gameWidth * devicePixelRatio, + height: gameHeight * devicePixelRatio, + }); + const desiredZoom = Math.min(realSize.width / viewportWidth, realSize.height / viewportHeight); + const realPixelNumber = gameWidth * devicePixelRatio * gameHeight * devicePixelRatio; + return desiredZoom / (this.hdpiManager.getOptimalZoomLevel(realPixelNumber) || 1); + } + public get zoomModifier(): number { return this.hdpiManager.zoomModifier; } @@ -72,6 +98,10 @@ class WaScaleManager { this._saveZoom = this.hdpiManager.zoomModifier; } + public getSaveZoom(): number { + return this._saveZoom; + } + public restoreZoom(): void { this.hdpiManager.zoomModifier = this._saveZoom; this.applyNewSize(); diff --git a/front/src/Stores/PictureStore.ts b/front/src/Stores/PictureStore.ts new file mode 100644 index 00000000..9908c942 --- /dev/null +++ b/front/src/Stores/PictureStore.ts @@ -0,0 +1,6 @@ +import type { Readable } from "svelte/store"; + +/** + * A store that contains the player/companion avatar picture + */ +export type PictureStore = Readable; diff --git a/front/src/Stores/PlayersStore.ts b/front/src/Stores/PlayersStore.ts index e6f5b1af..07c18b96 100644 --- a/front/src/Stores/PlayersStore.ts +++ b/front/src/Stores/PlayersStore.ts @@ -12,7 +12,7 @@ let idCount = 0; function createPlayersStore() { let players = new Map(); - const { subscribe, set, update } = writable(players); + const { subscribe, set, update } = writable>(players); return { subscribe, diff --git a/front/src/Stores/Utils/MapStore.ts b/front/src/Stores/Utils/MapStore.ts new file mode 100644 index 00000000..63c6c819 --- /dev/null +++ b/front/src/Stores/Utils/MapStore.ts @@ -0,0 +1,122 @@ +import type { Readable, Subscriber, Unsubscriber, Writable } from "svelte/store"; +import { get, readable, writable } from "svelte/store"; + +/** + * Is it a Map? Is it a Store? No! It's a MapStore! + * + * The MapStore behaves just like a regular JS Map, but... it is also a regular Svelte store. + * + * As a bonus, you can also get a store on any given key of the map. + * + * For instance: + * + * const mapStore = new MapStore(); + * mapStore.getStore('foo').subscribe((value) => { + * console.log('Foo key has been written to the store. New value: ', value); + * }); + * mapStore.set('foo', 'bar'); + * + * + * Even better, if the items stored in map contain stores, you can directly get the store to those values: + * + * const mapStore = new MapStore + * }>(); + * + * mapStore.getNestedStore('foo', item => item.nestedStore).subscribe((value) => { + * console.log('Foo key has been written to the store or the nested store has been updated. New value: ', value); + * }); + * mapStore.set('foo', { + * nestedStore: writable('bar') + * }); + * // Whenever the nested store is updated OR the 'foo' key is overwritten, the store returned by mapStore.getNestedStore + * // will be triggered. + */ +export class MapStore extends Map implements Readable> { + private readonly store = writable(this); + private readonly storesByKey = new Map>(); + + subscribe(run: Subscriber>, invalidate?: (value?: Map) => void): Unsubscriber { + return this.store.subscribe(run, invalidate); + } + + clear() { + super.clear(); + this.store.set(this); + this.storesByKey.forEach((store) => { + store.set(undefined); + }); + } + + delete(key: K): boolean { + const result = super.delete(key); + if (result) { + this.store.set(this); + this.storesByKey.get(key)?.set(undefined); + } + return result; + } + + set(key: K, value: V): this { + super.set(key, value); + this.store.set(this); + this.storesByKey.get(key)?.set(value); + return this; + } + + getStore(key: K): Readable { + const store = writable(this.get(key), () => { + return () => { + // No more subscribers! + this.storesByKey.delete(key); + }; + }); + this.storesByKey.set(key, store); + return store; + } + + /** + * Returns an "inner" store inside a value stored in the map. + */ + getNestedStore(key: K, accessor: (value: V) => Readable | undefined): Readable { + const initVal = this.get(key); + let initStore: Readable | undefined; + let initStoreValue: T | undefined; + if (initVal) { + initStore = accessor(initVal); + if (initStore !== undefined) { + initStoreValue = get(initStore); + } + } + + return readable(initStoreValue, (set) => { + const storeByKey = this.getStore(key); + + let unsubscribeDeepStore: Unsubscriber | undefined; + const unsubscribe = storeByKey.subscribe((newMapValue) => { + if (unsubscribeDeepStore) { + unsubscribeDeepStore(); + } + if (newMapValue === undefined) { + set(undefined); + } else { + const deepValueStore = accessor(newMapValue); + if (deepValueStore !== undefined) { + set(get(deepValueStore)); + + unsubscribeDeepStore = deepValueStore.subscribe((value) => { + set(value); + }); + } + } + }); + + return () => { + unsubscribe(); + if (unsubscribeDeepStore) { + unsubscribeDeepStore(); + } + }; + }); + } +} diff --git a/front/src/Utils/MathUtils.ts b/front/src/Utils/MathUtils.ts new file mode 100644 index 00000000..aea3bb11 --- /dev/null +++ b/front/src/Utils/MathUtils.ts @@ -0,0 +1,25 @@ +export class MathUtils { + /** + * + * @param p Position to check. + * @param r Rectangle to check the overlap against. + * @returns true is overlapping + */ + public static isOverlappingWithRectangle( + p: { x: number; y: number }, + r: { x: number; y: number; width: number; height: number } + ): boolean { + return this.isBetween(p.x, r.x, r.x + r.width) && this.isBetween(p.y, r.y, r.y + r.height); + } + + /** + * + * @param value Value to check + * @param min inclusive min value + * @param max inclusive max value + * @returns true if value is in + */ + public static isBetween(value: number, min: number, max: number): boolean { + return value >= min && value <= max; + } +} diff --git a/front/src/WebRtc/CoWebsiteManager.ts b/front/src/WebRtc/CoWebsiteManager.ts index 09de4b41..7a003604 100644 --- a/front/src/WebRtc/CoWebsiteManager.ts +++ b/front/src/WebRtc/CoWebsiteManager.ts @@ -642,6 +642,7 @@ class CoWebsiteManager { private fire(): void { this._onResize.next(); waScaleManager.applyNewSize(); + waScaleManager.refreshFocusOnTarget(); } private fullscreen(): void { diff --git a/front/src/iframe_api.ts b/front/src/iframe_api.ts index dcd10fdc..93415b0d 100644 --- a/front/src/iframe_api.ts +++ b/front/src/iframe_api.ts @@ -15,11 +15,11 @@ import ui from "./Api/iframe/ui"; import sound from "./Api/iframe/sound"; import room, { setMapURL, setRoomId } from "./Api/iframe/room"; import state, { initVariables } from "./Api/iframe/state"; -import player, { setPlayerName, setTags, setUuid } from "./Api/iframe/player"; +import player, { setPlayerName, setTags, setUserRoomToken, setUuid } from "./Api/iframe/player"; import type { ButtonDescriptor } from "./Api/iframe/Ui/ButtonDescriptor"; import type { Popup } from "./Api/iframe/Ui/Popup"; import type { Sound } from "./Api/iframe/Sound/Sound"; -import { answerPromises, queryWorkadventure, sendToWorkadventure } from "./Api/iframe/IframeApiContribution"; +import { answerPromises, queryWorkadventure } from "./Api/iframe/IframeApiContribution"; // Notify WorkAdventure that we are ready to receive data const initPromise = queryWorkadventure({ @@ -32,6 +32,7 @@ const initPromise = queryWorkadventure({ setTags(state.tags); setUuid(state.uuid); initVariables(state.variables as Map); + setUserRoomToken(state.userRoomToken); }); const wa = { diff --git a/front/src/index.ts b/front/src/index.ts index 3cb8d048..a2064cd8 100644 --- a/front/src/index.ts +++ b/front/src/index.ts @@ -144,10 +144,12 @@ window.addEventListener("resize", function (event) { coWebsiteManager.resetStyleMain(); waScaleManager.applyNewSize(); + waScaleManager.refreshFocusOnTarget(); }); coWebsiteManager.onResize.subscribe(() => { waScaleManager.applyNewSize(); + waScaleManager.refreshFocusOnTarget(); }); iframeListener.init(); diff --git a/front/src/types.ts b/front/src/types.ts new file mode 100644 index 00000000..90ebe31e --- /dev/null +++ b/front/src/types.ts @@ -0,0 +1,30 @@ +export enum Easing { + Linear = "Linear", + QuadEaseIn = "Quad.easeIn", + CubicEaseIn = "Cubic.easeIn", + QuartEaseIn = "Quart.easeIn", + QuintEaseIn = "Quint.easeIn", + SineEaseIn = "Sine.easeIn", + ExpoEaseIn = "Expo.easeIn", + CircEaseIn = "Circ.easeIn", + BackEaseIn = "Back.easeIn", + BounceEaseIn = "Bounce.easeIn", + QuadEaseOut = "Quad.easeOut", + CubicEaseOut = "Cubic.easeOut", + QuartEaseOut = "Quart.easeOut", + QuintEaseOut = "Quint.easeOut", + SineEaseOut = "Sine.easeOut", + ExpoEaseOut = "Expo.easeOut", + CircEaseOut = "Circ.easeOut", + BackEaseOut = "Back.easeOut", + BounceEaseOut = "Bounce.easeOut", + QuadEaseInOut = "Quad.easeInOut", + CubicEaseInOut = "Cubic.easeInOut", + QuartEaseInOut = "Quart.easeInOut", + QuintEaseInOut = "Quint.easeInOut", + SineEaseInOut = "Sine.easeInOut", + ExpoEaseInOut = "Expo.easeInOut", + CircEaseInOut = "Circ.easeInOut", + BackEaseInOut = "Back.easeInOut", + BounceEaseInOut = "Bounce.easeInOut", +} diff --git a/front/style/style.scss b/front/style/style.scss index 1654156d..89437a99 100644 --- a/front/style/style.scss +++ b/front/style/style.scss @@ -62,8 +62,7 @@ body .message-info.warning{ background-color: black; border-radius: 50%; text-align: center; - padding-top: 32px; - font-size: 28px; + font-size: 14px; color: white; overflow: hidden; } diff --git a/front/tests/Stores/Utils/MapStoreTest.ts b/front/tests/Stores/Utils/MapStoreTest.ts new file mode 100644 index 00000000..dddc83ee --- /dev/null +++ b/front/tests/Stores/Utils/MapStoreTest.ts @@ -0,0 +1,98 @@ +import "jasmine"; +import { MapStore } from "../../../src/Stores/Utils/MapStore"; +import type { Readable, Writable } from "svelte/store"; +import { get, writable } from "svelte/store"; + +describe("Main store", () => { + it("Set / delete / clear triggers main store updates", () => { + const mapStore = new MapStore(); + + let triggered = false; + + mapStore.subscribe((map) => { + triggered = true; + expect(map).toBe(mapStore); + }); + + expect(triggered).toBeTrue(); + triggered = false; + mapStore.set("foo", "bar"); + expect(triggered).toBeTrue(); + + triggered = false; + mapStore.delete("baz"); + expect(triggered).toBe(false); + mapStore.delete("foo"); + expect(triggered).toBe(true); + + triggered = false; + mapStore.clear(); + expect(triggered).toBe(true); + }); + + it("generates stores for keys with getStore", () => { + const mapStore = new MapStore(); + + let valueReceivedInStoreForFoo: string | undefined; + let valueReceivedInStoreForBar: string | undefined; + + mapStore.set("foo", "someValue"); + + mapStore.getStore("foo").subscribe((value) => { + valueReceivedInStoreForFoo = value; + }); + const unsubscribeBar = mapStore.getStore("bar").subscribe((value) => { + valueReceivedInStoreForBar = value; + }); + + expect(valueReceivedInStoreForFoo).toBe("someValue"); + expect(valueReceivedInStoreForBar).toBe(undefined); + mapStore.set("foo", "someOtherValue"); + expect(valueReceivedInStoreForFoo).toBe("someOtherValue"); + mapStore.delete("foo"); + expect(valueReceivedInStoreForFoo).toBe(undefined); + mapStore.set("bar", "baz"); + expect(valueReceivedInStoreForBar).toBe("baz"); + mapStore.clear(); + expect(valueReceivedInStoreForBar).toBe(undefined); + unsubscribeBar(); + mapStore.set("bar", "fiz"); + expect(valueReceivedInStoreForBar).toBe(undefined); + }); + + it("generates stores with getStoreByAccessor", () => { + const mapStore = new MapStore< + string, + { + foo: string; + store: Writable; + } + >(); + + const fooStore = mapStore.getNestedStore("foo", (value) => { + return value.store; + }); + + mapStore.set("foo", { + foo: "bar", + store: writable("init"), + }); + + expect(get(fooStore)).toBe("init"); + + mapStore.get("foo")?.store.set("newVal"); + + expect(get(fooStore)).toBe("newVal"); + + mapStore.set("foo", { + foo: "bar", + store: writable("anotherVal"), + }); + + expect(get(fooStore)).toBe("anotherVal"); + + mapStore.delete("foo"); + + expect(get(fooStore)).toBeUndefined(); + }); +}); diff --git a/maps/assets/skins/skin-blue.png b/maps/assets/skins/skin-blue.png new file mode 100644 index 00000000..ad6a976e Binary files /dev/null and b/maps/assets/skins/skin-blue.png differ diff --git a/maps/assets/skins/skin-green.png b/maps/assets/skins/skin-green.png new file mode 100644 index 00000000..bcf70d0e Binary files /dev/null and b/maps/assets/skins/skin-green.png differ diff --git a/maps/assets/skins/skin-yellow.png b/maps/assets/skins/skin-yellow.png new file mode 100644 index 00000000..f3214968 Binary files /dev/null and b/maps/assets/skins/skin-yellow.png differ diff --git a/maps/tests/Metadata/getCurrentRoom.js b/maps/tests/Metadata/getCurrentRoom.js index df3a995c..fa8e0226 100644 --- a/maps/tests/Metadata/getCurrentRoom.js +++ b/maps/tests/Metadata/getCurrentRoom.js @@ -4,6 +4,7 @@ WA.onInit().then(() => { console.log('Player name: ', WA.player.name); console.log('Player id: ', WA.player.id); console.log('Player tags: ', WA.player.tags); + console.log('Player token: ', WA.player.userRoomToken); }); WA.room.getTiledMap().then((data) => { diff --git a/maps/tests/Properties/mapProperties.json b/maps/tests/Properties/mapProperties.json index a58c002f..34178e38 100644 --- a/maps/tests/Properties/mapProperties.json +++ b/maps/tests/Properties/mapProperties.json @@ -8,6 +8,12 @@ "id":1, "name":"start", "opacity":1, + "properties":[ + { + "name":"audioCopyright", + "type":"string", + "value":"Copyright 2021 John Doe" + }], "type":"tilelayer", "visible":true, "width":10, @@ -124,4 +130,4 @@ "type":"map", "version":1.4, "width":10 -} \ No newline at end of file +} diff --git a/maps/tests/focusable_zone_map.json b/maps/tests/focusable_zone_map.json new file mode 100644 index 00000000..8a9aa6af --- /dev/null +++ b/maps/tests/focusable_zone_map.json @@ -0,0 +1,410 @@ +{ "compressionlevel":-1, + "height":17, + "infinite":false, + "layers":[ + { + "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 444, 444, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 444, 444, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 444, 444, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":17, + "id":6, + "name":"start", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":31, + "x":0, + "y":0 + }, + { + "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 443, 443, 443, 443, 443, 443, 443, 443, 443, 443, 443, 443, 443, 443, 443, 443, 443, 443, 443, 443, 443, 443, 443, 443, 443, 443, 443, 443, 443, 443, 443, 443, 0, 0, 0, 0, 0, 0, 443, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 443, 0, 0, 443, 443, 443, 0, 0, 443, 443, 0, 0, 0, 0, 0, 0, 443, 0, 0, 0, 443, 443, 0, 0, 0, 0, 443, 443, 0, 0, 0, 443, 0, 0, 0, 0, 0, 0, 0, 443, 443, 0, 0, 0, 0, 0, 0, 443, 0, 0, 0, 443, 443, 0, 0, 0, 0, 443, 443, 0, 0, 0, 443, 0, 0, 0, 0, 0, 0, 0, 443, 443, 0, 0, 0, 0, 0, 0, 443, 0, 0, 0, 443, 443, 0, 0, 0, 0, 443, 443, 0, 0, 0, 443, 0, 0, 0, 0, 0, 0, 0, 443, 443, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 443, 443, 0, 0, 0, 0, 443, 443, 0, 0, 0, 0, 0, 0, 0, 443, 443, 0, 0, 443, 443, 443, 443, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 443, 443, 0, 0, 443, 443, 443, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 443, 443, 0, 0, 443, 443, 443, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 443, 443, 0, 0, 443, 443, 443, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 443, 443, 443, 443, 443, 443, 0, 0, 0, 0, 443, 0, 0, 0, 0, 0, 0, 0, 443, 443, 443, 443, 0, 0, 0, 0, 0, 0, 0, 0, 0, 443, 443, 443, 443, 443, 443, 0, 0, 0, 0, 443, 0, 0, 0, 0, 0, 0, 0, 443, 443, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 443, 443, 443, 0, 0, 0, 0, 0, 0, 0, 443, 443, 443, 443, 443, 443, 443, 443, 443, 443, 443, 443, 443, 443, 443, 443, 443, 443, 443, 443, 443, 443, 443, 443, 443, 443, 443, 443, 443, 443, 443, 443, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":17, + "id":7, + "name":"collisions", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":31, + "x":0, + "y":0 + }, + { + "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 454, 454, 454, 454, 454, 454, 454, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 454, 454, 454, 454, 454, 454, 454, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 454, 454, 454, 454, 454, 454, 454, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 454, 454, 454, 454, 454, 454, 454, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 454, 454, 454, 454, 454, 454, 454, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 454, 454, 454, 454, 454, 454, 454, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 454, 454, 454, 454, 454, 454, 454, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 454, 454, 454, 454, 454, 454, 454, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 454, 454, 454, 454, 454, 454, 454, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 454, 454, 454, 454, 454, 454, 454, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 454, 454, 454, 454, 454, 454, 454, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":17, + "id":29, + "name":"jitsiMeetingRoom", + "opacity":1, + "properties":[ + { + "name":"jitsiRoom", + "type":"string", + "value":"MeetingRoom" + }], + "type":"tilelayer", + "visible":true, + "width":31, + "x":0, + "y":0 + }, + { + "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 454, 454, 454, 454, 454, 454, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 454, 454, 454, 454, 454, 454, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 454, 454, 454, 454, 454, 454, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 454, 454, 454, 454, 454, 454, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":17, + "id":38, + "name":"jitsiChillzone", + "opacity":1, + "properties":[ + { + "name":"jitsiRoom", + "type":"string", + "value":"ChillZone" + }, + { + "name":"jitsiTrigger", + "type":"string", + "value":"onaction" + }], + "type":"tilelayer", + "visible":true, + "width":31, + "x":0, + "y":0 + }, + { + "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 446, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":17, + "id":23, + "name":"clockZone", + "opacity":1, + "properties":[ + { + "name":"zone", + "type":"string", + "value":"clock" + }], + "type":"tilelayer", + "visible":true, + "width":31, + "x":0, + "y":0 + }, + { + "data":[201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 223, 223, 223, 223, 223, 223, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 223, 223, 223, 223, 223, 223, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 223, 223, 223, 223, 223, 223, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 223, 223, 223, 223, 223, 223, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201], + "height":17, + "id":4, + "name":"floor", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":31, + "x":0, + "y":0 + }, + { + "data":[49, 58, 58, 58, 58, 58, 58, 42, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 42, 57, 57, 57, 57, 57, 57, 57, 50, 45, 63, 63, 63, 63, 63, 63, 45, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 45, 63, 63, 63, 63, 63, 63, 63, 45, 45, 73, 73, 73, 73, 73, 73, 45, 73, 73, 73, 73, 73, 73, 73, 73, 73, 73, 73, 73, 73, 73, 45, 73, 73, 73, 73, 73, 73, 73, 45, 45, 0, 0, 0, 0, 0, 0, 45, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 45, 0, 0, 0, 0, 0, 0, 0, 45, 45, 0, 0, 0, 0, 0, 0, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 56, 0, 0, 0, 0, 0, 0, 0, 45, 45, 0, 0, 0, 0, 0, 0, 63, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 63, 0, 0, 0, 0, 0, 0, 0, 45, 45, 0, 0, 0, 0, 0, 0, 73, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 73, 0, 0, 0, 0, 0, 0, 0, 45, 45, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 45, 45, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 45, 45, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 45, 45, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 45, 45, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 46, 0, 0, 0, 0, 0, 0, 0, 45, 45, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 45, 0, 0, 0, 0, 0, 0, 0, 45, 45, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 45, 0, 0, 0, 0, 0, 0, 0, 45, 59, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 32, 58, 58, 58, 58, 58, 58, 58, 60, 83, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 84, 93, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 94], + "height":17, + "id":9, + "name":"walls", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":31, + "x":0, + "y":0 + }, + { + "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 293, 0, 0, 0, 0, 293, 0, 107, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 107, 0, 0, 128, 1, 2, 3, 0, 0, 0, 0, 304, 296, 297, 296, 297, 304, 0, 117, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 117, 0, 0, 0, 11, 12, 13, 0, 0, 0, 0, 315, 307, 308, 307, 308, 315, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 21, 22, 23, 0, 0, 0, 0, 243, 0, 0, 0, 0, 2147483943, 0, 0, 0, 325, 340, 340, 326, 0, 0, 325, 340, 340, 326, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 244, 0, 283, 283, 0, 2147483954, 0, 0, 0, 0, 340, 340, 0, 0, 0, 0, 340, 340, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 294, 294, 0, 0, 0, 0, 0, 325, 340, 340, 326, 0, 0, 325, 340, 340, 326, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 351, 351, 0, 0, 0, 0, 351, 351, 0, 0, 0, 0, 0, 0, 325, 273, 275, 326, 0, 0, 0, 394, 395, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 325, 2147483923, 275, 326, 0, 0, 0, 405, 406, 0, 0, 0, 0, 0, 0, 0, 0, 0, 333, 334, 333, 334, 333, 334, 0, 0, 0, 0, 0, 0, 0, 325, 2147483923, 275, 326, 0, 0, 0, 416, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 344, 345, 344, 345, 344, 345, 0, 0, 0, 0, 0, 0, 0, 325, 2147483923, 275, 326, 0, 0, 0, 427, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 217, 220, 220, 220, 220, 218, 0, 0, 0, 0, 0, 0, 0, 0, 284, 286, 0, 0, 0, 0, 438, 439, 0, 0, 0, 0, 0, 0, 0, 0, 0, 335, 336, 335, 336, 335, 336, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 282, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 346, 347, 346, 347, 346, 347, 0, 2147483811, 2147483810, 2147483809, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":17, + "id":1, + "name":"furniture", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":31, + "x":0, + "y":0 + }, + { + "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 232, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2147483909, 261, 0, 0, 0, 0, 2147483909, 261, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2147483909, 261, 0, 0, 0, 0, 2147483909, 261, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 166, 180, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 180, 0, 0, 0, 0, 0, 176, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 180, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 228, 231, 231, 231, 231, 229, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 282, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":17, + "id":33, + "name":"aboveFurniture", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":31, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"floorLayer", + "objects":[ + { + "height":64, + "id":4, + "name":"clockPopup", + "rotation":0, + "type":"", + "visible":true, + "width":128, + "x":512, + "y":0 + }, + { + "height":146.081567555252, + "id":9, + "name":"chillZone", + "properties":[ + { + "name":"focusable", + "type":"bool", + "value":true + }, + { + "name":"zoom_margin", + "type":"float", + "value":3 + }], + "rotation":0, + "type":"zone", + "visible":true, + "width":192, + "x":32, + "y":77.9184324447482 + }, + { + "height":416, + "id":11, + "name":"meetingZone", + "properties":[ + { + "name":"display_name", + "type":"string", + "value":"Brainstorm Zone!" + }, + { + "name":"focusable", + "type":"bool", + "value":true + }, + { + "name":"zoom_margin", + "type":"float", + "value":0.35 + }], + "rotation":0, + "type":"zone", + "visible":true, + "width":224, + "x":736, + "y":32 + }, + { + "height":66.6667, + "id":13, + "name":"", + "rotation":0, + "text": + { + "fontfamily":"Sans Serif", + "halign":"center", + "pixelsize":11, + "text":"Camera should show the whole zone. Zoom in before entering", + "valign":"center", + "wrap":true + }, + "type":"", + "visible":true, + "width":155.104, + "x":770.473518341308, + "y":126.688522863978 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }, + { + "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 329, 329, 0, 0, 0, 0, 329, 329, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 233, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 262, 263, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 206, 209, 209, 209, 209, 207, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 428, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2147483801, 2147483800, 2147483799, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":17, + "id":3, + "name":"abovePlayer1", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":31, + "x":0, + "y":0 + }, + { + "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 399, 400, 399, 400, 399, 400, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 410, 411, 410, 411, 410, 411, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":17, + "id":27, + "name":"abovePlayer2", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":31, + "x":0, + "y":0 + }, + { + "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 401, 402, 401, 402, 401, 402, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 412, 413, 412, 413, 412, 413, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":17, + "id":28, + "name":"abovePlayer3", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":31, + "x":0, + "y":0 + }], + "nextlayerid":39, + "nextobjectid":18, + "orientation":"orthogonal", + "properties":[ + { + "name":"mapCopyright", + "type":"string", + "value":"Credits: Valdo Romao https:\/\/www.linkedin.com\/in\/valdo-romao\/ \nLicense: CC-BY-SA 3.0 (http:\/\/creativecommons.org\/licenses\/by-sa\/3.0\/)" + }, + { + "name":"mapDescription", + "type":"string", + "value":"A perfect virtual office to get started with WorkAdventure!" + }, + { + "name":"mapImage", + "type":"string", + "value":"map.png" + }, + { + "name":"mapLink", + "type":"string", + "value":"https:\/\/thecodingmachine.github.io\/workadventure-map-starter-kit\/map.json" + }, + { + "name":"mapName", + "type":"string", + "value":"Starter kit" + }, + { + "name":"script", + "type":"string", + "value":"..\/dist\/script.js" + }], + "renderorder":"right-down", + "tiledversion":"1.7.2", + "tileheight":32, + "tilesets":[ + { + "columns":10, + "firstgid":1, + "image":"..\/assets\/tileset5_export.png", + "imageheight":320, + "imagewidth":320, + "margin":0, + "name":"tileset5_export", + "properties":[ + { + "name":"tilesetCopyright", + "type":"string", + "value":"\u00a9 2021 WorkAdventure \nLicence: WORKADVENTURE SPECIFIC RESOURCES LICENSE (see LICENSE.assets file)" + }], + "spacing":0, + "tilecount":100, + "tileheight":32, + "tilewidth":32 + }, + { + "columns":10, + "firstgid":101, + "image":"..\/assets\/tileset6_export.png", + "imageheight":320, + "imagewidth":320, + "margin":0, + "name":"tileset6_export", + "properties":[ + { + "name":"tilesetCopyright", + "type":"string", + "value":"\u00a9 2021 WorkAdventure \nLicence: WORKADVENTURE SPECIFIC RESOURCES LICENSE (see LICENSE.assets file)" + }], + "spacing":0, + "tilecount":100, + "tileheight":32, + "tilewidth":32 + }, + { + "columns":11, + "firstgid":201, + "image":"..\/assets\/tileset1.png", + "imageheight":352, + "imagewidth":352, + "margin":0, + "name":"tileset1", + "properties":[ + { + "name":"tilesetCopyright", + "type":"string", + "value":"\u00a9 2021 WorkAdventure \nLicence: WORKADVENTURE SPECIFIC RESOURCES LICENSE (see LICENSE.assets file)" + }], + "spacing":0, + "tilecount":121, + "tileheight":32, + "tilewidth":32 + }, + { + "columns":11, + "firstgid":322, + "image":"..\/assets\/tileset1-repositioning.png", + "imageheight":352, + "imagewidth":352, + "margin":0, + "name":"tileset1-repositioning", + "properties":[ + { + "name":"tilesetCopyright", + "type":"string", + "value":"\u00a9 2021 WorkAdventure \nLicence: WORKADVENTURE SPECIFIC RESOURCES LICENSE (see LICENSE.assets file)" + }], + "spacing":0, + "tilecount":121, + "tileheight":32, + "tilewidth":32 + }, + { + "columns":6, + "firstgid":443, + "image":"..\/assets\/Special_Zones.png", + "imageheight":64, + "imagewidth":192, + "margin":0, + "name":"Special_Zones", + "properties":[ + { + "name":"tilesetCopyright", + "type":"string", + "value":"\u00a9 2021 WorkAdventure \nLicence: WORKADVENTURE SPECIFIC RESOURCES LICENSE (see LICENSE.assets file)" + }], + "spacing":0, + "tilecount":12, + "tileheight":32, + "tiles":[ + { + "id":0, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }], + "tilewidth":32 + }], + "tilewidth":32, + "type":"map", + "version":"1.6", + "width":31 +} \ No newline at end of file diff --git a/maps/tests/index.html b/maps/tests/index.html index 068136ed..c920c876 100644 --- a/maps/tests/index.html +++ b/maps/tests/index.html @@ -104,6 +104,14 @@ Testing Emoji + + + Success Failure Pending + + + Focusable Zones + +

Iframe API

diff --git a/messages/protos/messages.proto b/messages/protos/messages.proto index 7a4d74d9..117ab582 100644 --- a/messages/protos/messages.proto +++ b/messages/protos/messages.proto @@ -198,6 +198,7 @@ message RoomJoinedMessage { int32 currentUserId = 4; repeated string tag = 5; repeated VariableMessage variable = 6; + string userRoomToken = 7; } message WebRtcStartMessage { @@ -300,6 +301,7 @@ message JoinRoomMessage { string IPAddress = 7; CompanionMessage companion = 8; string visitCardUrl = 9; + string userRoomToken = 10; } message UserJoinedZoneMessage { diff --git a/pusher/src/Controller/AuthenticateController.ts b/pusher/src/Controller/AuthenticateController.ts index 3e725da5..f063de10 100644 --- a/pusher/src/Controller/AuthenticateController.ts +++ b/pusher/src/Controller/AuthenticateController.ts @@ -288,6 +288,7 @@ export class AuthenticateController extends BaseController { messages: [], visitCardUrl: null, textures: [], + userRoomToken: undefined, }; try { data = await adminApi.fetchMemberDataByUuid(email, playUri, IPAddress); diff --git a/pusher/src/Controller/IoSocketController.ts b/pusher/src/Controller/IoSocketController.ts index 4d62f7fa..1f683b3b 100644 --- a/pusher/src/Controller/IoSocketController.ts +++ b/pusher/src/Controller/IoSocketController.ts @@ -29,7 +29,7 @@ import { emitInBatch } from "../Services/IoSocketHelpers"; import { ADMIN_API_URL, DISABLE_ANONYMOUS, SOCKET_IDLE_TIMER } from "../Enum/EnvironmentVariable"; import { Zone } from "_Model/Zone"; import { ExAdminSocketInterface } from "_Model/Websocket/ExAdminSocketInterface"; -import { CharacterTexture} from "../Messages/JsonMessages/CharacterTexture"; +import { CharacterTexture } from "../Messages/JsonMessages/CharacterTexture"; import { isAdminMessageInterface } from "../Model/Websocket/Admin/AdminMessages"; import Axios from "axios"; import { InvalidTokenError } from "../Controller/InvalidTokenError"; @@ -238,6 +238,7 @@ export class IoSocketController { let memberTags: string[] = []; let memberVisitCardUrl: string | null = null; let memberMessages: unknown; + let memberUserRoomToken: string | undefined; let memberTextures: CharacterTexture[] = []; const room = await socketManager.getOrCreateRoom(roomId); let userData: FetchMemberDataByUuidResponse = { @@ -248,6 +249,7 @@ export class IoSocketController { textures: [], messages: [], anonymous: true, + userRoomToken: undefined, }; if (ADMIN_API_URL) { try { @@ -286,6 +288,8 @@ export class IoSocketController { memberTags = userData.tags; memberVisitCardUrl = userData.visitCardUrl; memberTextures = userData.textures; + memberUserRoomToken = userData.userRoomToken; + if ( room.policyType === GameRoomPolicyTypes.USE_TAGS_POLICY && (userData.anonymous === true || !room.canAccess(memberTags)) @@ -335,6 +339,7 @@ export class IoSocketController { messages: memberMessages, tags: memberTags, visitCardUrl: memberVisitCardUrl, + userRoomToken: memberUserRoomToken, textures: memberTextures, position: { x: x, diff --git a/pusher/src/Model/Websocket/ExSocketInterface.ts b/pusher/src/Model/Websocket/ExSocketInterface.ts index ff5ed211..411d88fa 100644 --- a/pusher/src/Model/Websocket/ExSocketInterface.ts +++ b/pusher/src/Model/Websocket/ExSocketInterface.ts @@ -44,4 +44,5 @@ export interface ExSocketInterface extends WebSocket, Identificable { textures: CharacterTexture[]; backConnection: BackConnection; listenedZones: Set; + userRoomToken: string | undefined; } diff --git a/pusher/src/Services/AdminApi.ts b/pusher/src/Services/AdminApi.ts index 4410c29e..79177e1a 100644 --- a/pusher/src/Services/AdminApi.ts +++ b/pusher/src/Services/AdminApi.ts @@ -19,6 +19,7 @@ export interface FetchMemberDataByUuidResponse { textures: CharacterTexture[]; messages: unknown[]; anonymous?: boolean; + userRoomToken: string | undefined; } class AdminApi { diff --git a/pusher/src/Services/SocketManager.ts b/pusher/src/Services/SocketManager.ts index f66e20fc..a1777f0d 100644 --- a/pusher/src/Services/SocketManager.ts +++ b/pusher/src/Services/SocketManager.ts @@ -157,6 +157,11 @@ export class SocketManager implements ZoneEventListener { joinRoomMessage.setName(client.name); joinRoomMessage.setPositionmessage(ProtobufUtils.toPositionMessage(client.position)); joinRoomMessage.setTagList(client.tags); + + if (client.userRoomToken) { + joinRoomMessage.setUserroomtoken(client.userRoomToken); + } + if (client.visitCardUrl) { joinRoomMessage.setVisitcardurl(client.visitCardUrl); }