diff --git a/docs/maps/api-room.md b/docs/maps/api-room.md
index b8a99a53..a307b2da 100644
--- a/docs/maps/api-room.md
+++ b/docs/maps/api-room.md
@@ -145,14 +145,15 @@ WA.room.setTiles([
]);
```
-### Saving / loading metadata
+### Saving / loading state
```
-WA.room.saveMetadata(key : string, data : any): void
-WA.room.loadMetadata(key : string) : any
+WA.room.saveVariable(key : string, data : unknown): void
+WA.room.loadVariable(key : string) : unknown
+WA.room.onVariableChange(key : string).subscribe((data: unknown) => {}) : Subscription
```
-These 2 methods can be used to save and load data related to the current room.
+These 3 methods can be used to save, load and track changes in variables related to the current room.
`data` can be any value that is serializable in JSON.
@@ -161,17 +162,62 @@ configuration / metadatas.
Example :
```javascript
-WA.room.saveMetadata('config', {
+WA.room.saveVariable('config', {
'bottomExitUrl': '/@/org/world/castle',
'topExitUrl': '/@/org/world/tower',
'enableBirdSound': true
});
//...
-let config = WA.room.loadMetadata('config');
+let config = WA.room.loadVariable('config');
```
-{.alert.alert-danger}
-Important: metadata can only be saved/loaded if an administration server is attached to WorkAdventure. The `WA.room.saveMetadata`
-and `WA.room.loadMetadata` functions will therefore be available on the hosted version of WorkAdventure, but will not
-be available in the self-hosted version (unless you decide to code an administration server stub to provide storage for
-those data)
+If you are using Typescript, please note that the return type of `loadVariable` is `unknown`. This is
+for security purpose, as we don't know the type of the variable. In order to use the returned value,
+you will need to cast it to the correct type (or better, use a [Type guard](https://www.typescriptlang.org/docs/handbook/2/narrowing.html) to actually check at runtime
+that you get the expected type).
+
+{.alert.alert-warning}
+For security reasons, you cannot load or save **any** variable (otherwise, anyone on your map could set any data).
+Variables storage is subject to an authorization process. Read below to learn more.
+
+#### Declaring allowed keys
+
+In order to declare allowed keys related to a room, you need to add a **objects** in an "object layer" of the map.
+
+Each object will represent a variable.
+
+
+
+
+
+
+
+TODO: move the image in https://workadventu.re/img/docs
+
+
+The name of the variable is the name of the object.
+The object **type** MUST be **variable**.
+
+You can set a default value for the object in the `default` property.
+
+Use the `persist` property to save the state of the variable in database. If `persist` is false, the variable will stay
+in the memory of the WorkAdventure servers but will be wiped out of the memory as soon as the room is empty (or if the
+server restarts).
+
+{.alert.alert-info}
+Do not use `persist` for highly dynamic values that have a short life spawn.
+
+With `readableBy` and `writableBy`, you control who can read of write in this variable. The property accepts a string
+representing a "tag". Anyone having this "tag" can read/write in the variable.
+
+{.alert.alert-warning}
+`readableBy` and `writableBy` are specific to the public version of WorkAdventure because the notion of tags
+is not available unless you have an "admin" server (that is not part of the self-hosted version of WorkAdventure).
+
+Finally, the `jsonSchema` property can contain [a complete JSON schema](https://json-schema.org/) to validate the content of the variable.
+Trying to set a variable to a value that is not compatible with the schema will fail.
+
+
+
+
+TODO: document tracking, unsubscriber, etc...
diff --git a/front/src/Api/Events/IframeEvent.ts b/front/src/Api/Events/IframeEvent.ts
index fc3384f8..83d0e12e 100644
--- a/front/src/Api/Events/IframeEvent.ts
+++ b/front/src/Api/Events/IframeEvent.ts
@@ -18,6 +18,9 @@ import type { MenuItemClickedEvent } from "./ui/MenuItemClickedEvent";
import type { MenuItemRegisterEvent } from "./ui/MenuItemRegisterEvent";
import type { HasPlayerMovedEvent } from "./HasPlayerMovedEvent";
import type { SetTilesEvent } from "./SetTilesEvent";
+import type { SetVariableEvent } from "./SetVariableEvent";
+import type {InitEvent} from "./InitEvent";
+
export interface TypedMessageEvent extends MessageEvent {
data: T;
@@ -50,6 +53,9 @@ export type IframeEventMap = {
getState: undefined;
registerMenuCommand: MenuItemRegisterEvent;
setTiles: SetTilesEvent;
+ setVariable: SetVariableEvent;
+ // A script/iframe is ready to receive events
+ ready: null;
};
export interface IframeEvent {
type: T;
@@ -68,6 +74,8 @@ export interface IframeResponseEventMap {
hasPlayerMoved: HasPlayerMovedEvent;
dataLayer: DataLayerEvent;
menuItemClicked: MenuItemClickedEvent;
+ setVariable: SetVariableEvent;
+ init: InitEvent;
}
export interface IframeResponseEvent {
type: T;
diff --git a/front/src/Api/Events/InitEvent.ts b/front/src/Api/Events/InitEvent.ts
new file mode 100644
index 00000000..47326f81
--- /dev/null
+++ b/front/src/Api/Events/InitEvent.ts
@@ -0,0 +1,10 @@
+import * as tg from "generic-type-guard";
+
+export const isInitEvent =
+ new tg.IsInterface().withProperties({
+ variables: tg.isObject
+ }).get();
+/**
+ * A message sent from the game just after an iFrame opens, to send all important data (like variables)
+ */
+export type InitEvent = tg.GuardedType;
diff --git a/front/src/Api/Events/SetVariableEvent.ts b/front/src/Api/Events/SetVariableEvent.ts
new file mode 100644
index 00000000..b0effb30
--- /dev/null
+++ b/front/src/Api/Events/SetVariableEvent.ts
@@ -0,0 +1,18 @@
+import * as tg from "generic-type-guard";
+import {isMenuItemRegisterEvent} from "./ui/MenuItemRegisterEvent";
+
+export const isSetVariableEvent =
+ new tg.IsInterface().withProperties({
+ key: tg.isString,
+ value: tg.isUnknown,
+ }).get();
+/**
+ * A message sent from the iFrame to the game to change the value of the property of the layer
+ */
+export type SetVariableEvent = tg.GuardedType;
+
+export const isSetVariableIframeEvent =
+ new tg.IsInterface().withProperties({
+ type: tg.isSingletonString("setVariable"),
+ data: isSetVariableEvent
+ }).get();
diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts
index 314d5d2e..6caecc1f 100644
--- a/front/src/Api/IframeListener.ts
+++ b/front/src/Api/IframeListener.ts
@@ -32,6 +32,7 @@ import type { HasPlayerMovedEvent } from "./Events/HasPlayerMovedEvent";
import { isLoadPageEvent } from "./Events/LoadPageEvent";
import { handleMenuItemRegistrationEvent, isMenuItemRegisterIframeEvent } from "./Events/ui/MenuItemRegisterEvent";
import { SetTilesEvent, isSetTilesEvent } from "./Events/SetTilesEvent";
+import { isSetVariableIframeEvent, SetVariableEvent } from "./Events/SetVariableEvent";
type AnswererCallback = (query: IframeQueryMap[T]['query']) => IframeQueryMap[T]['answer']|Promise;
@@ -40,6 +41,9 @@ type AnswererCallback = (query: IframeQueryMap[T
* Also allows to send messages to those iframes.
*/
class IframeListener {
+ private readonly _readyStream: Subject = new Subject();
+ public readonly readyStream = this._readyStream.asObservable();
+
private readonly _chatStream: Subject = new Subject();
public readonly chatStream = this._chatStream.asObservable();
@@ -106,6 +110,9 @@ class IframeListener {
private readonly _setTilesStream: Subject = new Subject();
public readonly setTilesStream = this._setTilesStream.asObservable();
+ private readonly _setVariableStream: Subject = new Subject();
+ public readonly setVariableStream = this._setVariableStream.asObservable();
+
private readonly iframes = new Set();
private readonly iframeCloseCallbacks = new Map void)[]>();
private readonly scripts = new Map();
@@ -187,62 +194,76 @@ class IframeListener {
});
} else if (isIframeEventWrapper(payload)) {
- if (payload.type === "showLayer" && isLayerEvent(payload.data)) {
- this._showLayerStream.next(payload.data);
- } else if (payload.type === "hideLayer" && isLayerEvent(payload.data)) {
- this._hideLayerStream.next(payload.data);
- } else if (payload.type === "setProperty" && isSetPropertyEvent(payload.data)) {
- this._setPropertyStream.next(payload.data);
- } else if (payload.type === "chat" && isChatEvent(payload.data)) {
- this._chatStream.next(payload.data);
- } else if (payload.type === "openPopup" && isOpenPopupEvent(payload.data)) {
- this._openPopupStream.next(payload.data);
- } else if (payload.type === "closePopup" && isClosePopupEvent(payload.data)) {
- this._closePopupStream.next(payload.data);
- } else if (payload.type === "openTab" && isOpenTabEvent(payload.data)) {
- scriptUtils.openTab(payload.data.url);
- } else if (payload.type === "goToPage" && isGoToPageEvent(payload.data)) {
- scriptUtils.goToPage(payload.data.url);
- } else if (payload.type === "loadPage" && isLoadPageEvent(payload.data)) {
- this._loadPageStream.next(payload.data.url);
- } else if (payload.type === "playSound" && isPlaySoundEvent(payload.data)) {
- this._playSoundStream.next(payload.data);
- } else if (payload.type === "stopSound" && isStopSoundEvent(payload.data)) {
- this._stopSoundStream.next(payload.data);
- } else if (payload.type === "loadSound" && isLoadSoundEvent(payload.data)) {
- this._loadSoundStream.next(payload.data);
- } else if (payload.type === "openCoWebSite" && isOpenCoWebsite(payload.data)) {
- scriptUtils.openCoWebsite(
- payload.data.url,
- foundSrc,
- payload.data.allowApi,
- payload.data.allowPolicy
- );
- } else if (payload.type === "closeCoWebSite") {
- scriptUtils.closeCoWebSite();
- } else if (payload.type === "disablePlayerControls") {
- this._disablePlayerControlStream.next();
- } else if (payload.type === "restorePlayerControls") {
- this._enablePlayerControlStream.next();
- } else if (payload.type === "displayBubble") {
- this._displayBubbleStream.next();
- } else if (payload.type === "removeBubble") {
- this._removeBubbleStream.next();
- } else if (payload.type == "onPlayerMove") {
- this.sendPlayerMove = true;
- } else if (payload.type == "getDataLayer") {
- this._dataLayerChangeStream.next();
- } else if (isMenuItemRegisterIframeEvent(payload)) {
- const data = payload.data.menutItem;
- // @ts-ignore
- this.iframeCloseCallbacks.get(iframe).push(() => {
- this._unregisterMenuCommandStream.next(data);
- });
- handleMenuItemRegistrationEvent(payload.data);
- } else if (payload.type == "setTiles" && isSetTilesEvent(payload.data)) {
- this._setTilesStream.next(payload.data);
+ if (payload.type === 'ready') {
+ this._readyStream.next();
+ } else if (payload.type === "showLayer" && isLayerEvent(payload.data)) {
+ this._showLayerStream.next(payload.data);
+ } else if (payload.type === "hideLayer" && isLayerEvent(payload.data)) {
+ this._hideLayerStream.next(payload.data);
+ } else if (payload.type === "setProperty" && isSetPropertyEvent(payload.data)) {
+ this._setPropertyStream.next(payload.data);
+ } else if (payload.type === "chat" && isChatEvent(payload.data)) {
+ this._chatStream.next(payload.data);
+ } else if (payload.type === "openPopup" && isOpenPopupEvent(payload.data)) {
+ this._openPopupStream.next(payload.data);
+ } else if (payload.type === "closePopup" && isClosePopupEvent(payload.data)) {
+ this._closePopupStream.next(payload.data);
+ } else if (payload.type === "openTab" && isOpenTabEvent(payload.data)) {
+ scriptUtils.openTab(payload.data.url);
+ } else if (payload.type === "goToPage" && isGoToPageEvent(payload.data)) {
+ scriptUtils.goToPage(payload.data.url);
+ } else if (payload.type === "loadPage" && isLoadPageEvent(payload.data)) {
+ this._loadPageStream.next(payload.data.url);
+ } else if (payload.type === "playSound" && isPlaySoundEvent(payload.data)) {
+ this._playSoundStream.next(payload.data);
+ } else if (payload.type === "stopSound" && isStopSoundEvent(payload.data)) {
+ this._stopSoundStream.next(payload.data);
+ } else if (payload.type === "loadSound" && isLoadSoundEvent(payload.data)) {
+ this._loadSoundStream.next(payload.data);
+ } else if (payload.type === "openCoWebSite" && isOpenCoWebsite(payload.data)) {
+ scriptUtils.openCoWebsite(
+ payload.data.url,
+ foundSrc,
+ payload.data.allowApi,
+ payload.data.allowPolicy
+ );
+ } else if (payload.type === "closeCoWebSite") {
+ scriptUtils.closeCoWebSite();
+ } else if (payload.type === "disablePlayerControls") {
+ this._disablePlayerControlStream.next();
+ } else if (payload.type === "restorePlayerControls") {
+ this._enablePlayerControlStream.next();
+ } else if (payload.type === "displayBubble") {
+ this._displayBubbleStream.next();
+ } else if (payload.type === "removeBubble") {
+ this._removeBubbleStream.next();
+ } else if (payload.type == "onPlayerMove") {
+ this.sendPlayerMove = true;
+ } else if (payload.type == "getDataLayer") {
+ this._dataLayerChangeStream.next();
+ } else if (isMenuItemRegisterIframeEvent(payload)) {
+ const data = payload.data.menutItem;
+ // @ts-ignore
+ this.iframeCloseCallbacks.get(iframe).push(() => {
+ this._unregisterMenuCommandStream.next(data);
+ });
+ handleMenuItemRegistrationEvent(payload.data);
+ } else if (payload.type == "setTiles" && isSetTilesEvent(payload.data)) {
+ this._setTilesStream.next(payload.data);
+ } else if (isSetVariableIframeEvent(payload)) {
+ this._setVariableStream.next(payload.data);
+
+ // Let's dispatch the message to the other iframes
+ for (iframe of this.iframes) {
+ if (iframe.contentWindow !== message.source) {
+ iframe.contentWindow?.postMessage({
+ 'type': 'setVariable',
+ 'data': payload.data
+ }, '*');
+ }
}
}
+ }
},
false
);
@@ -394,6 +415,13 @@ class IframeListener {
});
}
+ setVariable(setVariableEvent: SetVariableEvent) {
+ this.postMessage({
+ 'type': 'setVariable',
+ 'data': setVariableEvent
+ });
+ }
+
/**
* Sends the message... to all allowed iframes.
*/
diff --git a/front/src/Api/iframe/room.ts b/front/src/Api/iframe/room.ts
index c70d0aad..623773c3 100644
--- a/front/src/Api/iframe/room.ts
+++ b/front/src/Api/iframe/room.ts
@@ -1,4 +1,4 @@
-import { Subject } from "rxjs";
+import {Observable, Subject} from "rxjs";
import { isDataLayerEvent } from "../Events/DataLayerEvent";
import { EnterLeaveEvent, isEnterLeaveEvent } from "../Events/EnterLeaveEvent";
@@ -6,6 +6,9 @@ import { isGameStateEvent } from "../Events/GameStateEvent";
import {IframeApiContribution, queryWorkadventure, sendToWorkadventure} from "./IframeApiContribution";
import { apiCallback } from "./registeredCallbacks";
+import type {LayerEvent} from "../Events/LayerEvent";
+import type {SetPropertyEvent} from "../Events/setPropertyEvent";
+import {isSetVariableEvent, SetVariableEvent} from "../Events/SetVariableEvent";
import type { ITiledMap } from "../../Phaser/Map/ITiledMap";
import type { DataLayerEvent } from "../Events/DataLayerEvent";
@@ -15,6 +18,9 @@ const enterStreams: Map> = new Map> = new Map>();
const dataLayerResolver = new Subject();
const stateResolvers = new Subject();
+const setVariableResolvers = new Subject();
+const variables = new Map();
+const variableSubscribers = new Map>();
let immutableDataPromise: Promise | undefined = undefined;
@@ -52,6 +58,14 @@ function getDataLayer(): Promise {
});
}
+setVariableResolvers.subscribe((event) => {
+ variables.set(event.key, event.value);
+ const subject = variableSubscribers.get(event.key);
+ if (subject !== undefined) {
+ subject.next(event.value);
+ }
+});
+
export class WorkadventureRoomCommands extends IframeApiContribution {
callbacks = [
apiCallback({
@@ -75,6 +89,13 @@ export class WorkadventureRoomCommands extends IframeApiContribution {
+ setVariableResolvers.next(payloadData);
+ }
+ }),
];
onEnterZone(name: string, callback: () => void): void {
@@ -132,6 +153,30 @@ export class WorkadventureRoomCommands extends IframeApiContribution {
+ let subject = variableSubscribers.get(key);
+ if (subject === undefined) {
+ subject = new Subject();
+ variableSubscribers.set(key, subject);
+ }
+ return subject.asObservable();
+ }
}
export default new WorkadventureRoomCommands();
diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts
index d767f0f4..c2a2b38d 100644
--- a/front/src/Phaser/Game/GameScene.ts
+++ b/front/src/Phaser/Game/GameScene.ts
@@ -91,6 +91,8 @@ 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 type {InitEvent} from "../../Api/Events/InitEvent";
export interface GameSceneInitInterface {
initPosition: PointInterface | null;
@@ -199,7 +201,8 @@ export class GameScene extends DirtyScene {
private mapTransitioning: boolean = false; //used to prevent transitions happenning at the same time.
private emoteManager!: EmoteManager;
private preloading: boolean = true;
- startPositionCalculator!: StartPositionCalculator;
+ private startPositionCalculator!: StartPositionCalculator;
+ private sharedVariablesManager!: SharedVariablesManager;
constructor(private room: Room, MapUrlFile: string, customKey?: string | undefined) {
super({
@@ -396,6 +399,23 @@ export class GameScene extends DirtyScene {
});
}
+
+ this.iframeSubscriptionList.push(iframeListener.readyStream.subscribe((iframe) => {
+ this.connectionAnswerPromise.then(connection => {
+ // Generate init message for an iframe
+ // TODO: merge with GameStateEvent
+ const initEvent: InitEvent = {
+ variables: this.sharedVariablesManager.variables
+ }
+
+ });
+ // TODO: SEND INIT MESSAGE TO IFRAMES ONLY WHEN CONNECTION IS ESTABLISHED
+ // TODO: SEND INIT MESSAGE TO IFRAMES ONLY WHEN CONNECTION IS ESTABLISHED
+ // TODO: SEND INIT MESSAGE TO IFRAMES ONLY WHEN CONNECTION IS ESTABLISHED
+ // TODO: SEND INIT MESSAGE TO IFRAMES ONLY WHEN CONNECTION IS ESTABLISHED
+ // TODO: SEND INIT MESSAGE TO IFRAMES ONLY WHEN CONNECTION IS ESTABLISHED
+ }));
+
// Now, let's load the script, if any
const scripts = this.getScriptUrls(this.mapFile);
for (const script of scripts) {
@@ -706,6 +726,9 @@ export class GameScene extends DirtyScene {
this.gameMap.setPosition(event.x, event.y);
});
+ // Set up variables manager
+ this.sharedVariablesManager = new SharedVariablesManager(this.connection, this.gameMap);
+
//this.initUsersPosition(roomJoinedMessage.users);
this.connectionAnswerPromiseResolve(onConnect.room);
// Analyze tags to find if we are admin. If yes, show console.
@@ -1148,6 +1171,7 @@ ${escapedMessage}
this.peerStoreUnsubscribe();
this.biggestAvailableAreaStoreUnsubscribe();
iframeListener.unregisterAnswerer('getState');
+ this.sharedVariablesManager?.close();
mediaManager.hideGameOverlay();
diff --git a/front/src/Phaser/Game/SharedVariablesManager.ts b/front/src/Phaser/Game/SharedVariablesManager.ts
new file mode 100644
index 00000000..abd2474e
--- /dev/null
+++ b/front/src/Phaser/Game/SharedVariablesManager.ts
@@ -0,0 +1,59 @@
+/**
+ * Handles variables shared between the scripting API and the server.
+ */
+import type {RoomConnection} from "../../Connexion/RoomConnection";
+import {iframeListener} from "../../Api/IframeListener";
+import type {Subscription} from "rxjs";
+import type {GameMap} from "./GameMap";
+import type {ITiledMapObject} from "../Map/ITiledMap";
+
+export class SharedVariablesManager {
+ private _variables = new Map();
+ private iframeListenerSubscription: Subscription;
+ private variableObjects: Map;
+
+ constructor(private roomConnection: RoomConnection, private gameMap: GameMap) {
+ // We initialize the list of variable object at room start. The objects cannot be edited later
+ // (otherwise, this would cause a security issue if the scripting API can edit this list of objects)
+ this.variableObjects = SharedVariablesManager.findVariablesInMap(gameMap);
+
+ // When a variable is modified from an iFrame
+ this.iframeListenerSubscription = iframeListener.setVariableStream.subscribe((event) => {
+ const key = event.key;
+
+ if (!this.variableObjects.has(key)) {
+ const errMsg = 'A script is trying to modify variable "'+key+'" but this variable is not defined in the map.' +
+ 'There should be an object in the map whose name is "'+key+'" and whose type is "variable"';
+ console.error(errMsg);
+ throw new Error(errMsg);
+ }
+
+ this._variables.set(key, event.value);
+ // TODO: dispatch to the room connection.
+ });
+ }
+
+ private static findVariablesInMap(gameMap: GameMap): Map {
+ const objects = new Map();
+ for (const layer of gameMap.getMap().layers) {
+ if (layer.type === 'objectgroup') {
+ for (const object of layer.objects) {
+ if (object.type === 'variable') {
+ // We store a copy of the object (to make it immutable)
+ objects.set(object.name, {...object});
+ }
+ }
+ }
+ }
+ return objects;
+ }
+
+
+ public close(): void {
+ this.iframeListenerSubscription.unsubscribe();
+ }
+
+ get variables(): Map {
+ return this._variables;
+ }
+}
diff --git a/front/src/iframe_api.ts b/front/src/iframe_api.ts
index 1915020e..b27bda2d 100644
--- a/front/src/iframe_api.ts
+++ b/front/src/iframe_api.ts
@@ -206,3 +206,9 @@ window.addEventListener(
// ...
}
);
+
+// Notify WorkAdventure that we are ready to receive data
+sendToWorkadventure({
+ type: 'ready',
+ data: null
+});
diff --git a/maps/tests/Variables/script.js b/maps/tests/Variables/script.js
new file mode 100644
index 00000000..afd16773
--- /dev/null
+++ b/maps/tests/Variables/script.js
@@ -0,0 +1,11 @@
+
+console.log('Trying to set variable "not_exists". This should display an error in the console.')
+WA.room.saveVariable('not_exists', 'foo');
+
+console.log('Trying to set variable "config". This should work.');
+WA.room.saveVariable('config', {'foo': 'bar'});
+
+console.log('Trying to read variable "config". This should display a {"foo": "bar"} object.');
+console.log(WA.room.loadVariable('config'));
+
+
diff --git a/maps/tests/Variables/variables.json b/maps/tests/Variables/variables.json
new file mode 100644
index 00000000..93573da8
--- /dev/null
+++ b/maps/tests/Variables/variables.json
@@ -0,0 +1,112 @@
+{ "compressionlevel":-1,
+ "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, 23, 23, 0, 0, 0, 0, 0, 0, 0, 0, 23, 23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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":6,
+ "name":"triggerZone",
+ "opacity":1,
+ "properties":[
+ {
+ "name":"zone",
+ "type":"string",
+ "value":"myTrigger"
+ }],
+ "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
+ },
+ {
+ "draworder":"topdown",
+ "id":3,
+ "name":"floorLayer",
+ "objects":[
+ {
+ "height":67,
+ "id":3,
+ "name":"",
+ "rotation":0,
+ "text":
+ {
+ "fontfamily":"Sans Serif",
+ "pixelsize":11,
+ "text":"Test:\nTODO",
+ "wrap":true
+ },
+ "type":"",
+ "visible":true,
+ "width":252.4375,
+ "x":2.78125,
+ "y":2.5
+ },
+ {
+ "id":5,
+ "template":"config.tx",
+ "x":57.5,
+ "y":111
+ }],
+ "opacity":1,
+ "type":"objectgroup",
+ "visible":true,
+ "x":0,
+ "y":0
+ }],
+ "nextlayerid":8,
+ "nextobjectid":6,
+ "orientation":"orthogonal",
+ "properties":[
+ {
+ "name":"script",
+ "type":"string",
+ "value":"script.js"
+ }],
+ "renderorder":"right-down",
+ "tiledversion":"2021.03.23",
+ "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.5,
+ "width":10
+}
\ No newline at end of file
diff --git a/maps/tests/index.html b/maps/tests/index.html
index 38ee51ef..a96690b8 100644
--- a/maps/tests/index.html
+++ b/maps/tests/index.html
@@ -202,6 +202,14 @@
Test set tiles
+