diff --git a/README.md b/README.md
index 26f1e816..cf640d9c 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-![](https://github.com/thecodingmachine/workadventure/workflows/Continuous%20Integration/badge.svg)
+![](https://github.com/thecodingmachine/workadventure/workflows/Continuous%20Integration/badge.svg) [![Discord](https://img.shields.io/discord/821338762134290432?label=Discord)](https://discord.gg/JVVhXzcE)
![WorkAdventure landscape image](README-INTRO.jpg)
diff --git a/front/dist/index.tmpl.html b/front/dist/index.tmpl.html
index f57d512e..ce0cbad3 100644
--- a/front/dist/index.tmpl.html
+++ b/front/dist/index.tmpl.html
@@ -98,12 +98,12 @@
diff --git a/front/dist/resources/style/style.css b/front/dist/resources/style/style.css
index b0cdeed7..1bda9602 100644
--- a/front/dist/resources/style/style.css
+++ b/front/dist/resources/style/style.css
@@ -363,10 +363,14 @@ body {
justify-content: center;
}
+.audioplayer > div {
+ padding-right: 1.2rem;
+}
+
#audioplayerctrl {
position: fixed;
top: 0;
- right: 50%;
+ right: calc(50% - 120px);
padding: 0.3rem 0.5rem;
color: white;
transition: transform 0.5s;
diff --git a/front/src/Connexion/LocalUserStore.ts b/front/src/Connexion/LocalUserStore.ts
index 8ac8c7b2..1e4267de 100644
--- a/front/src/Connexion/LocalUserStore.ts
+++ b/front/src/Connexion/LocalUserStore.ts
@@ -1,12 +1,15 @@
import {LocalUser} from "./LocalUser";
-const characterLayersKey = 'characterLayers';
-const gameQualityKey = 'gameQuality';
-const videoQualityKey = 'videoQuality';
+const playerNameKey = 'playerName';
+const selectedPlayerKey = 'selectedPlayer';
+const customCursorPositionKey = 'customCursorPosition';
+const characterLayersKey = 'characterLayers';
+const gameQualityKey = 'gameQuality';
+const videoQualityKey = 'videoQuality';
+const audioPlayerVolumeKey = 'audioVolume';
+const audioPlayerMuteKey = 'audioMute';
-//todo: add localstorage fallback
class LocalUserStore {
-
saveUser(localUser: LocalUser) {
localStorage.setItem('localUser', JSON.stringify(localUser));
}
@@ -14,48 +17,62 @@ class LocalUserStore {
const data = localStorage.getItem('localUser');
return data ? JSON.parse(data) : null;
}
-
+
setName(name:string): void {
- window.localStorage.setItem('playerName', name);
+ localStorage.setItem(playerNameKey, name);
}
getName(): string {
- return window.localStorage.getItem('playerName') ?? '';
+ return localStorage.getItem(playerNameKey) || '';
}
setPlayerCharacterIndex(playerCharacterIndex: number): void {
- window.localStorage.setItem('selectedPlayer', ''+playerCharacterIndex);
+ localStorage.setItem(selectedPlayerKey, ''+playerCharacterIndex);
}
getPlayerCharacterIndex(): number {
- return parseInt(window.localStorage.getItem('selectedPlayer') || '');
+ return parseInt(localStorage.getItem(selectedPlayerKey) || '');
}
setCustomCursorPosition(activeRow:number, selectedLayers: number[]): void {
- window.localStorage.setItem('customCursorPosition', JSON.stringify({activeRow, selectedLayers}));
+ localStorage.setItem(customCursorPositionKey, JSON.stringify({activeRow, selectedLayers}));
}
getCustomCursorPosition(): {activeRow:number, selectedLayers:number[]}|null {
- return JSON.parse(window.localStorage.getItem('customCursorPosition') || "null");
+ return JSON.parse(localStorage.getItem(customCursorPositionKey) || "null");
}
setCharacterLayers(layers: string[]): void {
- window.localStorage.setItem(characterLayersKey, JSON.stringify(layers));
+ localStorage.setItem(characterLayersKey, JSON.stringify(layers));
}
getCharacterLayers(): string[]|null {
- return JSON.parse(window.localStorage.getItem(characterLayersKey) || "null");
- }
-
- getGameQualityValue(): number {
- return parseInt(window.localStorage.getItem(gameQualityKey) || '') || 60;
+ return JSON.parse(localStorage.getItem(characterLayersKey) || "null");
}
+
setGameQualityValue(value: number): void {
localStorage.setItem(gameQualityKey, '' + value);
}
-
- getVideoQualityValue(): number {
- return parseInt(window.localStorage.getItem(videoQualityKey) || '') || 20;
+ getGameQualityValue(): number {
+ return parseInt(localStorage.getItem(gameQualityKey) || '60');
}
+
setVideoQualityValue(value: number): void {
localStorage.setItem(videoQualityKey, '' + value);
}
+ getVideoQualityValue(): number {
+ return parseInt(localStorage.getItem(videoQualityKey) || '20');
+ }
+
+ setAudioPlayerVolume(value: number): void {
+ localStorage.setItem(audioPlayerVolumeKey, '' + value);
+ }
+ getAudioPlayerVolume(): number {
+ return parseFloat(localStorage.getItem(audioPlayerVolumeKey) || '1');
+ }
+
+ setAudioPlayerMuted(value: boolean): void {
+ localStorage.setItem(audioPlayerMuteKey, value.toString());
+ }
+ getAudioPlayerMuted(): boolean {
+ return localStorage.getItem(audioPlayerMuteKey) === 'true';
+ }
}
-export const localUserStore = new LocalUserStore();
\ No newline at end of file
+export const localUserStore = new LocalUserStore();
diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts
index 512319c6..9321dbce 100644
--- a/front/src/Phaser/Game/GameScene.ts
+++ b/front/src/Phaser/Game/GameScene.ts
@@ -29,7 +29,9 @@ import {
ON_ACTION_TRIGGER_BUTTON,
TRIGGER_JITSI_PROPERTIES,
TRIGGER_WEBSITE_PROPERTIES,
- WEBSITE_MESSAGE_PROPERTIES
+ WEBSITE_MESSAGE_PROPERTIES,
+ AUDIO_VOLUME_PROPERTY,
+ AUDIO_LOOP_PROPERTY
} from "../../WebRtc/LayoutManager";
import {GameMap} from "./GameMap";
import {coWebsiteManager} from "../../WebRtc/CoWebsiteManager";
@@ -455,16 +457,12 @@ export class GameScene extends ResizableScene implements CenterListener {
});
this.connection.onGroupUpdatedOrCreated((groupPositionMessage: GroupCreatedUpdatedMessageInterface) => {
- audioManager.decreaseVolume();
this.shareGroupPosition(groupPositionMessage);
- this.openChatIcon.setVisible(true);
})
this.connection.onGroupDeleted((groupId: number) => {
- audioManager.restoreVolume();
try {
this.deleteGroup(groupId);
- this.openChatIcon.setVisible(false);
} catch (e) {
console.error(e);
}
@@ -517,11 +515,15 @@ export class GameScene extends ResizableScene implements CenterListener {
onConnect(user: UserSimplePeerInterface) {
self.presentationModeSprite.setVisible(true);
self.chatModeSprite.setVisible(true);
+ self.openChatIcon.setVisible(true);
+ audioManager.decreaseVolume();
},
onDisconnect(userId: number) {
if (self.simplePeer.getNbConnections() === 0) {
self.presentationModeSprite.setVisible(false);
self.chatModeSprite.setVisible(false);
+ self.openChatIcon.setVisible(false);
+ audioManager.restoreVolume();
}
}
})
@@ -630,7 +632,8 @@ export class GameScene extends ResizableScene implements CenterListener {
}else{
const openJitsiRoomFunction = () => {
const roomName = jitsiFactory.getRoomName(newValue.toString(), this.instance);
- if (JITSI_PRIVATE_MODE) {
+ const jitsiUrl = allProps.get("jitsiUrl") as string|undefined;
+ if (JITSI_PRIVATE_MODE && !jitsiUrl) {
const adminTag = allProps.get("jitsiRoomAdminTag") as string|undefined;
this.connection.emitQueryJitsiJwtMessage(roomName, adminTag);
@@ -661,14 +664,15 @@ export class GameScene extends ResizableScene implements CenterListener {
this.connection.setSilent(true);
}
});
- this.gameMap.onPropertyChange('playAudio', (newValue, oldValue) => {
- newValue === undefined ? audioManager.unloadAudio() : audioManager.playAudio(newValue, this.getMapDirUrl());
+ this.gameMap.onPropertyChange('playAudio', (newValue, oldValue, allProps) => {
+ const volume = allProps.get(AUDIO_VOLUME_PROPERTY) as number|undefined;
+ const loop = allProps.get(AUDIO_LOOP_PROPERTY) as boolean|undefined;
+ newValue === undefined ? audioManager.unloadAudio() : audioManager.playAudio(newValue, this.getMapDirUrl(), volume, loop);
});
-
+ // TODO: This legacy property should be removed at some point
this.gameMap.onPropertyChange('playAudioLoop', (newValue, oldValue) => {
- newValue === undefined ? audioManager.unloadAudio() : audioManager.playAudio(newValue, this.getMapDirUrl());
+ newValue === undefined ? audioManager.unloadAudio() : audioManager.playAudio(newValue, this.getMapDirUrl(), undefined, true);
});
-
}
private getMapDirUrl(): string {
@@ -1206,8 +1210,9 @@ export class GameScene extends ResizableScene implements CenterListener {
const allProps = this.gameMap.getCurrentProperties();
const jitsiConfig = this.safeParseJSONstring(allProps.get("jitsiConfig") as string|undefined, 'jitsiConfig');
const jitsiInterfaceConfig = this.safeParseJSONstring(allProps.get("jitsiInterfaceConfig") as string|undefined, 'jitsiInterfaceConfig');
+ const jitsiUrl = allProps.get("jitsiUrl") as string|undefined;
- jitsiFactory.start(roomName, this.playerName, jwt, jitsiConfig, jitsiInterfaceConfig);
+ jitsiFactory.start(roomName, this.playerName, jwt, jitsiConfig, jitsiInterfaceConfig, jitsiUrl);
this.connection.setSilent(true);
mediaManager.hideGameOverlay();
diff --git a/front/src/WebRtc/AudioManager.ts b/front/src/WebRtc/AudioManager.ts
index 24fb74a1..60255a77 100644
--- a/front/src/WebRtc/AudioManager.ts
+++ b/front/src/WebRtc/AudioManager.ts
@@ -1,5 +1,6 @@
import {HtmlUtils} from "./HtmlUtils";
import {isUndefined} from "generic-type-guard";
+import {localUserStore} from "../Connexion/LocalUserStore";
enum audioStates {
closed = 0,
@@ -9,6 +10,8 @@ enum audioStates {
const audioPlayerDivId = "audioplayer";
const audioPlayerCtrlId = "audioplayerctrl";
+const audioPlayerVolId = "audioplayer_volume";
+const audioPlayerMuteId = "audioplayer_volume_icon_playing";
const animationTime = 500;
class AudioManager {
@@ -17,6 +20,8 @@ class AudioManager {
private audioPlayerDiv: HTMLDivElement;
private audioPlayerCtrl: HTMLDivElement;
private audioPlayerElem: HTMLAudioElement | undefined;
+ private audioPlayerVol: HTMLInputElement;
+ private audioPlayerMute: HTMLInputElement;
private volume = 1;
private muted = false;
@@ -26,19 +31,19 @@ class AudioManager {
constructor() {
this.audioPlayerDiv = HtmlUtils.getElementByIdOrFail
(audioPlayerDivId);
this.audioPlayerCtrl = HtmlUtils.getElementByIdOrFail(audioPlayerCtrlId);
+ this.audioPlayerVol = HtmlUtils.getElementByIdOrFail(audioPlayerVolId);
+ this.audioPlayerMute = HtmlUtils.getElementByIdOrFail(audioPlayerMuteId);
- const storedVolume = localStorage.getItem('volume')
- if (storedVolume === null) {
- this.setVolume(1);
- } else {
- this.volume = parseFloat(storedVolume);
- HtmlUtils.getElementByIdOrFail('audioplayer_volume').value = storedVolume;
+ this.volume = localUserStore.getAudioPlayerVolume();
+ this.audioPlayerVol.value = '' + this.volume;
+
+ this.muted = localUserStore.getAudioPlayerMuted();
+ if (this.muted) {
+ this.audioPlayerMute.classList.add('muted');
}
-
- HtmlUtils.getElementByIdOrFail('audioplayer_volume').value = '' + this.volume;
}
- public playAudio(url: string|number|boolean, mapDirUrl: string, loop=false): void {
+ public playAudio(url: string|number|boolean, mapDirUrl: string, volume: number|undefined, loop=false): void {
const audioPath = url as string;
let realAudioPath = '';
@@ -50,7 +55,7 @@ class AudioManager {
realAudioPath = mapDirUrl + '/' + url;
}
- this.loadAudio(realAudioPath);
+ this.loadAudio(realAudioPath, volume);
if (loop) {
this.loop();
@@ -75,26 +80,29 @@ class AudioManager {
}
private changeVolume(talking = false): void {
- if (!isUndefined(this.audioPlayerElem)) {
- this.audioPlayerElem.volume = this.naturalVolume(talking && this.decreaseWhileTalking);
- this.audioPlayerElem.muted = this.muted;
+ if (isUndefined(this.audioPlayerElem)) {
+ return;
}
- }
- private naturalVolume(makeSofter: boolean = false): number {
- const volume = this.volume
- const retVol = makeSofter && !this.volumeReduced ? Math.pow(volume * 0.5, 3) : volume
- this.volumeReduced = makeSofter
- return retVol;
+ const reduceVolume = talking && this.decreaseWhileTalking;
+ if (reduceVolume && !this.volumeReduced) {
+ this.volume *= 0.5;
+ } else if (!reduceVolume && this.volumeReduced) {
+ this.volume *= 2.0;
+ }
+ this.volumeReduced = reduceVolume;
+
+ this.audioPlayerElem.volume = this.volume;
+ this.audioPlayerVol.value = '' + this.volume;
+ this.audioPlayerElem.muted = this.muted;
}
private setVolume(volume: number): void {
this.volume = volume;
- localStorage.setItem('volume', '' + volume);
+ localUserStore.setAudioPlayerVolume(volume);
}
-
- private loadAudio(url: string): void {
+ private loadAudio(url: string, volume: number|undefined): void {
this.load();
/* Solution 1, remove whole audio player */
@@ -112,23 +120,24 @@ class AudioManager {
this.audioPlayerElem.append(srcElem);
this.audioPlayerDiv.append(this.audioPlayerElem);
+ this.volume = volume ? Math.min(volume, this.volume) : this.volume;
this.changeVolume();
this.audioPlayerElem.play();
const muteElem = HtmlUtils.getElementByIdOrFail('audioplayer_mute');
- muteElem.onclick = (ev: Event)=> {
+ muteElem.onclick = (ev: Event) => {
this.muted = !this.muted;
this.changeVolume();
+ localUserStore.setAudioPlayerMuted(this.muted);
if (this.muted) {
- HtmlUtils.getElementByIdOrFail('audioplayer_volume_icon_playing').classList.add('muted');
+ this.audioPlayerMute.classList.add('muted');
} else {
- HtmlUtils.getElementByIdOrFail('audioplayer_volume_icon_playing').classList.remove('muted');
+ this.audioPlayerMute.classList.remove('muted');
}
}
- const volumeElem = HtmlUtils.getElementByIdOrFail('audioplayer_volume');
- volumeElem.oninput = (ev: Event)=> {
+ this.audioPlayerVol.oninput = (ev: Event)=> {
this.setVolume(parseFloat((ev.currentTarget).value));
this.changeVolume();
diff --git a/front/src/WebRtc/JitsiFactory.ts b/front/src/WebRtc/JitsiFactory.ts
index 70ee66bf..983b08e2 100644
--- a/front/src/WebRtc/JitsiFactory.ts
+++ b/front/src/WebRtc/JitsiFactory.ts
@@ -72,6 +72,7 @@ class JitsiFactory {
private audioCallback = this.onAudioChange.bind(this);
private videoCallback = this.onVideoChange.bind(this);
private previousConfigMeet? : jitsiConfigInterface;
+ private jitsiScriptLoaded: boolean = false;
/**
* Slugifies the room name and prepends the room name with the instance
@@ -80,11 +81,11 @@ class JitsiFactory {
return slugify(instance.replace('/', '-') + "-" + roomName);
}
- public start(roomName: string, playerName:string, jwt?: string, config?: object, interfaceConfig?: object): void {
+ public start(roomName: string, playerName:string, jwt?: string, config?: object, interfaceConfig?: object, jitsiUrl?: string): void {
//save previous config
this.previousConfigMeet = getDefaultConfig();
- coWebsiteManager.insertCoWebsite((cowebsiteDiv => {
+ coWebsiteManager.insertCoWebsite((async cowebsiteDiv => {
// Jitsi meet external API maintains some data in local storage
// which is sent via the appData URL parameter when joining a
// conference. Problem is that this data grows indefinitely. Thus
@@ -93,7 +94,12 @@ class JitsiFactory {
// clear jitsi local storage before starting a new conference.
window.localStorage.removeItem("jitsiLocalStorage");
- const domain = JITSI_URL;
+ const domain = jitsiUrl || JITSI_URL;
+ if (domain === undefined) {
+ throw new Error('Missing JITSI_URL environment variable or jitsiUrl parameter in the map.')
+ }
+ await this.loadJitsiScript(domain);
+
const options: any = { // eslint-disable-line @typescript-eslint/no-explicit-any
roomName: roomName,
jwt: jwt,
@@ -157,6 +163,32 @@ class JitsiFactory {
mediaManager.enableCamera();
}
}
+
+ private async loadJitsiScript(domain: string): Promise {
+ return new Promise((resolve, reject) => {
+ if (this.jitsiScriptLoaded) {
+ resolve();
+ return;
+ }
+
+ this.jitsiScriptLoaded = true;
+
+ // Load Jitsi if the environment variable is set.
+ const jitsiScript = document.createElement('script');
+ jitsiScript.src = 'https://' + domain + '/external_api.js';
+ jitsiScript.onload = () => {
+ resolve();
+ }
+ jitsiScript.onerror = () => {
+ reject();
+ }
+
+ document.head.appendChild(jitsiScript);
+
+ })
+
+
+ }
}
export const jitsiFactory = new JitsiFactory();
diff --git a/front/src/WebRtc/LayoutManager.ts b/front/src/WebRtc/LayoutManager.ts
index 91d78798..233b5327 100644
--- a/front/src/WebRtc/LayoutManager.ts
+++ b/front/src/WebRtc/LayoutManager.ts
@@ -31,6 +31,9 @@ export const TRIGGER_JITSI_PROPERTIES = 'jitsiTrigger';
export const WEBSITE_MESSAGE_PROPERTIES = 'openWebsiteTriggerMessage';
export const JITSI_MESSAGE_PROPERTIES = 'jitsiTriggerMessage';
+export const AUDIO_VOLUME_PROPERTY = 'audioVolume';
+export const AUDIO_LOOP_PROPERTY = 'audioLoop';
+
/**
* This class is in charge of the video-conference layout.
* It receives positioning requests for videos and does its best to place them on the screen depending on the active layout mode.
diff --git a/front/src/WebRtc/SimplePeer.ts b/front/src/WebRtc/SimplePeer.ts
index 2baeef11..73a87d14 100644
--- a/front/src/WebRtc/SimplePeer.ts
+++ b/front/src/WebRtc/SimplePeer.ts
@@ -63,7 +63,7 @@ export class SimplePeer {
}
public getNbConnections(): number {
- return this.PeerConnectionArray.size;
+ return this.Users.length;
}
/**
@@ -230,9 +230,6 @@ export class SimplePeer {
this.closeScreenSharingConnection(userId);
- for (const peerConnectionListener of this.peerConnectionListeners) {
- peerConnectionListener.onDisconnect(userId);
- }
const userIndex = this.Users.findIndex(user => user.userId === userId);
if(userIndex < 0){
throw 'Couln\'t delete user';
@@ -250,6 +247,10 @@ export class SimplePeer {
this.PeerScreenSharingConnectionArray.delete(userId);
}
}
+
+ for (const peerConnectionListener of this.peerConnectionListeners) {
+ peerConnectionListener.onDisconnect(userId);
+ }
}
/**
diff --git a/front/src/index.ts b/front/src/index.ts
index 1b8b7205..c313c301 100644
--- a/front/src/index.ts
+++ b/front/src/index.ts
@@ -16,13 +16,6 @@ import {HelpCameraSettingsScene} from "./Phaser/Menu/HelpCameraSettingsScene";
import {localUserStore} from "./Connexion/LocalUserStore";
import {ErrorScene} from "./Phaser/Reconnecting/ErrorScene";
-// Load Jitsi if the environment variable is set.
-if (JITSI_URL) {
- const jitsiScript = document.createElement('script');
- jitsiScript.src = 'https://' + JITSI_URL + '/external_api.js';
- document.head.appendChild(jitsiScript);
-}
-
const {width, height} = coWebsiteManager.getGameSize();
const valueGameQuality = localUserStore.getGameQualityValue();
diff --git a/maps/tests/jitsi_custom_url.json b/maps/tests/jitsi_custom_url.json
new file mode 100644
index 00000000..65e3be9f
--- /dev/null
+++ b/maps/tests/jitsi_custom_url.json
@@ -0,0 +1,94 @@
+{ "compressionlevel":-1,
+ "editorsettings":
+ {
+ "export":
+ {
+ "target":"."
+ }
+ },
+ "height":10,
+ "infinite":false,
+ "layers":[
+ {
+ "data":[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
+ "height":10,
+ "id":1,
+ "name":"floor",
+ "opacity":1,
+ "type":"tilelayer",
+ "visible":true,
+ "width":10,
+ "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, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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":10,
+ "id":2,
+ "name":"start",
+ "opacity":1,
+ "type":"tilelayer",
+ "visible":true,
+ "width":10,
+ "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, 34, 34, 34, 34, 34, 0, 0, 0, 0, 0, 34, 34, 34, 34, 34, 0, 0, 0, 0, 0, 34, 34, 34, 34, 34, 0, 0, 0, 0, 0, 34, 34, 34, 34, 34, 0, 0, 0, 0, 0, 34, 34, 34, 34, 34, 0, 0, 0, 0, 0, 0, 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":10,
+ "id":5,
+ "name":"jitsiConf",
+ "opacity":1,
+ "properties":[
+ {
+ "name":"jitsiRoom",
+ "type":"string",
+ "value":"myRoom"
+ },
+ {
+ "name":"jitsiUrl",
+ "type":"string",
+ "value":"meet.jit.si"
+ }],
+ "type":"tilelayer",
+ "visible":true,
+ "width":10,
+ "x":0,
+ "y":0
+ },
+ {
+ "draworder":"topdown",
+ "id":3,
+ "name":"floorLayer",
+ "objects":[],
+ "opacity":1,
+ "type":"objectgroup",
+ "visible":true,
+ "x":0,
+ "y":0
+ }],
+ "nextlayerid":6,
+ "nextobjectid":1,
+ "orientation":"orthogonal",
+ "renderorder":"right-down",
+ "tiledversion":"1.3.3",
+ "tileheight":32,
+ "tilesets":[
+ {
+ "columns":11,
+ "firstgid":1,
+ "image":"tileset1.png",
+ "imageheight":352,
+ "imagewidth":352,
+ "margin":0,
+ "name":"tileset1",
+ "spacing":0,
+ "tilecount":121,
+ "tileheight":32,
+ "tilewidth":32
+ }],
+ "tilewidth":32,
+ "type":"map",
+ "version":1.2,
+ "width":10
+}
\ No newline at end of file