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/camera.md b/docs/maps/camera.md
new file mode 100644
index 00000000..ac25c843
--- /dev/null
+++ b/docs/maps/camera.md
@@ -0,0 +1,86 @@
+{.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. Edit this new object and click on **Add Property**, like this:
+
+
+
+
+
+5. Add a **bool** property of name *focusable*:
+
+
+
+
+
+6. 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_click_add_property.png b/docs/maps/images/camera/4_click_add_property.png
new file mode 100644
index 00000000..9aa96a2f
Binary files /dev/null and b/docs/maps/images/camera/4_click_add_property.png differ
diff --git a/docs/maps/images/camera/5_add_focusable_prop.png b/docs/maps/images/camera/5_add_focusable_prop.png
new file mode 100644
index 00000000..3ba1b955
Binary files /dev/null and b/docs/maps/images/camera/5_add_focusable_prop.png differ
diff --git a/docs/maps/images/camera/6_make_sure_checked.png b/docs/maps/images/camera/6_make_sure_checked.png
new file mode 100644
index 00000000..7fbcdb89
Binary files /dev/null and b/docs/maps/images/camera/6_make_sure_checked.png differ
diff --git a/docs/maps/images/camera/7_add_zoom_margin.png b/docs/maps/images/camera/7_add_zoom_margin.png
new file mode 100644
index 00000000..8e3f5256
Binary files /dev/null and b/docs/maps/images/camera/7_add_zoom_margin.png differ
diff --git a/docs/maps/images/camera/8_optional_zoom_margin_defined.png b/docs/maps/images/camera/8_optional_zoom_margin_defined.png
new file mode 100644
index 00000000..8b41d7d0
Binary files /dev/null and b/docs/maps/images/camera/8_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/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/IframeEvent.ts b/front/src/Api/Events/IframeEvent.ts
index 89230302..894e0c58 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";
import { isGetPropertyEvent } from "./GetPropertyEvent";
export interface TypedMessageEvent extends MessageEvent {
@@ -77,6 +78,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 3c6e8a07..16b6138c 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"],
@@ -420,6 +421,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/Components/Companion/Companion.svelte b/front/src/Components/Companion/Companion.svelte
new file mode 100644
index 00000000..54ee31ac
--- /dev/null
+++ b/front/src/Components/Companion/Companion.svelte
@@ -0,0 +1,50 @@
+
+
+
+
+
diff --git a/front/src/Components/Menu/ProfileSubMenu.svelte b/front/src/Components/Menu/ProfileSubMenu.svelte
index 711981d0..e178d32f 100644
--- a/front/src/Components/Menu/ProfileSubMenu.svelte
+++ b/front/src/Components/Menu/ProfileSubMenu.svelte
@@ -15,7 +15,8 @@
import btnProfileSubMenuCamera from "../images/btn-menu-profile-camera.svg";
import btnProfileSubMenuIdentity from "../images/btn-menu-profile-identity.svg";
import btnProfileSubMenuCompanion from "../images/btn-menu-profile-companion.svg";
- import btnProfileSubMenuWoka from "../images/btn-menu-profile-woka.svg";
+ import Woka from "../Woka/Woka.svelte";
+ import Companion from "../Companion/Companion.svelte";
function disableMenuStores() {
menuVisiblilityStore.set(false);
@@ -67,12 +68,12 @@
-->
-
+
Edit your Avatar
-
- Change your companion
+
+ Edit your companion
diff --git a/front/src/Components/Video/VideoMediaBox.svelte b/front/src/Components/Video/VideoMediaBox.svelte
index c0359bc5..a750b41e 100644
--- a/front/src/Components/Video/VideoMediaBox.svelte
+++ b/front/src/Components/Video/VideoMediaBox.svelte
@@ -8,6 +8,8 @@
import { showReportScreenStore } from "../../Stores/ShowReportScreenStore";
import { getColorByString, srcObject } from "./utils";
+ import Woka from "../Woka/Woka.svelte";
+
export let peer: VideoPeer;
let streamStore = peer.streamStore;
let name = peer.userName;
@@ -26,9 +28,15 @@
{#if $statusStore === "error"}
{/if}
- {#if !$constraintStore || $constraintStore.video === false}
- {name}
- {/if}
+
+
+ {peer.userName}
+
+
+
{#if $constraintStore && $constraintStore.audio === false}
{/if}
@@ -43,3 +51,21 @@
{/if}
+
+
diff --git a/front/src/Components/Woka/Woka.svelte b/front/src/Components/Woka/Woka.svelte
new file mode 100644
index 00000000..0106676e
--- /dev/null
+++ b/front/src/Components/Woka/Woka.svelte
@@ -0,0 +1,45 @@
+
+
+
+
+
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 2cd34b4c..f2fd27a3 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);
}
@@ -378,4 +477,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 72b7bd25..067447a9 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;
@@ -552,7 +551,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));
@@ -593,7 +598,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();
@@ -646,7 +651,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(
@@ -668,7 +673,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) => {
@@ -781,6 +785,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);
+ // });
+ // });
});
}
@@ -1388,6 +1428,7 @@ export class GameScene extends DirtyScene {
this.userInputManager.destroy();
this.pinchManager?.destroy();
this.emoteManager.destroy();
+ this.cameraManager.destroy();
this.peerStoreUnsubscribe();
this.emoteUnsubscribe();
this.emoteMenuUnsubscribe();
@@ -1419,7 +1460,7 @@ export class GameScene extends DirtyScene {
this.MapPlayers.remove(player);
});
- this.MapPlayersByKey = new Map();
+ this.MapPlayersByKey.clear();
}
private getExitUrl(layer: ITiledMapLayer): string | undefined {
@@ -1477,13 +1518,6 @@ export class GameScene extends DirtyScene {
}
}
- //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) {
@@ -1875,23 +1909,6 @@ export class GameScene extends DirtyScene {
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(
@@ -2001,6 +2018,9 @@ export class GameScene extends DirtyScene {
}
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 5c81786b..9841b36a 100644
--- a/front/src/WebRtc/CoWebsiteManager.ts
+++ b/front/src/WebRtc/CoWebsiteManager.ts
@@ -651,6 +651,7 @@ class CoWebsiteManager {
private fire(): void {
this._onResize.next();
waScaleManager.applyNewSize();
+ waScaleManager.refreshFocusOnTarget();
}
private fullscreen(): void {
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
index d957a2c2..d1ff3475 100644
--- a/front/src/types.ts
+++ b/front/src/types.ts
@@ -21,3 +21,34 @@ export interface IVirtualJoystick extends Phaser.GameObjects.GameObject {
visible: boolean;
createCursorKeys: () => CursorKeys;
}
+
+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 88114d6f..ccfdd6c0 100644
--- a/front/style/style.scss
+++ b/front/style/style.scss
@@ -54,8 +54,7 @@ body .message-info.warning{
left: calc(50% - 50px);
top: calc(50% - 50px);
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..dfb4967d
--- /dev/null
+++ b/front/tests/Stores/Utils/MapStoreTest.ts
@@ -0,0 +1,97 @@
+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
+ }>();
+
+ 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/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