diff --git a/deeployer.libsonnet b/deeployer.libsonnet index 07f5f491..52cea293 100644 --- a/deeployer.libsonnet +++ b/deeployer.libsonnet @@ -88,8 +88,6 @@ "JITSI_URL": env.JITSI_URL, "SECRET_JITSI_KEY": env.SECRET_JITSI_KEY, "TURN_SERVER": "turn:coturn.workadventu.re:443,turns:coturn.workadventu.re:443", - "TURN_USER": "workadventure", - "TURN_PASSWORD": "WorkAdventure123", "JITSI_PRIVATE_MODE": if env.SECRET_JITSI_KEY != '' then "true" else "false", "START_ROOM_URL": "/_/global/maps."+url+"/Floor0/floor0.json" //"GA_TRACKING_ID": "UA-10196481-11" diff --git a/front/Dockerfile b/front/Dockerfile index b0d17877..51734535 100644 --- a/front/Dockerfile +++ b/front/Dockerfile @@ -8,6 +8,11 @@ FROM thecodingmachine/nodejs:14-apache COPY --chown=docker:docker front . COPY --from=builder --chown=docker:docker /var/www/messages/generated /var/www/html/src/Messages/generated + +# Removing the iframe.html file from the final image as this adds a XSS attack. +# iframe.html is only in dev mode to circumvent a limitation +RUN rm dist/iframe.html + RUN yarn install ENV NODE_ENV=production diff --git a/front/dist/.gitignore b/front/dist/.gitignore index dd1fae3d..a60c53be 100644 --- a/front/dist/.gitignore +++ b/front/dist/.gitignore @@ -1,2 +1,3 @@ index.html index.tmpl.html.tmp +/js/ diff --git a/front/dist/iframe.html b/front/dist/iframe.html new file mode 100644 index 00000000..c8fafb4b --- /dev/null +++ b/front/dist/iframe.html @@ -0,0 +1,17 @@ + + + + + + + diff --git a/front/src/Api/Events/ChatEvent.ts b/front/src/Api/Events/ChatEvent.ts new file mode 100644 index 00000000..5729a120 --- /dev/null +++ b/front/src/Api/Events/ChatEvent.ts @@ -0,0 +1,11 @@ +import * as tg from "generic-type-guard"; + +export const isChatEvent = + new tg.IsInterface().withProperties({ + message: tg.isString, + author: tg.isString, + }).get(); +/** + * A message sent from the iFrame to the game to add a message in the chat. + */ +export type ChatEvent = tg.GuardedType; diff --git a/front/src/Api/Events/EnterLeaveEvent.ts b/front/src/Api/Events/EnterLeaveEvent.ts new file mode 100644 index 00000000..0c0cb4ff --- /dev/null +++ b/front/src/Api/Events/EnterLeaveEvent.ts @@ -0,0 +1,10 @@ +import * as tg from "generic-type-guard"; + +export const isEnterLeaveEvent = + 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 marked with the "zone" property. + */ +export type EnterLeaveEvent = tg.GuardedType; diff --git a/front/src/Api/Events/IframeEvent.ts b/front/src/Api/Events/IframeEvent.ts new file mode 100644 index 00000000..65d2b443 --- /dev/null +++ b/front/src/Api/Events/IframeEvent.ts @@ -0,0 +1,7 @@ +export interface IframeEvent { + type: string; + data: unknown; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const isIframeEventWrapper = (event: any): event is IframeEvent => typeof event.type === 'string' && typeof event.data === 'object'; diff --git a/front/src/Api/Events/OpenPopupEvent.ts b/front/src/Api/Events/OpenPopupEvent.ts new file mode 100644 index 00000000..bbfc12bf --- /dev/null +++ b/front/src/Api/Events/OpenPopupEvent.ts @@ -0,0 +1,22 @@ +import * as tg from "generic-type-guard"; + +const isButtonDescriptor = + new tg.IsInterface().withProperties({ + label: tg.isString, + className: tg.isOptional(tg.isString), + closeOnClick: tg.isOptional(tg.isBoolean) + }).get(); +type ButtonDescriptor = tg.GuardedType; + +export const isOpenPopupEvent = + new tg.IsInterface().withProperties({ + popupId: tg.isNumber, + targetObject: tg.isString, + message: tg.isString, + buttons: tg.isAny //tg.isArray, + }).get(); + +/** + * A message sent from the iFrame to the game to add a message in the chat. + */ +export type OpenPopupEvent = tg.GuardedType; diff --git a/front/src/Api/Events/UserInputChatEvent.ts b/front/src/Api/Events/UserInputChatEvent.ts new file mode 100644 index 00000000..de21ff6e --- /dev/null +++ b/front/src/Api/Events/UserInputChatEvent.ts @@ -0,0 +1,10 @@ +import * as tg from "generic-type-guard"; + +export const isUserInputChatEvent = + new tg.IsInterface().withProperties({ + message: tg.isString, + }).get(); +/** + * A message sent from the game to the iFrame when a user types a message in the chat. + */ +export type UserInputChatEvent = tg.GuardedType; diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts new file mode 100644 index 00000000..1a6a0ea7 --- /dev/null +++ b/front/src/Api/IframeListener.ts @@ -0,0 +1,171 @@ +import {Subject} from "rxjs"; +import {ChatEvent, isChatEvent} from "./Events/ChatEvent"; +import {IframeEvent, isIframeEventWrapper} from "./Events/IframeEvent"; +import {UserInputChatEvent} from "./Events/UserInputChatEvent"; +import * as crypto from "crypto"; +import {HtmlUtils} from "../WebRtc/HtmlUtils"; +import {EnterLeaveEvent} from "./Events/EnterLeaveEvent"; +import {isOpenPopupEvent, OpenPopupEvent} from "./Events/OpenPopupEvent"; + + + +/** + * Listens to messages from iframes and turn those messages into easy to use observables. + * Also allows to send messages to those iframes. + */ +class IframeListener { + private readonly _chatStream: Subject = new Subject(); + public readonly chatStream = this._chatStream.asObservable(); + + private readonly _openPopupStream: Subject = new Subject(); + public readonly openPopupStream = this._openPopupStream.asObservable(); + + private readonly iframes = new Set(); + private readonly scripts = new Map(); + + init() { + window.addEventListener("message", (message) => { + // Do we trust the sender of this message? + // Let's only accept messages from the iframe that are allowed. + // Note: maybe we could restrict on the domain too for additional security (in case the iframe goes to another domain). + let found = false; + for (const iframe of this.iframes) { + if (iframe.contentWindow === message.source) { + found = true; + break; + } + } + if (!found) { + return; + } + + const payload = message.data; + console.log('FOO'); + if (isIframeEventWrapper(payload)) { + console.log('FOOBAR', payload); + if (payload.type === 'chat' && isChatEvent(payload.data)) { + this._chatStream.next(payload.data); + } else if (payload.type === 'openPopup' && isOpenPopupEvent(payload.data)) { + console.log('OPENPOPUP called'); + this._openPopupStream.next(payload.data); + } + } + + + }, false); + + } + + /** + * Allows the passed iFrame to send/receive messages via the API. + */ + registerIframe(iframe: HTMLIFrameElement): void { + this.iframes.add(iframe); + } + + unregisterIframe(iframe: HTMLIFrameElement): void { + this.iframes.delete(iframe); + } + + registerScript(scriptUrl: string): void { + console.log('Loading map related script at ', scriptUrl) + + if (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') { + // Using external iframe mode ( + const iframe = document.createElement('iframe'); + iframe.id = this.getIFrameId(scriptUrl); + iframe.style.display = 'none'; + iframe.src = '/iframe.html?script='+encodeURIComponent(scriptUrl); + + // We are putting a sandbox on this script because it will run in the same domain as the main website. + iframe.sandbox.add('allow-scripts'); + iframe.sandbox.add('allow-top-navigation-by-user-activation'); + + document.body.prepend(iframe); + + this.scripts.set(scriptUrl, iframe); + this.registerIframe(iframe); + } else { + // production code + const iframe = document.createElement('iframe'); + iframe.id = this.getIFrameId(scriptUrl); + + // We are putting a sandbox on this script because it will run in the same domain as the main website. + iframe.sandbox.add('allow-scripts'); + iframe.sandbox.add('allow-top-navigation-by-user-activation'); + + const html = '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n'; + + //iframe.src = "data:text/html;charset=utf-8," + escape(html); + iframe.srcdoc = html; + + document.body.prepend(iframe); + + this.scripts.set(scriptUrl, iframe); + this.registerIframe(iframe); + } + + + } + + private getIFrameId(scriptUrl: string): string { + return 'script'+crypto.createHash('md5').update(scriptUrl).digest("hex"); + } + + unregisterScript(scriptUrl: string): void { + const iFrameId = this.getIFrameId(scriptUrl); + const iframe = HtmlUtils.getElementByIdOrFail(iFrameId); + if (!iframe) { + throw new Error('Unknown iframe for script "'+scriptUrl+'"'); + } + this.unregisterIframe(iframe); + iframe.remove(); + + this.scripts.delete(scriptUrl); + } + + sendUserInputChat(message: string) { + this.postMessage({ + 'type': 'userInputChat', + 'data': { + 'message': message, + } as UserInputChatEvent + }); + } + + sendEnterEvent(name: string) { + this.postMessage({ + 'type': 'enterEvent', + 'data': { + "name": name + } as EnterLeaveEvent + }); + } + + sendLeaveEvent(name: string) { + this.postMessage({ + 'type': 'leaveEvent', + 'data': { + "name": name + } as EnterLeaveEvent + }); + } + + /** + * Sends the message... to all allowed iframes. + */ + private postMessage(message: IframeEvent) { + for (const iframe of this.iframes) { + iframe.contentWindow?.postMessage(message, '*'); + } + } +} + +export const iframeListener = new IframeListener(); diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index e97899bf..0a953363 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -72,8 +72,10 @@ import {TextureError} from "../../Exception/TextureError"; import {addLoader} from "../Components/Loader"; import {ErrorSceneName} from "../Reconnecting/ErrorScene"; import {localUserStore} from "../../Connexion/LocalUserStore"; +import {iframeListener} from "../../Api/IframeListener"; import DOMElement = Phaser.GameObjects.DOMElement; import Tween = Phaser.Tweens.Tween; +import {HtmlUtils} from "../../WebRtc/HtmlUtils"; export interface GameSceneInitInterface { initPosition: PointInterface|null, @@ -164,8 +166,8 @@ export class GameScene extends ResizableScene implements CenterListener { private openChatIcon!: OpenChatIcon; private playerName!: string; private characterLayers!: string[]; + private popUpElements : Map = new Map(); - private popUpElement : DOMElement| undefined; constructor(private room: Room, MapUrlFile: string, customKey?: string|undefined) { super({ key: customKey ?? room.id @@ -316,6 +318,12 @@ export class GameScene extends ResizableScene implements CenterListener { // }); // }); } + + // Now, let's load the script, if any + const scripts = this.getScriptUrls(this.mapFile); + for (const script of scripts) { + iframeListener.registerScript(script); + } } //hook initialisation @@ -435,6 +443,7 @@ export class GameScene extends ResizableScene implements CenterListener { // From now, this game scene will be notified of reposition events layoutManager.setListener(this); this.triggerOnMapLayerPropertyChange(); + this.listenToIframeEvents(); const camera = this.cameras.main; @@ -648,33 +657,6 @@ export class GameScene extends ResizableScene implements CenterListener { this.gameMap.onPropertyChange('exitSceneUrl', (newValue, oldValue) => { if (newValue) this.onMapExit(newValue as string); }); - this.gameMap.onPropertyChange('inGameConsoleMessage', (newValue, oldValue, allProps) => { - if (newValue !== undefined) { - this.popUpElement?.destroy(); - this.popUpElement = this.add.dom(2100, 150).createFromHTML(newValue as string); - this.popUpElement.scale = 0; - this.tweens.add({ - targets : this.popUpElement , - scale : 1, - ease : "EaseOut", - duration : 400, - }); - - this.popUpElement.setClassName("popUpElement"); - - } else { - this.tweens.add({ - targets : this.popUpElement , - scale : 0, - ease : "EaseOut", - duration : 400, - onComplete : () => { - this.popUpElement?.destroy(); - this.popUpElement = undefined; - }, - }); - } - }); this.gameMap.onPropertyChange('exitUrl', (newValue, oldValue) => { if (newValue) this.onMapExit(newValue as string); }); @@ -684,7 +666,7 @@ export class GameScene extends ResizableScene implements CenterListener { coWebsiteManager.closeCoWebsite(); }else{ const openWebsiteFunction = () => { - coWebsiteManager.loadCoWebsite(newValue as string, this.MapUrlFile, allProps.get('openWebsitePolicy') as string | undefined); + coWebsiteManager.loadCoWebsite(newValue as string, this.MapUrlFile, allProps.get('openWebsiteAllowApi') as boolean | undefined, allProps.get('openWebsitePolicy') as string | undefined); layoutManager.removeActionButton('openWebsite', this.userInputManager); }; @@ -748,6 +730,64 @@ export class GameScene extends ResizableScene implements CenterListener { this.playAudio(newValue, true); }); + this.gameMap.onPropertyChange('zone', (newValue, oldValue) => { + if (newValue === undefined || newValue === false || newValue === '') { + iframeListener.sendLeaveEvent(oldValue as string); + } else { + iframeListener.sendEnterEvent(newValue as string); + } + }); + + } + + private listenToIframeEvents(): void { + iframeListener.openPopupStream.subscribe((openPopupEvent) => { + const escapedMessage = HtmlUtils.escapeHtml(openPopupEvent.message); + + let html = `
+${escapedMessage} +
`; + + const domElement = this.add.dom(150, 150).createFromHTML(html); + domElement.scale = 0; + domElement.setClassName('popUpElement'); + this.tweens.add({ + targets : domElement , + scale : 1, + ease : "EaseOut", + duration : 400, + }); + + this.popUpElements.set(openPopupEvent.popupId, domElement); + }); + /*this.gameMap.onPropertyChange('inGameConsoleMessage', (newValue, oldValue, allProps) => { + if (newValue !== undefined) { + this.popUpElement?.destroy(); + this.popUpElement = this.add.dom(2100, 150).createFromHTML(newValue as string); + this.popUpElement.scale = 0; + this.tweens.add({ + targets : this.popUpElement , + scale : 1, + ease : "EaseOut", + duration : 400, + }); + + this.popUpElement.setClassName("popUpElement"); + + } else { + this.tweens.add({ + targets : this.popUpElement , + scale : 0, + ease : "EaseOut", + duration : 400, + onComplete : () => { + this.popUpElement?.destroy(); + this.popUpElement = undefined; + }, + }); + } + });*/ + } private onMapExit(exitKey: string) { @@ -774,6 +814,12 @@ export class GameScene extends ResizableScene implements CenterListener { public cleanupClosingScene(): void { // stop playing audio, close any open website, stop any open Jitsi coWebsiteManager.closeCoWebsite(); + // Stop the script, if any + const scripts = this.getScriptUrls(this.mapFile); + for (const script of scripts) { + iframeListener.unregisterScript(script); + } + this.stopJitsi(); this.playAudio(undefined); // We are completely destroying the current scene to avoid using a half-backed instance when coming back to the same map. @@ -859,8 +905,12 @@ export class GameScene extends ResizableScene implements CenterListener { return this.getProperty(layer, "startLayer") == true; } - private getProperty(layer: ITiledMapLayer, name: string): string|boolean|number|undefined { - const properties = layer.properties; + private getScriptUrls(map: ITiledMap): string[] { + return (this.getProperties(map, "script") as string[]).map((script) => (new URL(script, this.MapUrlFile)).toString()); + } + + private getProperty(layer: ITiledMapLayer|ITiledMap, name: string): string|boolean|number|undefined { + const properties: ITiledMapLayerProperty[] = layer.properties; if (!properties) { return undefined; } @@ -871,6 +921,14 @@ export class GameScene extends ResizableScene implements CenterListener { return obj.value; } + private getProperties(layer: ITiledMapLayer|ITiledMap, name: string): (string|number|boolean|undefined)[] { + const properties: ITiledMapLayerProperty[] = layer.properties; + if (!properties) { + return []; + } + return properties.filter((property: ITiledMapLayerProperty) => property.name.toLowerCase() === name.toLowerCase()).map((property) => property.value); + } + //todo: push that into the gameManager private async loadNextGame(exitSceneIdentifier: string){ const {roomId, hash} = Room.getIdFromIdentifier(exitSceneIdentifier, this.MapUrlFile, this.instance); diff --git a/front/src/Phaser/Map/ITiledMap.ts b/front/src/Phaser/Map/ITiledMap.ts index 2a82e93a..39e0a1f5 100644 --- a/front/src/Phaser/Map/ITiledMap.ts +++ b/front/src/Phaser/Map/ITiledMap.ts @@ -14,7 +14,7 @@ export interface ITiledMap { * Map orientation (orthogonal) */ orientation: string; - properties: {[key: string]: string}; + properties: ITiledMapLayerProperty[]; /** * Render order (right-down) diff --git a/front/src/WebRtc/CoWebsiteManager.ts b/front/src/WebRtc/CoWebsiteManager.ts index 4e74c4a7..472e7a13 100644 --- a/front/src/WebRtc/CoWebsiteManager.ts +++ b/front/src/WebRtc/CoWebsiteManager.ts @@ -1,4 +1,5 @@ import {HtmlUtils} from "./HtmlUtils"; +import {iframeListener} from "../Api/IframeListener"; export type CoWebsiteStateChangedCallback = () => void; @@ -12,8 +13,8 @@ const cowebsiteDivId = "cowebsite"; // the id of the parent div of the iframe. const animationTime = 500; //time used by the css transitions, in ms. class CoWebsiteManager { - - private opened: iframeStates = iframeStates.closed; + + private opened: iframeStates = iframeStates.closed; private observers = new Array(); /** @@ -21,12 +22,12 @@ class CoWebsiteManager { * So we use this promise to queue up every cowebsite state transition */ private currentOperationPromise: Promise = Promise.resolve(); - private cowebsiteDiv: HTMLDivElement; - + private cowebsiteDiv: HTMLDivElement; + constructor() { this.cowebsiteDiv = HtmlUtils.getElementByIdOrFail(cowebsiteDivId); } - + private close(): void { this.cowebsiteDiv.classList.remove('loaded'); //edit the css class to trigger the transition this.cowebsiteDiv.classList.add('hidden'); @@ -42,7 +43,7 @@ class CoWebsiteManager { this.opened = iframeStates.opened; } - public loadCoWebsite(url: string, base: string, allowPolicy?: string): void { + public loadCoWebsite(url: string, base: string, allowApi?: boolean, allowPolicy?: string): void { this.load(); this.cowebsiteDiv.innerHTML = ` + +
+ + + diff --git a/maps/tests/iframe_api.json b/maps/tests/iframe_api.json new file mode 100644 index 00000000..96d92423 --- /dev/null +++ b/maps/tests/iframe_api.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":"iframe_api", + "opacity":1, + "properties":[ + { + "name":"openWebsite", + "type":"string", + "value":"iframe.html" + }, + { + "name":"openWebsiteAllowApi", + "type":"bool", + "value":true + }], + "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 diff --git a/maps/tests/script.js b/maps/tests/script.js new file mode 100644 index 00000000..5d5f3c62 --- /dev/null +++ b/maps/tests/script.js @@ -0,0 +1,43 @@ +console.log('SCRIPT LAUNCHED'); +WA.sendChatMessage('Hi, my name is Poly and I repeat what you say!', 'Poly Parrot'); + + +WA.onChatMessage((message => { + console.log('CHAT MESSAGE RECEIVED BY SCRIPT'); + WA.sendChatMessage('Poly Parrot says: "'+message+'"', 'Poly Parrot'); +})); + +WA.onEnterZone('myTrigger', () => { + WA.sendChatMessage("Don't step on my carpet!", 'Poly Parrot'); +}) + +WA.onLeaveZone('myTrigger', () => { + WA.sendChatMessage("Thanks!", 'Poly Parrot'); +}) + +WA.onEnterZone('notExist', () => { + WA.sendChatMessage("YOU SHOULD NEVER SEE THIS", 'Poly Parrot'); +}) + +let popupId; + +WA.onEnterZone('popupZone', () => { + popupId = WA.openPopup('foobar', 'This is a test message. Hi!', [ + { + label: "Close", + className: "normal", + closeOnClick: true + }, + { + label: "Next", + className: "success", + callback: () => { + console.log('BUTTON CLICKED') + } + } + ]) +}) + +/*WA.onLeaveZone('popupZone', () => { + WA.sendChatMessage("Thanks!", 'Poly Parrot'); +})*/ diff --git a/maps/tests/script_api.json b/maps/tests/script_api.json new file mode 100644 index 00000000..6b6c91ad --- /dev/null +++ b/maps/tests/script_api.json @@ -0,0 +1,124 @@ +{ "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, 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, 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], + "height":10, + "id":7, + "name":"popupZone", + "opacity":1, + "properties":[ + { + "name":"zone", + "type":"string", + "value":"popupZone" + }], + "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":120.377012261239, + "id":1, + "name":"myPopup", + "rotation":0, + "type":"", + "visible":true, + "width":162.815914588373, + "x":77.4042872633247, + "y":61.1226958044874 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":8, + "nextobjectid":2, + "orientation":"orthogonal", + "properties":[ + { + "name":"script", + "type":"string", + "value":"script.js" + }], + "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