Merge branch 'develop' of github.com:thecodingmachine/workadventure into upgrade_typescript_4.5

This commit is contained in:
David Négrier 2021-12-16 16:07:17 +01:00
commit b2bcfde5b1
64 changed files with 1650 additions and 145 deletions

View file

@ -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();

View file

@ -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";

View file

@ -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;

92
docs/maps/camera.md Normal file
View file

@ -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:
<div class="px-5 card rounded d-inline-block">
<img class="document-img" src="images/camera/0_focusable_zone.png" alt="" />
</div>
### Adding new **Focusable Zone**:
1. Make sure you are editing an **Object Layer**
<div class="px-5 card rounded d-inline-block">
<img class="document-img" src="images/camera/1_object_layer.png" alt="" />
</div>
2. Select **Insert Rectangle** tool
<div class="px-5 card rounded d-inline-block">
<img class="document-img" src="images/camera/2_rectangle_zone.png" alt="" />
</div>
3. Define new object wherever you want. For example, you can make your chilling room event cosier!
<div class="px-5 card rounded d-inline-block">
<img class="document-img" src="images/camera/3_define_new_zone.png" alt="" />
</div>
4. Make sure your object is of type "zone"!
<div class="px-5 card rounded d-inline-block">
<img class="document-img" src="images/camera/4_add_zone_type.png" alt="" />
</div>
5. Edit this new object and click on **Add Property**, like this:
<div class="px-5 card rounded d-inline-block">
<img class="document-img" src="images/camera/5_click_add_property.png" alt="" />
</div>
6. Add a **bool** property of name *focusable*:
<div class="px-5 card rounded d-inline-block">
<img class="document-img" src="images/camera/6_add_focusable_prop.png" alt="" />
</div>
7. Make sure it's checked! :)
<div class="px-5 card rounded d-inline-block">
<img class="document-img" src="images/camera/7_make_sure_checked.png" alt="" />
</div>
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**
<div class="px-5 card rounded d-inline-block">
<img class="document-img" src="images/camera/5_click_add_property.png" alt="" />
</div>
2. Add a **float** property of name *zoom_margin*:
<div class="px-5 card rounded d-inline-block">
<img class="document-img" src="images/camera/8_add_zoom_margin.png" alt="" />
</div>
2. Define how much (in percentage value) should the zoom be decreased:
<div class="px-5 card rounded d-inline-block">
<img class="document-img" src="images/camera/9_optional_zoom_margin_defined.png" alt="" />
</div>
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
<div class="px-5 card rounded d-inline-block">
<img class="document-img" src="images/camera/no_margin.png" alt="" />
</div>
- Margin set to **0.35**
<div class="px-5 card rounded d-inline-block">
<img class="document-img" src="images/camera/with_margin.png" alt="" />
</div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

View file

@ -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',

View file

@ -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

View file

@ -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}

View file

@ -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<typeof isChangeZoneEvent>;

View file

@ -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();
/**

View file

@ -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<T> 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;

View file

@ -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<T extends keyof IframeQueryMap> = (
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({

View file

@ -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<Workadven
}
return uuid;
}
get userRoomToken(): string | undefined {
if (userRoomToken === undefined) {
throw new Error(
"User-room token not initialized yet. You should call WA.player.userRoomToken within a WA.onInit callback."
);
}
return userRoomToken;
}
}
export default new WorkadventurePlayerCommands();

View file

@ -0,0 +1,50 @@
<script lang="typescript">
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);
}
</script>
<img {src} alt="" class="nes-pointer" style="--theme-width: {width}; --theme-height: {height}" />
<style>
img {
display: inline-block;
pointer-events: auto;
width: var(--theme-width);
height: var(--theme-height);
margin: 0;
padding: 0;
position: static;
left: 0;
bottom: 0;
right: 0;
top: 0;
image-rendering: pixelated;
}
</style>

View file

@ -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 @@
<section class="container-overflow">
<h3>{mapName}</h3>
<p class="string-HTML">{mapDescription}</p>
{#if mapLink}
<p class="string-HTML">&gt; <a href={mapLink} target="_blank">link to this map</a> &lt;</p>
{/if}
<h3 class="nes-pointer hoverable" on:click={() => (expandedMapCopyright = !expandedMapCopyright)}>
Copyrights of the map
</h3>
@ -60,8 +81,21 @@
<p class="string-HTML">{copyright}</p>
{:else}
<p>
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.
</p>
{/each}
</section>
<h3 class="nes-pointer hoverable" on:click={() => (expandedAudioCopyright = !expandedAudioCopyright)}>
Copyrights of audio files
</h3>
<section hidden={!expandedAudioCopyright}>
{#each audioCopyright as copyright}
<p class="string-HTML">{copyright}</p>
{:else}
<p>
The map creator did not declare a copyright for audio files. This doesn't mean that those tilesets
have no license.
</p>
{/each}
</section>

View file

@ -1,6 +1,6 @@
<script lang="typescript">
import logoWA from "../images/logo-WA-pixel.png";
import logoTalk from "../images/logo-message-pixel.png";
import logoWA from "../images/logo-WA-pixel.png";
import { menuVisiblilityStore } from "../../Stores/MenuStore";
import { chatVisibilityStore } from "../../Stores/ChatStore";
import { get } from "svelte/store";
@ -31,6 +31,7 @@
width: 60px;
padding-top: 0;
margin: 3px;
image-rendering: pixelated;
}
}
.menuIcon img:hover {
@ -38,9 +39,26 @@
}
@media only screen and (max-width: 800px), only screen and (max-height: 800px) {
.menuIcon {
margin: 3px;
display: inline-grid;
z-index: 90;
position: relative;
margin: 25px;
img {
width: 50px;
pointer-events: auto;
width: 60px;
padding-top: 0;
margin: 3px;
}
}
.menuIcon img:hover {
transform: scale(1.2);
}
@media only screen and (max-width: 800px), only screen and (max-height: 800px) {
.menuIcon {
margin: 3px;
img {
width: 50px;
}
}
}
}

View file

@ -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);
@ -65,11 +66,11 @@
<span class="btn-hover">Edit your name</span>
</button>
<button type="button" class="nes-btn" on:click|preventDefault={openEditSkinScene}>
<img src={btnProfileSubMenuWoka} alt="Edit your WOKA" />
<Woka userId={-1} placeholderSrc="" width="26px" height="26px" />
<span class="btn-hover">Edit your WOKA</span>
</button>
<button type="button" class="nes-btn" on:click|preventDefault={openEditCompanionScene}>
<img src={btnProfileSubMenuCompanion} alt="Edit your companion" />
<Companion userId={-1} placeholderSrc={btnProfileSubMenuCompanion} width="26px" height="26px" />
<span class="btn-hover">Edit your companion</span>
</button>
<button type="button" class="nes-btn" on:click|preventDefault={openEnableCameraScene}>

View file

@ -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"}
<div class="rtc-error" />
{/if}
{#if !$constraintStore || $constraintStore.video === false}
<i style="background-color: {getColorByString(name)};">{name}</i>
{/if}
<!-- {#if !$constraintStore || $constraintStore.video === false} -->
<i
class="container {!$constraintStore || $constraintStore.video === false ? '' : 'minimized'}"
style="background-color: {getColorByString(name)};"
>
<span>{peer.userName}</span>
<div class="woka-icon"><Woka userId={peer.userId} placeholderSrc={""} /></div>
</i>
<!-- {/if} -->
{#if $constraintStore && $constraintStore.audio === false}
<img src={microphoneCloseImg} class="active" alt="Muted" />
{/if}
@ -43,3 +51,21 @@
<SoundMeterWidget stream={$streamStore} />
{/if}
</div>
<style>
.container {
display: flex;
flex-direction: column;
padding-top: 15px;
}
.minimized {
left: auto;
transform: scale(0.5);
opacity: 0.5;
}
.woka-icon {
margin-right: 3px;
}
</style>

View file

@ -0,0 +1,45 @@
<script lang="typescript">
import { onDestroy } from "svelte";
import { gameManager } from "../../Phaser/Game/GameManager";
export let userId: number;
export let placeholderSrc: string;
export let width: string = "62px";
export let height: string = "62px";
const gameScene = gameManager.getCurrentGameScene();
let playerWokaPictureStore;
if (userId === -1) {
playerWokaPictureStore = gameScene.CurrentPlayer.pictureStore;
} else {
playerWokaPictureStore = gameScene.MapPlayersByKey.getNestedStore(userId, (item) => item.pictureStore);
}
let src = placeholderSrc;
const unsubscribe = playerWokaPictureStore.subscribe((source) => {
src = source ?? placeholderSrc;
});
onDestroy(unsubscribe);
</script>
<img {src} alt="" class="nes-pointer" style="--theme-width: {width}; --theme-height: {height}" />
<style>
img {
display: inline-block;
pointer-events: auto;
width: var(--theme-width);
height: var(--theme-height);
margin: 0;
padding: 0;
position: static;
left: 0;
bottom: 0;
right: 0;
top: 0;
image-rendering: pixelated;
}
</style>

View file

@ -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;
}
}

View file

@ -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<string | undefined>;
constructor(scene: Phaser.Scene, x: number, y: number, name: string, texturePromise: Promise<string>) {
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<string> {
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;
}
}

View file

@ -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<string | undefined>;
constructor(
scene: GameScene,
@ -57,6 +60,7 @@ export abstract class Character extends Container {
this.invisible = true;
this.sprites = new Map<string, Sprite>();
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<string> {
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<string>): 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;
}
}

View file

@ -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<HTMLCanvasElement>("#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);
}
);
}
}

View file

@ -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<ITiledMapLayer>
) => void;
export type zoneChangeCallback = (
zonesChangedByAction: Array<ITiledMapObject>,
allZonesOnNewPosition: Array<ITiledMapObject>
) => 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<string, string | boolean | number>();
private propertiesChangeCallbacks = new Map<string, Array<PropertyChangeCallback>>();
private enterLayerCallbacks = Array<layerChangeCallback>();
private leaveLayerCallbacks = Array<layerChangeCallback>();
private enterZoneCallbacks = Array<zoneChangeCallback>();
private leaveZoneCallbacks = Array<zoneChangeCallback>();
private tileNameMap = new Map<string, number>();
private tileSetPropertyMap: { [tile_index: number]: Array<ITiledMapProperty> } = {};
public readonly flatLayers: ITiledMapLayer[];
public readonly tiledObjects: ITiledMapObject[];
public readonly phaserLayers: TilemapLayer[] = [];
public readonly zones: ITiledMapObject[] = [];
public exitUrls: Array<string> = [];
@ -44,6 +75,9 @@ export class GameMap {
terrains: Array<Phaser.Tilemaps.Tileset>
) {
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<string, string | boolean | number> {
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;
}
}

View file

@ -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<Phaser.Tilemaps.Tileset>;
CurrentPlayer!: Player;
MapPlayers!: Phaser.Physics.Arcade.Group;
MapPlayersByKey: Map<number, RemotePlayer> = new Map<number, RemotePlayer>();
MapPlayersByKey: MapStore<number, RemotePlayer> = new MapStore<number, RemotePlayer>();
Map!: Phaser.Tilemaps.Tilemap;
Objects!: Array<Phaser.Physics.Arcade.Sprite>;
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<number, RemotePlayer>();
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<HTMLCanvasElement>("#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();
}

View file

@ -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 {

View file

@ -0,0 +1,34 @@
export class TexturesHelper {
public static async getSnapshot(
scene: Phaser.Scene,
...sprites: { sprite: Phaser.GameObjects.Sprite; frame?: string | number }[]
): Promise<string> {
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<string>((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");
}
}
}

View file

@ -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";

View file

@ -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;

View file

@ -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();

View file

@ -0,0 +1,6 @@
import type { Readable } from "svelte/store";
/**
* A store that contains the player/companion avatar picture
*/
export type PictureStore = Readable<string | undefined>;

View file

@ -12,7 +12,7 @@ let idCount = 0;
function createPlayersStore() {
let players = new Map<number, PlayerInterface>();
const { subscribe, set, update } = writable(players);
const { subscribe, set, update } = writable<Map<number, PlayerInterface>>(players);
return {
subscribe,

View file

@ -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<string, string>();
* 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<string, {
* nestedStore: Readable<string>
* }>();
*
* 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<K, V> extends Map<K, V> implements Readable<Map<K, V>> {
private readonly store = writable(this);
private readonly storesByKey = new Map<K, Writable<V | undefined>>();
subscribe(run: Subscriber<Map<K, V>>, invalidate?: (value?: Map<K, V>) => 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<V | undefined> {
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<T>(key: K, accessor: (value: V) => Readable<T> | undefined): Readable<T | undefined> {
const initVal = this.get(key);
let initStore: Readable<T> | 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();
}
};
});
}
}

View file

@ -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 <min, max>
*/
public static isBetween(value: number, min: number, max: number): boolean {
return value >= min && value <= max;
}
}

View file

@ -642,6 +642,7 @@ class CoWebsiteManager {
private fire(): void {
this._onResize.next();
waScaleManager.applyNewSize();
waScaleManager.refreshFocusOnTarget();
}
private fullscreen(): void {

View file

@ -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<string, unknown>);
setUserRoomToken(state.userRoomToken);
});
const wa = {

View file

@ -144,10 +144,12 @@ window.addEventListener("resize", function (event) {
coWebsiteManager.resetStyleMain();
waScaleManager.applyNewSize();
waScaleManager.refreshFocusOnTarget();
});
coWebsiteManager.onResize.subscribe(() => {
waScaleManager.applyNewSize();
waScaleManager.refreshFocusOnTarget();
});
iframeListener.init();

30
front/src/types.ts Normal file
View file

@ -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",
}

View file

@ -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;
}

View file

@ -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<string, string>();
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<string, string>();
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<string>;
}
>();
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();
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -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) => {

View file

@ -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
}
}

View file

@ -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 <https:\/\/workadventu.re\/> \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 <https:\/\/workadventu.re\/> \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 <https:\/\/workadventu.re\/> \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 <https:\/\/workadventu.re\/> \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 <https:\/\/workadventu.re\/> \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
}

View file

@ -104,6 +104,14 @@
<a href="#" class="testLink" data-testmap="emoji.json" target="_blank">Testing Emoji</a>
</td>
</tr>
<tr>
<td>
<input type="radio" name="test-emoji"> Success <input type="radio" name="test-emoji"> Failure <input type="radio" name="test-emoji" checked> Pending
</td>
<td>
<a href="#" class="testLink" data-testmap="focusable_zone_map.json" target="_blank">Focusable Zones</a>
</td>
</tr>
</table>
<h2>Iframe API</h2>
<table class="table">

View file

@ -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 {

View file

@ -288,6 +288,7 @@ export class AuthenticateController extends BaseController {
messages: [],
visitCardUrl: null,
textures: [],
userRoomToken: undefined,
};
try {
data = await adminApi.fetchMemberDataByUuid(email, playUri, IPAddress);

View file

@ -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,

View file

@ -44,4 +44,5 @@ export interface ExSocketInterface extends WebSocket, Identificable {
textures: CharacterTexture[];
backConnection: BackConnection;
listenedZones: Set<Zone>;
userRoomToken: string | undefined;
}

View file

@ -19,6 +19,7 @@ export interface FetchMemberDataByUuidResponse {
textures: CharacterTexture[];
messages: unknown[];
anonymous?: boolean;
userRoomToken: string | undefined;
}
class AdminApi {

View file

@ -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);
}