From 3836d5037c95d2ec78a3a8855ed2c7c0df5ed20b Mon Sep 17 00:00:00 2001 From: jonny Date: Wed, 21 Apr 2021 15:51:01 +0200 Subject: [PATCH 01/46] game state can be read out by the client APIs # Conflicts: # front/src/Api/IframeListener.ts # front/src/Phaser/Game/GameScene.ts # front/src/iframe_api.ts --- front/src/Api/Events/ApiGameStateEvent.ts | 11 +++++++++++ front/src/Api/IframeListener.ts | 16 ++++++++++++++++ front/src/Phaser/Game/GameScene.ts | 7 +++++++ front/src/iframe_api.ts | 22 ++++++++++++++++++++++ front/src/utility.ts | 18 ++++++++++++++++++ 5 files changed, 74 insertions(+) create mode 100644 front/src/Api/Events/ApiGameStateEvent.ts create mode 100644 front/src/utility.ts diff --git a/front/src/Api/Events/ApiGameStateEvent.ts b/front/src/Api/Events/ApiGameStateEvent.ts new file mode 100644 index 00000000..2d5ec686 --- /dev/null +++ b/front/src/Api/Events/ApiGameStateEvent.ts @@ -0,0 +1,11 @@ +import * as tg from "generic-type-guard"; + +export const isGameStateEvent = + new tg.IsInterface().withProperties({ + roomId: tg.isString, + data:tg.isObject + }).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 GameStateEvent = tg.GuardedType; \ No newline at end of file diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index c875ebbb..ef7dc6a3 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -12,6 +12,8 @@ import {ClosePopupEvent, isClosePopupEvent} from "./Events/ClosePopupEvent"; import {scriptUtils} from "./ScriptUtils"; import {GoToPageEvent, isGoToPageEvent} from "./Events/GoToPageEvent"; import {isOpenCoWebsite, OpenCoWebSiteEvent} from "./Events/OpenCoWebSiteEvent"; +import { GameStateEvent } from './Events/ApiGameStateEvent'; +import { deepFreezeClone as deepFreezeClone } from '../utility'; /** @@ -52,6 +54,10 @@ class IframeListener { private readonly _removeBubbleStream: Subject = new Subject(); public readonly removeBubbleStream = this._removeBubbleStream.asObservable(); + + private readonly _gameStateStream: Subject = new Subject(); + public readonly gameStateStream = this._gameStateStream.asObservable(); + private readonly iframes = new Set(); private readonly scripts = new Map(); @@ -103,6 +109,8 @@ class IframeListener { } else if (payload.type === 'removeBubble'){ this._removeBubbleStream.next(); + }else if(payload.type=="getState"){ + this._gameStateStream.next(); } } @@ -111,6 +119,14 @@ class IframeListener { } + + sendFrozenGameStateEvent(gameStateEvent: GameStateEvent) { + this.postMessage({ + 'type': 'gameState', + 'data': deepFreezeClone(gameStateEvent) + }); + } + /** * Allows the passed iFrame to send/receive messages via the API. */ diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 990f702c..ae9f23b8 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -841,6 +841,13 @@ ${escapedMessage} this.iframeSubscriptionList.push(iframeListener.enablePlayerControlStream.subscribe(()=>{ this.userInputManager.restoreControls(); })); + this.iframeSubscriptionList.push(iframeListener.gameStateStream.subscribe(()=>{ + iframeListener.sendFrozenGameStateEvent({ + roomId:this.RoomId, + data: this.mapFile + }) + })); + let scriptedBubbleSprite : Sprite; this.iframeSubscriptionList.push(iframeListener.displayBubbleStream.subscribe(()=>{ scriptedBubbleSprite = new Sprite(this,this.CurrentPlayer.x + 25,this.CurrentPlayer.y,'circleSprite-white'); diff --git a/front/src/iframe_api.ts b/front/src/iframe_api.ts index 18d8d172..b1a8de48 100644 --- a/front/src/iframe_api.ts +++ b/front/src/iframe_api.ts @@ -9,6 +9,7 @@ import {ClosePopupEvent} from "./Api/Events/ClosePopupEvent"; import {OpenTabEvent} from "./Api/Events/OpenTabEvent"; import {GoToPageEvent} from "./Api/Events/GoToPageEvent"; import {OpenCoWebSiteEvent} from "./Api/Events/OpenCoWebSiteEvent"; +import { GameStateEvent, isGameStateEvent } from './Api/Events/ApiGameStateEvent'; interface WorkAdventureApi { sendChatMessage(message: string, author: string): void; @@ -24,6 +25,7 @@ interface WorkAdventureApi { restorePlayerControl() : void; displayBubble() : void; removeBubble() : void; + getGameState():Promise } declare global { @@ -74,7 +76,23 @@ class Popup { } } + +const stateResolvers:Array<(event:GameStateEvent)=>void> =[] + window.WA = { + + + + getGameState(){ + return new Promise((resolver,thrower)=>{ + stateResolvers.push(resolver); + window.parent.postMessage({ + type:"getState" + },"*") + }) + }, + + /** * Send a message in the chat. * Only the local user will receive this message. @@ -224,6 +242,10 @@ window.addEventListener('message', message => { if (callback) { callback(popup); } + }else if(payload.type=="gameState" && isGameStateEvent(payloadData)){ + stateResolvers.forEach(resolver=>{ + resolver(payloadData); + }) } } diff --git a/front/src/utility.ts b/front/src/utility.ts new file mode 100644 index 00000000..a95da6f8 --- /dev/null +++ b/front/src/utility.ts @@ -0,0 +1,18 @@ +export function deepFreezeClone (obj:T):Readonly { + return deepFreeze(JSON.parse(JSON.stringify(obj))); +} + +function deepFreeze (obj:T):T{ + Object.freeze(obj); + if (obj === undefined) { + return obj; + } + const propertyNames = Object.getOwnPropertyNames(obj) as Array; + propertyNames.forEach(function (prop) { + if (obj[prop] !== null&& (typeof obj[prop] === "object" || typeof obj[prop] === "function") && !Object.isFrozen(obj[prop])) { + deepFreezeClone(obj[prop]); + } + }); + + return obj; +} \ No newline at end of file From 79e530f0e60ac4e6156ad6afadbb3a8259fb6860 Mon Sep 17 00:00:00 2001 From: jonny Date: Tue, 27 Apr 2021 00:04:08 +0200 Subject: [PATCH 02/46] launch jsons + type fixes --- back/.vscode/launch.json | 27 +++++++++++++++++++++++++++ front/src/Phaser/Game/GameScene.ts | 2 +- pusher/.vscode/launch.json | 27 +++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 back/.vscode/launch.json create mode 100644 pusher/.vscode/launch.json diff --git a/back/.vscode/launch.json b/back/.vscode/launch.json new file mode 100644 index 00000000..77cdeee0 --- /dev/null +++ b/back/.vscode/launch.json @@ -0,0 +1,27 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Example", + "type": "node", + "request": "launch", + "runtimeExecutable": "node", + "runtimeArgs": [ + "--nolazy", + "-r", + "ts-node/register/transpile-only" + ], + "args": [ + "server.ts", + "--example", + "hello" + ], + "cwd": "${workspaceRoot}", + "internalConsoleOptions": "openOnSessionStart", + "skipFiles": [ + "/**", + "node_modules/**" + ] + } + ] +} \ No newline at end of file diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 2995fbc0..7c48239b 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -151,7 +151,7 @@ export class GameScene extends ResizableScene implements CenterListener { private GlobalMessageManager!: GlobalMessageManager; public ConsoleGlobalMessageManager!: ConsoleGlobalMessageManager; private connectionAnswerPromise: Promise; - private connectionAnswerPromiseResolve!: (value?: RoomJoinedMessageInterface | PromiseLike) => void; + private connectionAnswerPromiseResolve!: (value: RoomJoinedMessageInterface | PromiseLike) => void; // A promise that will resolve when the "create" method is called (signaling loading is ended) private createPromise: Promise; private createPromiseResolve!: (value?: void | PromiseLike) => void; diff --git a/pusher/.vscode/launch.json b/pusher/.vscode/launch.json new file mode 100644 index 00000000..2a3c02c2 --- /dev/null +++ b/pusher/.vscode/launch.json @@ -0,0 +1,27 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Pusher", + "type": "node", + "request": "launch", + "runtimeExecutable": "node", + "runtimeArgs": [ + "--nolazy", + "-r", + "ts-node/register/transpile-only" + ], + "args": [ + "server.ts", + "--example", + "hello" + ], + "cwd": "${workspaceRoot}", + "internalConsoleOptions": "openOnSessionStart", + "skipFiles": [ + "/**", + "node_modules/**" + ] + } + ] +} \ No newline at end of file From fafaabb6e7226e033c6a132d6f8ab270fcd11e1b Mon Sep 17 00:00:00 2001 From: jonny Date: Tue, 27 Apr 2021 11:59:22 +0200 Subject: [PATCH 03/46] script api can add menu commands # Conflicts: # front/src/Api/IframeListener.ts # front/src/iframe_api.ts --- front/src/Api/Events/MenuItemClickedEvent.ts | 10 ++++++ front/src/Api/Events/MenuItemRegisterEvent.ts | 10 ++++++ front/src/Api/IframeListener.ts | 15 +++++++++ front/src/Phaser/Menu/MenuScene.ts | 33 +++++++++++++++++-- front/src/iframe_api.ts | 21 ++++++++++-- 5 files changed, 84 insertions(+), 5 deletions(-) create mode 100644 front/src/Api/Events/MenuItemClickedEvent.ts create mode 100644 front/src/Api/Events/MenuItemRegisterEvent.ts diff --git a/front/src/Api/Events/MenuItemClickedEvent.ts b/front/src/Api/Events/MenuItemClickedEvent.ts new file mode 100644 index 00000000..dd80c0f2 --- /dev/null +++ b/front/src/Api/Events/MenuItemClickedEvent.ts @@ -0,0 +1,10 @@ +import * as tg from "generic-type-guard"; + +export const isMenuItemClickedEvent = + new tg.IsInterface().withProperties({ + menuItem: 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 MenuItemClickedEvent = tg.GuardedType; diff --git a/front/src/Api/Events/MenuItemRegisterEvent.ts b/front/src/Api/Events/MenuItemRegisterEvent.ts new file mode 100644 index 00000000..98d4c7d3 --- /dev/null +++ b/front/src/Api/Events/MenuItemRegisterEvent.ts @@ -0,0 +1,10 @@ +import * as tg from "generic-type-guard"; + +export const isMenuItemRegisterEvent = + new tg.IsInterface().withProperties({ + menutItem: 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 MenuItemRegisterEvent = tg.GuardedType; diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index c875ebbb..dbb45db3 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -12,6 +12,8 @@ import {ClosePopupEvent, isClosePopupEvent} from "./Events/ClosePopupEvent"; import {scriptUtils} from "./ScriptUtils"; import {GoToPageEvent, isGoToPageEvent} from "./Events/GoToPageEvent"; import {isOpenCoWebsite, OpenCoWebSiteEvent} from "./Events/OpenCoWebSiteEvent"; +import { isMenuItemRegisterEvent } from './Events/MenuItemRegisterEvent'; +import { MenuItemClickedEvent } from './Events/MenuItemClickedEvent'; /** @@ -52,6 +54,8 @@ class IframeListener { private readonly _removeBubbleStream: Subject = new Subject(); public readonly removeBubbleStream = this._removeBubbleStream.asObservable(); + private readonly _registerMenuCommandStream: Subject = new Subject(); + public readonly registerMenuCommandStream = this._registerMenuCommandStream.asObservable(); private readonly iframes = new Set(); private readonly scripts = new Map(); @@ -103,6 +107,8 @@ class IframeListener { } else if (payload.type === 'removeBubble'){ this._removeBubbleStream.next(); + } else if (payload.type == "registerMenuCommand" && isMenuItemRegisterEvent(payload.data)) { + this._registerMenuCommandStream.next(payload.data.menutItem) } } @@ -187,6 +193,15 @@ class IframeListener { this.scripts.delete(scriptUrl); } + sendMenuClickedEvent(menuItem: string) { + this.postMessage({ + 'type': 'menuItemClicked', + 'data': { + menuItem: menuItem, + } as MenuItemClickedEvent + }); + } + sendUserInputChat(message: string) { this.postMessage({ 'type': 'userInputChat', diff --git a/front/src/Phaser/Menu/MenuScene.ts b/front/src/Phaser/Menu/MenuScene.ts index 05cea305..9e11a873 100644 --- a/front/src/Phaser/Menu/MenuScene.ts +++ b/front/src/Phaser/Menu/MenuScene.ts @@ -9,6 +9,9 @@ import {connectionManager} from "../../Connexion/ConnectionManager"; import {GameConnexionTypes} from "../../Url/UrlManager"; import {WarningContainer, warningContainerHtml, warningContainerKey} from "../Components/WarningContainer"; import {worldFullWarningStream} from "../../Connexion/WorldFullWarningStream"; +import { HtmlUtils } from '../../WebRtc/HtmlUtils'; +import { iframeListener } from '../../Api/IframeListener'; +import { Subscription } from 'rxjs'; export const MenuSceneName = 'MenuScene'; const gameMenuKey = 'gameMenu'; @@ -36,11 +39,20 @@ export class MenuScene extends Phaser.Scene { private warningContainer: WarningContainer | null = null; private warningContainerTimeout: NodeJS.Timeout | null = null; + private apiMenus = [] + + + private subscriptions = new Subscription() constructor() { super({key: MenuSceneName}); this.gameQualityValue = localUserStore.getGameQualityValue(); this.videoQualityValue = localUserStore.getVideoQualityValue(); + + this.subscriptions.add(iframeListener.registerMenuCommandStream.subscribe(menuCommand => { + this.addMenuOption(menuCommand); + + })) } preload () { @@ -266,13 +278,28 @@ export class MenuScene extends Phaser.Scene { }); } - private onMenuClick(event:MouseEvent) { - if((event?.target as HTMLInputElement).classList.contains('not-button')){ + public addMenuOption(menuText: string) { + const wrappingSection = document.createElement("section") + wrappingSection.innerHTML = `` + const menuItemContainer = this.menuElement.node.querySelector("#gameMenu main"); + if (menuItemContainer) { + menuItemContainer.insertBefore(wrappingSection, menuItemContainer.querySelector("#socialLinks")) + } + } + + private onMenuClick(event: MouseEvent) { + const htmlMenuItem = (event?.target as HTMLInputElement); + if (htmlMenuItem.classList.contains('not-button')) { return; } event.preventDefault(); - switch ((event?.target as HTMLInputElement).id) { + if (htmlMenuItem.classList.contains("fromApi")) { + iframeListener.sendMenuClickedEvent(htmlMenuItem.id) + return + } + + switch (htmlMenuItem.id) { case 'changeNameButton': this.closeSideMenu(); gameManager.leaveGame(this, LoginSceneName, new LoginScene()); diff --git a/front/src/iframe_api.ts b/front/src/iframe_api.ts index 18d8d172..1b68b0c1 100644 --- a/front/src/iframe_api.ts +++ b/front/src/iframe_api.ts @@ -9,6 +9,8 @@ import {ClosePopupEvent} from "./Api/Events/ClosePopupEvent"; import {OpenTabEvent} from "./Api/Events/OpenTabEvent"; import {GoToPageEvent} from "./Api/Events/GoToPageEvent"; import {OpenCoWebSiteEvent} from "./Api/Events/OpenCoWebSiteEvent"; +import { isMenuItemClickedEvent } from './Api/Events/MenuItemClickedEvent'; +import { MenuItemRegisterEvent } from './Api/Events/MenuItemRegisterEvent'; interface WorkAdventureApi { sendChatMessage(message: string, author: string): void; @@ -24,6 +26,7 @@ interface WorkAdventureApi { restorePlayerControl() : void; displayBubble() : void; removeBubble() : void; + registerMenuCommand(commandDescriptor: string, callback: (commandDescriptor: string) => void): void } declare global { @@ -40,7 +43,7 @@ const enterStreams: Map> = new Map> = new Map>(); const popups: Map = new Map(); const popupCallbacks: Map> = new Map>(); - +const menuCallbacks: Map void> = new Map() let popupId = 0; interface ButtonDescriptor { /** @@ -172,6 +175,16 @@ window.WA = { popups.set(popupId, popup) return popup; }, + + registerMenuCommand(commandDescriptor: string, callback: (commandDescriptor: string) => void) { + menuCallbacks.set(commandDescriptor, callback); + window.parent.postMessage({ + 'type': 'registerMenuCommand', + 'data': { + menutItem: commandDescriptor + } as MenuItemRegisterEvent + }, '*'); + }, /** * Listen to messages sent by the local user, in the chat. */ @@ -224,8 +237,12 @@ window.addEventListener('message', message => { if (callback) { callback(popup); } + } else if (payload.type == "menuItemClicked" && isMenuItemClickedEvent(payload.data)) { + const callback = menuCallbacks.get(payload.data.menuItem); + if (callback) { + callback(payload.data.menuItem) + } } - } // ... From 4069e878721deffa2d0e2b3833f62f05b834aca2 Mon Sep 17 00:00:00 2001 From: jonny Date: Tue, 27 Apr 2021 12:40:29 +0200 Subject: [PATCH 04/46] replace menu items if already present --- front/src/Phaser/Menu/MenuScene.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/front/src/Phaser/Menu/MenuScene.ts b/front/src/Phaser/Menu/MenuScene.ts index 9e11a873..348554b3 100644 --- a/front/src/Phaser/Menu/MenuScene.ts +++ b/front/src/Phaser/Menu/MenuScene.ts @@ -280,9 +280,11 @@ export class MenuScene extends Phaser.Scene { public addMenuOption(menuText: string) { const wrappingSection = document.createElement("section") - wrappingSection.innerHTML = `` + const excapedHtml = HtmlUtils.escapeHtml(menuText); + wrappingSection.innerHTML = `` const menuItemContainer = this.menuElement.node.querySelector("#gameMenu main"); if (menuItemContainer) { + menuItemContainer.querySelector(`#${excapedHtml}.fromApi`)?.remove() menuItemContainer.insertBefore(wrappingSection, menuItemContainer.querySelector("#socialLinks")) } } From 6295c8275ec6b3b3f71306ac5bb3af2ee4b2ea67 Mon Sep 17 00:00:00 2001 From: jonny Date: Tue, 27 Apr 2021 16:40:56 +0200 Subject: [PATCH 05/46] reset menu items on map change --- front/src/Phaser/Game/GameScene.ts | 3 +++ front/src/Phaser/Menu/MenuScene.ts | 11 +++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 464c3ca4..1a7a2d9f 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -90,6 +90,7 @@ import {LayersIterator} from "../Map/LayersIterator"; import {touchScreenManager} from "../../Touch/TouchScreenManager"; import {PinchManager} from "../UserInput/PinchManager"; import {joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey} from "../Components/MobileJoystick"; +import { MenuScene, MenuSceneName } from '../Menu/MenuScene'; export interface GameSceneInitInterface { initPosition: PointInterface|null, @@ -880,6 +881,8 @@ ${escapedMessage} const {roomId, hash} = Room.getIdFromIdentifier(exitKey, this.MapUrlFile, this.instance); if (!roomId) throw new Error('Could not find the room from its exit key: '+exitKey); urlManager.pushStartLayerNameToUrl(hash); + const menuScene: MenuScene = this.scene.get(MenuSceneName) as MenuScene + menuScene.reset() if (roomId !== this.scene.key) { if (this.scene.get(roomId) === null) { console.error("next room not loaded", exitKey); diff --git a/front/src/Phaser/Menu/MenuScene.ts b/front/src/Phaser/Menu/MenuScene.ts index 348554b3..702fb67b 100644 --- a/front/src/Phaser/Menu/MenuScene.ts +++ b/front/src/Phaser/Menu/MenuScene.ts @@ -38,10 +38,6 @@ export class MenuScene extends Phaser.Scene { private menuButton!: Phaser.GameObjects.DOMElement; private warningContainer: WarningContainer | null = null; private warningContainerTimeout: NodeJS.Timeout | null = null; - - private apiMenus = [] - - private subscriptions = new Subscription() constructor() { super({key: MenuSceneName}); @@ -64,6 +60,13 @@ export class MenuScene extends Phaser.Scene { this.load.html(warningContainerKey, warningContainerHtml); } + reset() { + const addedMenuItems=[...this.menuElement.node.querySelectorAll(".fromApi")]; + for(let index=addedMenuItems.length-1;index>=0;index--){ + addedMenuItems[index].remove() + } + } + create() { this.menuElement = this.add.dom(closedSideMenuX, 30).createFromCache(gameMenuKey); this.menuElement.setOrigin(0); From cd77af318d779a10deb136b980d5dc1304340f30 Mon Sep 17 00:00:00 2001 From: jonny Date: Sat, 1 May 2021 19:44:14 +0200 Subject: [PATCH 06/46] added more properties # Conflicts: # front/src/Phaser/Game/GameScene.ts --- front/src/Api/Events/ApiGameStateEvent.ts | 21 ++++++++++++++- front/src/Phaser/Game/GameScene.ts | 32 ++++++++++++++++++++--- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/front/src/Api/Events/ApiGameStateEvent.ts b/front/src/Api/Events/ApiGameStateEvent.ts index 2d5ec686..4f4e98ff 100644 --- a/front/src/Api/Events/ApiGameStateEvent.ts +++ b/front/src/Api/Events/ApiGameStateEvent.ts @@ -1,9 +1,28 @@ import * as tg from "generic-type-guard"; +export const isPositionState = new tg.IsInterface().withProperties({ + x: tg.isNumber, + y: tg.isNumber +}).get() +export const isPlayerState = new tg.IsInterface() + .withStringIndexSignature( + new tg.IsInterface().withProperties({ + position: isPositionState, + pusherId: tg.isUnion(tg.isNumber, tg.isUndefined) + }).get() + ).get() + +export type PlayerStateObject = tg.GuardedType; + export const isGameStateEvent = new tg.IsInterface().withProperties({ roomId: tg.isString, - data:tg.isObject + data: tg.isObject, + mapUrl: tg.isString, + nickName: tg.isString, + uuid: tg.isUnion(tg.isString, tg.isUndefined), + players: isPlayerState, + startLayerName: tg.isUnion(tg.isString, tg.isNull) }).get(); /** * A message sent from the game to the iFrame when a user enters or leaves a zone marked with the "zone" property. diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index ae9f23b8..3841ab07 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -80,6 +80,7 @@ import { lazyLoadCompanionResource } from "../Companion/CompanionTexturesLoading import {touchScreenManager} from "../../Touch/TouchScreenManager"; import {PinchManager} from "../UserInput/PinchManager"; import {joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey} from "../Components/MobileJoystick"; +import { PlayerStateObject } from '../../Api/Events/ApiGameStateEvent'; export interface GameSceneInitInterface { initPosition: PointInterface|null, @@ -841,10 +842,35 @@ ${escapedMessage} this.iframeSubscriptionList.push(iframeListener.enablePlayerControlStream.subscribe(()=>{ this.userInputManager.restoreControls(); })); - this.iframeSubscriptionList.push(iframeListener.gameStateStream.subscribe(()=>{ + this.iframeSubscriptionList.push(iframeListener.gameStateStream.subscribe(() => { + const playerObject: PlayerStateObject = { + [this.playerName]: { + position: { + x: this.CurrentPlayer.x, + y: this.CurrentPlayer.y + }, + pusherId: this.connection?.getUserId() + } + } + for (const mapPlayer of this.MapPlayers.children.entries) { + const remotePlayer: RemotePlayer = mapPlayer as RemotePlayer; + playerObject[remotePlayer.PlayerValue] = { + position: { + x: remotePlayer.x, + y: remotePlayer.y + }, + pusherId: remotePlayer.userId + + } + } iframeListener.sendFrozenGameStateEvent({ - roomId:this.RoomId, - data: this.mapFile + mapUrl: this.MapUrlFile, + nickName: this.playerName, + startLayerName: this.startLayerName, + uuid: localUserStore.getLocalUser()?.uuid, + roomId: this.RoomId, + data: this.mapFile, + players: playerObject }) })); From ffe03d40f5691c4114269916d68bbff78ba98c7b Mon Sep 17 00:00:00 2001 From: jonny Date: Mon, 10 May 2021 00:27:21 +0200 Subject: [PATCH 07/46] option to update tile # Conflicts: # front/src/Api/Events/ApiUpdateTileEvent.ts # front/src/Api/IframeListener.ts # front/src/Phaser/Game/GameScene.ts --- front/src/Api/Events/ApiUpdateTileEvent.ts | 16 + front/src/Api/IframeListener.ts | 7 + front/src/Phaser/Game/GameScene.ts | 346 ++++++++++++--------- front/src/Phaser/Map/ITiledMap.ts | 21 +- 4 files changed, 227 insertions(+), 163 deletions(-) create mode 100644 front/src/Api/Events/ApiUpdateTileEvent.ts diff --git a/front/src/Api/Events/ApiUpdateTileEvent.ts b/front/src/Api/Events/ApiUpdateTileEvent.ts new file mode 100644 index 00000000..8a53fbe5 --- /dev/null +++ b/front/src/Api/Events/ApiUpdateTileEvent.ts @@ -0,0 +1,16 @@ + +import * as tg from "generic-type-guard"; +export const updateTile = "updateTile" + + +export const isUpdateTileEvent = + new tg.IsInterface().withProperties({ + x: tg.isNumber, + y: tg.isNumber, + tile: tg.isUnion(tg.isNumber, tg.isString), + layer: tg.isUnion(tg.isNumber, 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 UpdateTileEvent = tg.GuardedType; \ No newline at end of file diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index f20e055c..715eddc0 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -57,6 +57,9 @@ class IframeListener { private readonly _removeBubbleStream: Subject = new Subject(); public readonly removeBubbleStream = this._removeBubbleStream.asObservable(); + private readonly _updateTileEvent: Subject = new Subject(); + public readonly updateTileEvent = this._updateTileEvent.asObservable(); + private readonly iframes = new Set(); private readonly scripts = new Map(); @@ -110,6 +113,10 @@ class IframeListener { this._removeBubbleStream.next(); }else if (payload.type === 'loadPage' && isLoadPageEvent(payload.data)){ this._loadPageStream.next(payload.data.url); + } else if (payload.type == "getState") { + this._gameStateStream.next(); + } else if (payload.type == "updateTile" && isUpdateTileEvent(payload.data)) { + this._updateTileEvent.next(payload.data) } } diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 7c48239b..138ca5ae 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -1,4 +1,4 @@ -import {gameManager, HasMovedEvent} from "./GameManager"; +import { gameManager, HasMovedEvent } from "./GameManager"; import { GroupCreatedUpdatedMessageInterface, MessageUserJoined, @@ -9,7 +9,7 @@ import { PositionInterface, RoomJoinedMessageInterface } from "../../Connexion/ConnexionModels"; -import {CurrentGamerInterface, hasMovedEventName, Player} from "../Player/Player"; +import { CurrentGamerInterface, hasMovedEventName, Player } from "../Player/Player"; import { DEBUG_MODE, JITSI_PRIVATE_MODE, @@ -27,15 +27,15 @@ import { ITiledMapTileLayer, ITiledTileSet } from "../Map/ITiledMap"; -import {AddPlayerInterface} from "./AddPlayerInterface"; -import {PlayerAnimationDirections} from "../Player/Animation"; -import {PlayerMovement} from "./PlayerMovement"; -import {PlayersPositionInterpolator} from "./PlayersPositionInterpolator"; -import {RemotePlayer} from "../Entity/RemotePlayer"; -import {Queue} from 'queue-typescript'; -import {SimplePeer, UserSimplePeerInterface} from "../../WebRtc/SimplePeer"; -import {ReconnectingSceneName} from "../Reconnecting/ReconnectingScene"; -import {lazyLoadPlayerCharacterTextures, loadCustomTexture} from "../Entity/PlayerTexturesLoadingManager"; +import { AddPlayerInterface } from "./AddPlayerInterface"; +import { PlayerAnimationDirections } from "../Player/Animation"; +import { PlayerMovement } from "./PlayerMovement"; +import { PlayersPositionInterpolator } from "./PlayersPositionInterpolator"; +import { RemotePlayer } from "../Entity/RemotePlayer"; +import { Queue } from 'queue-typescript'; +import { SimplePeer, UserSimplePeerInterface } from "../../WebRtc/SimplePeer"; +import { ReconnectingSceneName } from "../Reconnecting/ReconnectingScene"; +import { lazyLoadPlayerCharacterTextures, loadCustomTexture } from "../Entity/PlayerTexturesLoadingManager"; import { CenterListener, JITSI_MESSAGE_PROPERTIES, @@ -48,52 +48,56 @@ import { AUDIO_VOLUME_PROPERTY, AUDIO_LOOP_PROPERTY } from "../../WebRtc/LayoutManager"; -import {GameMap} from "./GameMap"; -import {coWebsiteManager} from "../../WebRtc/CoWebsiteManager"; -import {mediaManager} from "../../WebRtc/MediaManager"; -import {ItemFactoryInterface} from "../Items/ItemFactoryInterface"; -import {ActionableItem} from "../Items/ActionableItem"; -import {UserInputManager} from "../UserInput/UserInputManager"; -import {UserMovedMessage} from "../../Messages/generated/messages_pb"; -import {ProtobufClientUtils} from "../../Network/ProtobufClientUtils"; -import {connectionManager} from "../../Connexion/ConnectionManager"; -import {RoomConnection} from "../../Connexion/RoomConnection"; -import {GlobalMessageManager} from "../../Administration/GlobalMessageManager"; -import {userMessageManager} from "../../Administration/UserMessageManager"; -import {ConsoleGlobalMessageManager} from "../../Administration/ConsoleGlobalMessageManager"; -import {ResizableScene} from "../Login/ResizableScene"; -import {Room} from "../../Connexion/Room"; -import {jitsiFactory} from "../../WebRtc/JitsiFactory"; -import {urlManager} from "../../Url/UrlManager"; -import {audioManager} from "../../WebRtc/AudioManager"; -import {PresentationModeIcon} from "../Components/PresentationModeIcon"; -import {ChatModeIcon} from "../Components/ChatModeIcon"; -import {OpenChatIcon, openChatIconName} from "../Components/OpenChatIcon"; -import {SelectCharacterScene, SelectCharacterSceneName} from "../Login/SelectCharacterScene"; -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 {HtmlUtils} from "../../WebRtc/HtmlUtils"; +import { GameMap } from "./GameMap"; +import { coWebsiteManager } from "../../WebRtc/CoWebsiteManager"; +import { mediaManager } from "../../WebRtc/MediaManager"; +import { ItemFactoryInterface } from "../Items/ItemFactoryInterface"; +import { ActionableItem } from "../Items/ActionableItem"; +import { UserInputManager } from "../UserInput/UserInputManager"; +import { UserMovedMessage } from "../../Messages/generated/messages_pb"; +import { ProtobufClientUtils } from "../../Network/ProtobufClientUtils"; +import { connectionManager } from "../../Connexion/ConnectionManager"; +import { RoomConnection } from "../../Connexion/RoomConnection"; +import { GlobalMessageManager } from "../../Administration/GlobalMessageManager"; +import { userMessageManager } from "../../Administration/UserMessageManager"; +import { ConsoleGlobalMessageManager } from "../../Administration/ConsoleGlobalMessageManager"; +import { ResizableScene } from "../Login/ResizableScene"; +import { Room } from "../../Connexion/Room"; +import { jitsiFactory } from "../../WebRtc/JitsiFactory"; +import { urlManager } from "../../Url/UrlManager"; +import { audioManager } from "../../WebRtc/AudioManager"; +import { PresentationModeIcon } from "../Components/PresentationModeIcon"; +import { ChatModeIcon } from "../Components/ChatModeIcon"; +import { OpenChatIcon, openChatIconName } from "../Components/OpenChatIcon"; +import { SelectCharacterScene, SelectCharacterSceneName } from "../Login/SelectCharacterScene"; +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 { HtmlUtils } from "../../WebRtc/HtmlUtils"; 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 EVENT_TYPE =Phaser.Scenes.Events -import {Subscription} from "rxjs"; -import {worldFullMessageStream} from "../../Connexion/WorldFullMessageStream"; +import EVENT_TYPE = Phaser.Scenes.Events +import { Subscription } from "rxjs"; +import { worldFullMessageStream } from "../../Connexion/WorldFullMessageStream"; import { lazyLoadCompanionResource } from "../Companion/CompanionTexturesLoadingManager"; import {TextUtils} from "../Components/TextUtils"; import {LayersIterator} from "../Map/LayersIterator"; import {touchScreenManager} from "../../Touch/TouchScreenManager"; import {PinchManager} from "../UserInput/PinchManager"; import {joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey} from "../Components/MobileJoystick"; +import { TextUtils } from "../Components/TextUtils"; +import { touchScreenManager } from "../../Touch/TouchScreenManager"; +import { PinchManager } from "../UserInput/PinchManager"; +import { joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey } from "../Components/MobileJoystick"; export interface GameSceneInitInterface { - initPosition: PointInterface|null, + initPosition: PointInterface | null, reconnecting: boolean } @@ -130,10 +134,10 @@ interface DeleteGroupEventInterface { const defaultStartLayerName = 'start'; export class GameScene extends ResizableScene implements CenterListener { - Terrains : Array; + Terrains: Array; CurrentPlayer!: CurrentGamerInterface; MapPlayers!: Phaser.Physics.Arcade.Group; - MapPlayersByKey : Map = new Map(); + MapPlayersByKey: Map = new Map(); Map!: Phaser.Tilemaps.Tilemap; Layers!: Array; Objects!: Array; @@ -143,10 +147,10 @@ export class GameScene extends ResizableScene implements CenterListener { startY!: number; circleTexture!: CanvasTexture; circleRedTexture!: CanvasTexture; - pendingEvents: Queue = new Queue(); - private initPosition: PositionInterface|null = null; + pendingEvents: Queue = new Queue(); + private initPosition: PositionInterface | null = null; private playersPositionInterpolator = new PlayersPositionInterpolator(); - public connection: RoomConnection|undefined; + public connection: RoomConnection | undefined; private simplePeer!: SimplePeer; private GlobalMessageManager!: GlobalMessageManager; public ConsoleGlobalMessageManager!: ConsoleGlobalMessageManager; @@ -155,7 +159,7 @@ export class GameScene extends ResizableScene implements CenterListener { // A promise that will resolve when the "create" method is called (signaling loading is ended) private createPromise: Promise; private createPromiseResolve!: (value?: void | PromiseLike) => void; - private iframeSubscriptionList! : Array; + private iframeSubscriptionList!: Array; MapUrlFile: string; RoomId: string; instance: string; @@ -174,19 +178,19 @@ export class GameScene extends ResizableScene implements CenterListener { private gameMap!: GameMap; private actionableItems: Map = new Map(); // The item that can be selected by pressing the space key. - private outlinedItem: ActionableItem|null = null; + private outlinedItem: ActionableItem | null = null; public userInputManager!: UserInputManager; - private isReconnecting: boolean|undefined = undefined; + private isReconnecting: boolean | undefined = undefined; private startLayerName!: string | null; private openChatIcon!: OpenChatIcon; private playerName!: string; private characterLayers!: string[]; - private companion!: string|null; - private messageSubscription: Subscription|null = null; - private popUpElements : Map = new Map(); - private originalMapUrl: string|undefined; + private companion!: string | null; + private messageSubscription: Subscription | null = null; + private popUpElements: Map = new Map(); + private originalMapUrl: string | undefined; - constructor(private room: Room, MapUrlFile: string, customKey?: string|undefined) { + constructor(private room: Room, MapUrlFile: string, customKey?: string | undefined) { super({ key: customKey ?? room.id }); @@ -222,13 +226,13 @@ export class GameScene extends ResizableScene implements CenterListener { this.load.image(joystickBaseKey, joystickBaseImg); this.load.image(joystickThumbKey, joystickThumbImg); } - this.load.on(FILE_LOAD_ERROR, (file: {src: string}) => { + this.load.on(FILE_LOAD_ERROR, (file: { src: string }) => { // If we happen to be in HTTP and we are trying to load a URL in HTTPS only... (this happens only in dev environments) if (window.location.protocol === 'http:' && file.src === this.MapUrlFile && file.src.startsWith('http:') && this.originalMapUrl === undefined) { this.originalMapUrl = this.MapUrlFile; this.MapUrlFile = this.MapUrlFile.replace('http://', 'https://'); this.load.tilemapTiledJSON(this.MapUrlFile, this.MapUrlFile); - this.load.on('filecomplete-tilemapJSON-'+this.MapUrlFile, (key: string, type: string, data: unknown) => { + this.load.on('filecomplete-tilemapJSON-' + this.MapUrlFile, (key: string, type: string, data: unknown) => { this.onMapLoad(data); }); return; @@ -242,7 +246,7 @@ export class GameScene extends ResizableScene implements CenterListener { this.originalMapUrl = this.MapUrlFile; this.MapUrlFile = this.MapUrlFile.replace('https://', 'http://'); this.load.tilemapTiledJSON(this.MapUrlFile, this.MapUrlFile); - this.load.on('filecomplete-tilemapJSON-'+this.MapUrlFile, (key: string, type: string, data: unknown) => { + this.load.on('filecomplete-tilemapJSON-' + this.MapUrlFile, (key: string, type: string, data: unknown) => { this.onMapLoad(data); }); return; @@ -254,7 +258,7 @@ export class GameScene extends ResizableScene implements CenterListener { message: this.originalMapUrl ?? file.src }); }); - this.load.on('filecomplete-tilemapJSON-'+this.MapUrlFile, (key: string, type: string, data: unknown) => { + this.load.on('filecomplete-tilemapJSON-' + this.MapUrlFile, (key: string, type: string, data: unknown) => { this.onMapLoad(data); }); //TODO strategy to add access token @@ -266,7 +270,7 @@ export class GameScene extends ResizableScene implements CenterListener { this.onMapLoad(data); } - this.load.spritesheet('layout_modes', 'resources/objects/layout_modes.png', {frameWidth: 32, frameHeight: 32}); + this.load.spritesheet('layout_modes', 'resources/objects/layout_modes.png', { frameWidth: 32, frameHeight: 32 }); this.load.bitmapFont('main_font', 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml'); } @@ -292,7 +296,7 @@ export class GameScene extends ResizableScene implements CenterListener { for (const layer of this.mapFile.layers) { if (layer.type === 'objectgroup') { for (const object of layer.objects) { - let objectsOfType: ITiledMapObject[]|undefined; + let objectsOfType: ITiledMapObject[] | undefined; if (!objects.has(object.type)) { objectsOfType = new Array(); } else { @@ -320,7 +324,7 @@ export class GameScene extends ResizableScene implements CenterListener { } default: continue; - //throw new Error('Unsupported object type: "'+ itemType +'"'); + //throw new Error('Unsupported object type: "'+ itemType +'"'); } itemFactory.preload(this.load); @@ -355,7 +359,7 @@ export class GameScene extends ResizableScene implements CenterListener { } //hook initialisation - init(initData : GameSceneInitInterface) { + init(initData: GameSceneInitInterface) { if (initData.initPosition !== undefined) { this.initPosition = initData.initPosition; //todo: still used? } @@ -433,7 +437,7 @@ export class GameScene extends ResizableScene implements CenterListener { this.Objects = new Array(); //initialise list of other player - this.MapPlayers = this.physics.add.group({immovable: true}); + this.MapPlayers = this.physics.add.group({ immovable: true }); //create input to move @@ -522,7 +526,7 @@ export class GameScene extends ResizableScene implements CenterListener { bottom: camera.scrollY + camera.height, }, this.companion - ).then((onConnect: OnConnectInterface) => { + ).then((onConnect: OnConnectInterface) => { this.connection = onConnect.connection; this.connection.onUserJoins((message: MessageUserJoined) => { @@ -673,23 +677,23 @@ export class GameScene extends ResizableScene implements CenterListener { const contextRed = this.circleRedTexture.context; contextRed.beginPath(); contextRed.arc(48, 48, 48, 0, 2 * Math.PI, false); - //context.lineWidth = 5; + //context.lineWidth = 5; contextRed.strokeStyle = '#ff0000'; contextRed.stroke(); this.circleRedTexture.refresh(); } - private safeParseJSONstring(jsonString: string|undefined, propertyName: string) { + private safeParseJSONstring(jsonString: string | undefined, propertyName: string) { try { return jsonString ? JSON.parse(jsonString) : {}; - } catch(e) { + } catch (e) { console.warn('Invalid JSON found in property "' + propertyName + '" of the map:' + jsonString, e); return {} } } - private triggerOnMapLayerPropertyChange(){ + private triggerOnMapLayerPropertyChange() { this.gameMap.onPropertyChange('exitSceneUrl', (newValue, oldValue) => { if (newValue) this.onMapExit(newValue as string); }); @@ -700,22 +704,22 @@ export class GameScene extends ResizableScene implements CenterListener { if (newValue === undefined) { layoutManager.removeActionButton('openWebsite', this.userInputManager); coWebsiteManager.closeCoWebsite(); - }else{ + } else { const openWebsiteFunction = () => { coWebsiteManager.loadCoWebsite(newValue as string, this.MapUrlFile, allProps.get('openWebsiteAllowApi') as boolean | undefined, allProps.get('openWebsitePolicy') as string | undefined); layoutManager.removeActionButton('openWebsite', this.userInputManager); }; const openWebsiteTriggerValue = allProps.get(TRIGGER_WEBSITE_PROPERTIES); - if(openWebsiteTriggerValue && openWebsiteTriggerValue === ON_ACTION_TRIGGER_BUTTON) { + if (openWebsiteTriggerValue && openWebsiteTriggerValue === ON_ACTION_TRIGGER_BUTTON) { let message = allProps.get(WEBSITE_MESSAGE_PROPERTIES); - if(message === undefined){ + if (message === undefined) { message = 'Press SPACE or touch here to open web site'; } layoutManager.addActionButton('openWebsite', message.toString(), () => { openWebsiteFunction(); }, this.userInputManager); - }else{ + } else { openWebsiteFunction(); } } @@ -724,12 +728,12 @@ export class GameScene extends ResizableScene implements CenterListener { if (newValue === undefined) { layoutManager.removeActionButton('jitsiRoom', this.userInputManager); this.stopJitsi(); - }else{ + } else { const openJitsiRoomFunction = () => { const roomName = jitsiFactory.getRoomName(newValue.toString(), this.instance); - const jitsiUrl = allProps.get("jitsiUrl") as string|undefined; + const jitsiUrl = allProps.get("jitsiUrl") as string | undefined; if (JITSI_PRIVATE_MODE && !jitsiUrl) { - const adminTag = allProps.get("jitsiRoomAdminTag") as string|undefined; + const adminTag = allProps.get("jitsiRoomAdminTag") as string | undefined; this.connection?.emitQueryJitsiJwtMessage(roomName, adminTag); } else { @@ -739,7 +743,7 @@ export class GameScene extends ResizableScene implements CenterListener { } const jitsiTriggerValue = allProps.get(TRIGGER_JITSI_PROPERTIES); - if(jitsiTriggerValue && jitsiTriggerValue === ON_ACTION_TRIGGER_BUTTON) { + if (jitsiTriggerValue && jitsiTriggerValue === ON_ACTION_TRIGGER_BUTTON) { let message = allProps.get(JITSI_MESSAGE_PROPERTIES); if (message === undefined) { message = 'Press SPACE or touch here to enter Jitsi Meet room'; @@ -747,7 +751,7 @@ export class GameScene extends ResizableScene implements CenterListener { layoutManager.addActionButton('jitsiRoom', message.toString(), () => { openJitsiRoomFunction(); }, this.userInputManager); - }else{ + } else { openJitsiRoomFunction(); } } @@ -760,8 +764,8 @@ export class GameScene extends ResizableScene implements CenterListener { } }); 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; + 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 @@ -780,13 +784,13 @@ export class GameScene extends ResizableScene implements CenterListener { } private listenToIframeEvents(): void { - this.iframeSubscriptionList = []; - this.iframeSubscriptionList.push(iframeListener.openPopupStream.subscribe((openPopupEvent) => { + this.iframeSubscriptionList = []; + this.iframeSubscriptionList.push(iframeListener.openPopupStream.subscribe((openPopupEvent) => { - let objectLayerSquare : ITiledMapObject; + let objectLayerSquare: ITiledMapObject; const targetObjectData = this.getObjectLayerData(openPopupEvent.targetObject); - if (targetObjectData !== undefined){ - objectLayerSquare = targetObjectData; + if (targetObjectData !== undefined) { + objectLayerSquare = targetObjectData; } else { console.error("Error while opening a popup. Cannot find an object on the map with name '" + openPopupEvent.targetObject + "'. The first parameter of WA.openPopup() must be the name of a rectangle object in your map."); return; @@ -799,14 +803,14 @@ ${escapedMessage} html += buttonContainer; let id = 0; for (const button of openPopupEvent.buttons) { - html += ``; + html += ``; id++; } html += ''; - const domElement = this.add.dom(objectLayerSquare.x , + const domElement = this.add.dom(objectLayerSquare.x, objectLayerSquare.y).createFromHTML(html); - const container : HTMLDivElement = domElement.getChildByID("container") as HTMLDivElement; + const container: HTMLDivElement = domElement.getChildByID("container") as HTMLDivElement; container.style.width = objectLayerSquare.width + "px"; domElement.scale = 0; domElement.setClassName('popUpElement'); @@ -826,67 +830,99 @@ ${escapedMessage} id++; } this.tweens.add({ - targets : domElement , - scale : 1, - ease : "EaseOut", - duration : 400, + targets: domElement, + scale: 1, + ease: "EaseOut", + duration: 400, }); this.popUpElements.set(openPopupEvent.popupId, domElement); })); - this.iframeSubscriptionList.push(iframeListener.closePopupStream.subscribe((closePopupEvent) => { + this.iframeSubscriptionList.push(iframeListener.closePopupStream.subscribe((closePopupEvent) => { const popUpElement = this.popUpElements.get(closePopupEvent.popupId); if (popUpElement === undefined) { - console.error('Could not close popup with ID ', closePopupEvent.popupId,'. Maybe it has already been closed?'); + console.error('Could not close popup with ID ', closePopupEvent.popupId, '. Maybe it has already been closed?'); } this.tweens.add({ - targets : popUpElement , - scale : 0, - ease : "EaseOut", - duration : 400, - onComplete : () => { + targets: popUpElement, + scale: 0, + ease: "EaseOut", + duration: 400, + onComplete: () => { popUpElement?.destroy(); this.popUpElements.delete(closePopupEvent.popupId); }, }); })); - this.iframeSubscriptionList.push(iframeListener.disablePlayerControlStream.subscribe(()=>{ + this.iframeSubscriptionList.push(iframeListener.disablePlayerControlStream.subscribe(() => { this.userInputManager.disableControls(); })); - this.iframeSubscriptionList.push(iframeListener.enablePlayerControlStream.subscribe(()=>{ + this.iframeSubscriptionList.push(iframeListener.enablePlayerControlStream.subscribe(() => { this.userInputManager.restoreControls(); })); - this.iframeSubscriptionList.push(iframeListener.loadPageStream.subscribe((url:string)=>{ - this.loadNextGame(url).then(()=>{ - this.events.once(EVENT_TYPE.POST_UPDATE,()=>{ + this.iframeSubscriptionList.push(iframeListener.loadPageStream.subscribe((url: string) => { + this.loadNextGame(url).then(() => { + this.events.once(EVENT_TYPE.POST_UPDATE, () => { this.onMapExit(url); }) }) })); - let scriptedBubbleSprite : Sprite; - this.iframeSubscriptionList.push(iframeListener.displayBubbleStream.subscribe(()=>{ - scriptedBubbleSprite = new Sprite(this,this.CurrentPlayer.x + 25,this.CurrentPlayer.y,'circleSprite-white'); + + this.iframeSubscriptionList.push(iframeListener.updateTileEvent.subscribe(event => { + const layer = this.Layers.find(layer => layer.layer.name == event.layer) + if (layer) { + const tile = layer.getTileAt(event.x, event.y) + if (typeof event.tile == "string") { + const tileIndex = this.getIndexForTileType(event.tile); + if (tileIndex) { + tile.index = tileIndex + } else { + return + } + } else { + tile.index = event.tile + } + this.scene.scene.sys.game.events.emit("contextrestored") + } + })) + + let scriptedBubbleSprite: Sprite; + this.iframeSubscriptionList.push(iframeListener.displayBubbleStream.subscribe(() => { + scriptedBubbleSprite = new Sprite(this, this.CurrentPlayer.x + 25, this.CurrentPlayer.y, 'circleSprite-white'); scriptedBubbleSprite.setDisplayOrigin(48, 48); this.add.existing(scriptedBubbleSprite); })); - this.iframeSubscriptionList.push(iframeListener.removeBubbleStream.subscribe(()=>{ + this.iframeSubscriptionList.push(iframeListener.removeBubbleStream.subscribe(() => { scriptedBubbleSprite.destroy(); })); } + private getIndexForTileType(tileType: string): number | undefined { + for (const tileset of this.mapFile.tilesets) { + if (tileset.tiles) { + for (const tilesetTile of tileset.tiles) { + if (tilesetTile.type == tileType) { + return tileset.firstgid + tilesetTile.id + } + } + } + } + return undefined + } + private getMapDirUrl(): string { return this.MapUrlFile.substr(0, this.MapUrlFile.lastIndexOf('/')); } private onMapExit(exitKey: string) { - const {roomId, hash} = Room.getIdFromIdentifier(exitKey, this.MapUrlFile, this.instance); - if (!roomId) throw new Error('Could not find the room from its exit key: '+exitKey); + const { roomId, hash } = Room.getIdFromIdentifier(exitKey, this.MapUrlFile, this.instance); + if (!roomId) throw new Error('Could not find the room from its exit key: ' + exitKey); urlManager.pushStartLayerNameToUrl(hash); if (roomId !== this.scene.key) { if (this.scene.get(roomId) === null) { @@ -922,7 +958,7 @@ ${escapedMessage} this.simplePeer?.unregister(); this.messageSubscription?.unsubscribe(); - for(const iframeEvents of this.iframeSubscriptionList){ + for (const iframeEvents of this.iframeSubscriptionList) { iframeEvents.unsubscribe(); } } @@ -942,7 +978,7 @@ ${escapedMessage} private switchLayoutMode(): void { //if discussion is activated, this layout cannot be activated - if(mediaManager.activatedDiscussion){ + if (mediaManager.activatedDiscussion) { return; } const mode = layoutManager.getLayoutMode(); @@ -983,24 +1019,24 @@ ${escapedMessage} private initPositionFromLayerName(layerName: string) { for (const layer of this.gameMap.layersIterator) { - if ((layerName === layer.name || layer.name.endsWith('/'+layerName)) && layer.type === 'tilelayer' && (layerName === defaultStartLayerName || this.isStartLayer(layer))) { + if ((layerName === layer.name || layer.name.endsWith('/' + layerName)) && layer.type === 'tilelayer' && (layerName === defaultStartLayerName || this.isStartLayer(layer))) { const startPosition = this.startUser(layer); - this.startX = startPosition.x + this.mapFile.tilewidth/2; - this.startY = startPosition.y + this.mapFile.tileheight/2; + this.startX = startPosition.x + this.mapFile.tilewidth / 2; + this.startY = startPosition.y + this.mapFile.tileheight / 2; } } } - private getExitUrl(layer: ITiledMapLayer): string|undefined { - return this.getProperty(layer, "exitUrl") as string|undefined; + private getExitUrl(layer: ITiledMapLayer): string | undefined { + return this.getProperty(layer, "exitUrl") as string | undefined; } /** * @deprecated the map property exitSceneUrl is deprecated */ - private getExitSceneUrl(layer: ITiledMapLayer): string|undefined { - return this.getProperty(layer, "exitSceneUrl") as string|undefined; + private getExitSceneUrl(layer: ITiledMapLayer): string | undefined { + return this.getProperty(layer, "exitSceneUrl") as string | undefined; } private isStartLayer(layer: ITiledMapLayer): boolean { @@ -1011,8 +1047,8 @@ ${escapedMessage} 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[]|undefined = layer.properties; + private getProperty(layer: ITiledMapLayer | ITiledMap, name: string): string | boolean | number | undefined { + const properties: ITiledMapLayerProperty[] | undefined = layer.properties; if (!properties) { return undefined; } @@ -1023,8 +1059,8 @@ ${escapedMessage} return obj.value; } - private getProperties(layer: ITiledMapLayer|ITiledMap, name: string): (string|number|boolean|undefined)[] { - const properties: ITiledMapLayerProperty[]|undefined = layer.properties; + private getProperties(layer: ITiledMapLayer | ITiledMap, name: string): (string | number | boolean | undefined)[] { + const properties: ITiledMapLayerProperty[] | undefined = layer.properties; if (!properties) { return []; } @@ -1032,30 +1068,30 @@ ${escapedMessage} } //todo: push that into the gameManager - private async loadNextGame(exitSceneIdentifier: string){ - const {roomId, hash} = Room.getIdFromIdentifier(exitSceneIdentifier, this.MapUrlFile, this.instance); + private async loadNextGame(exitSceneIdentifier: string) { + const { roomId, hash } = Room.getIdFromIdentifier(exitSceneIdentifier, this.MapUrlFile, this.instance); const room = new Room(roomId); await gameManager.loadMap(room, this.scene); } private startUser(layer: ITiledMapTileLayer): PositionInterface { const tiles = layer.data; - if (typeof(tiles) === 'string') { + if (typeof (tiles) === 'string') { throw new Error('The content of a JSON map must be filled as a JSON array, not as a string'); } - const possibleStartPositions : PositionInterface[] = []; - tiles.forEach((objectKey : number, key: number) => { - if(objectKey === 0){ + const possibleStartPositions: PositionInterface[] = []; + tiles.forEach((objectKey: number, key: number) => { + if (objectKey === 0) { return; } const y = Math.floor(key / layer.width); const x = key % layer.width; - possibleStartPositions.push({x: x * this.mapFile.tilewidth, y: y * this.mapFile.tilewidth}); + possibleStartPositions.push({ x: x * this.mapFile.tilewidth, y: y * this.mapFile.tilewidth }); }); // Get a value at random amongst allowed values if (possibleStartPositions.length === 0) { - console.warn('The start layer "'+layer.name+'" for this map is empty.'); + console.warn('The start layer "' + layer.name + '" for this map is empty.'); return { x: 0, y: 0 @@ -1067,12 +1103,12 @@ ${escapedMessage} //todo: in a dedicated class/function? initCamera() { - this.cameras.main.setBounds(0,0, this.Map.widthInPixels, this.Map.heightInPixels); + this.cameras.main.setBounds(0, 0, this.Map.widthInPixels, this.Map.heightInPixels); this.updateCameraOffset(); this.cameras.main.setZoom(ZOOM_LEVEL); } - addLayer(Layer : Phaser.Tilemaps.StaticTilemapLayer){ + addLayer(Layer: Phaser.Tilemaps.StaticTilemapLayer) { this.Layers.push(Layer); } @@ -1082,7 +1118,7 @@ ${escapedMessage} this.physics.add.collider(this.CurrentPlayer, Layer, (object1: GameObject, object2: GameObject) => { //this.CurrentPlayer.say("Collision with layer : "+ (object2 as Tile).layer.name) }); - Layer.setCollisionByProperty({collides: true}); + Layer.setCollisionByProperty({ collides: true }); if (DEBUG_MODE) { //debug code to see the collision hitbox of the object in the top layer Layer.renderDebug(this.add.graphics(), { @@ -1094,7 +1130,7 @@ ${escapedMessage} }); } - createCurrentPlayer(){ + createCurrentPlayer() { //TODO create animation moving between exit and start const texturesPromise = lazyLoadPlayerCharacterTextures(this.load, this.characterLayers); try { @@ -1110,8 +1146,8 @@ ${escapedMessage} this.companion, this.companion !== null ? lazyLoadCompanionResource(this.load, this.companion) : undefined ); - }catch (err){ - if(err instanceof TextureError) { + } catch (err) { + if (err instanceof TextureError) { gameManager.leaveGame(this, SelectCharacterSceneName, new SelectCharacterScene()); } throw err; @@ -1172,7 +1208,7 @@ ${escapedMessage} } let shortestDistance: number = Infinity; - let selectedItem: ActionableItem|null = null; + let selectedItem: ActionableItem | null = null; for (const item of this.actionableItems.values()) { const distance = item.actionableDistance(x, y); if (distance !== null && distance < shortestDistance) { @@ -1206,7 +1242,7 @@ ${escapedMessage} * @param time * @param delta The delta time in ms since the last frame. This is a smoothed and capped value based on the FPS rate. */ - update(time: number, delta: number) : void { + update(time: number, delta: number): void { mediaManager.setLastUpdateScene(); this.currentTick = time; this.CurrentPlayer.moveUser(delta); @@ -1263,8 +1299,8 @@ ${escapedMessage} const currentPlayerId = this.connection?.getUserId(); this.removeAllRemotePlayers(); // load map - usersPosition.forEach((userPosition : MessageUserPositionInterface) => { - if(userPosition.userId === currentPlayerId){ + usersPosition.forEach((userPosition: MessageUserPositionInterface) => { + if (userPosition.userId === currentPlayerId) { return; } this.addPlayer(userPosition); @@ -1274,16 +1310,16 @@ ${escapedMessage} /** * Called by the connexion when a new player arrives on a map */ - public addPlayer(addPlayerData : AddPlayerInterface) : void { + public addPlayer(addPlayerData: AddPlayerInterface): void { this.pendingEvents.enqueue({ type: "AddPlayerEvent", event: addPlayerData }); } - private doAddPlayer(addPlayerData : AddPlayerInterface): void { + private doAddPlayer(addPlayerData: AddPlayerInterface): void { //check if exist player, if exist, move position - if(this.MapPlayersByKey.has(addPlayerData.userId)){ + if (this.MapPlayersByKey.has(addPlayerData.userId)) { this.updatePlayerPosition({ userId: addPlayerData.userId, position: addPlayerData.position @@ -1344,10 +1380,10 @@ ${escapedMessage} } private doUpdatePlayerPosition(message: MessageUserMovedInterface): void { - const player : RemotePlayer | undefined = this.MapPlayersByKey.get(message.userId); + const player: RemotePlayer | undefined = this.MapPlayersByKey.get(message.userId); if (player === undefined) { //throw new Error('Cannot find player with ID "' + message.userId +'"'); - console.error('Cannot update position of player with ID "' + message.userId +'": player not found'); + console.error('Cannot update position of player with ID "' + message.userId + '": player not found'); return; } @@ -1391,7 +1427,7 @@ ${escapedMessage} doDeleteGroup(groupId: number): void { const group = this.groups.get(groupId); - if(!group){ + if (!group) { return; } group.destroy(); @@ -1419,7 +1455,7 @@ ${escapedMessage} bottom: camera.scrollY + camera.height, }); } - private getObjectLayerData(objectName : string) : ITiledMapObject| undefined{ + private getObjectLayerData(objectName: string): ITiledMapObject | undefined { for (const layer of this.mapFile.layers) { if (layer.type === 'objectgroup' && layer.name === 'floorLayer') { for (const object of layer.objects) { @@ -1454,7 +1490,7 @@ ${escapedMessage} xCenter /= ZOOM_LEVEL * RESOLUTION; yCenter /= ZOOM_LEVEL * RESOLUTION; - this.cameras.main.startFollow(this.CurrentPlayer, true, 1, 1, xCenter - this.game.renderer.width / 2, yCenter - this.game.renderer.height / 2); + this.cameras.main.startFollow(this.CurrentPlayer, true, 1, 1, xCenter - this.game.renderer.width / 2, yCenter - this.game.renderer.height / 2); } public onCenterChange(): void { @@ -1463,16 +1499,16 @@ ${escapedMessage} public startJitsi(roomName: string, jwt?: string): void { 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; + 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, jitsiUrl); this.connection?.setSilent(true); mediaManager.hideGameOverlay(); //permit to stop jitsi when user close iframe - mediaManager.addTriggerCloseJitsiFrameButton('close-jisi',() => { + mediaManager.addTriggerCloseJitsiFrameButton('close-jisi', () => { this.stopJitsi(); }); } @@ -1486,7 +1522,7 @@ ${escapedMessage} } //todo: put this into an 'orchestrator' scene (EntryScene?) - private bannedUser(){ + private bannedUser() { this.cleanupClosingScene(); this.userInputManager.disableControls(); this.scene.start(ErrorSceneName, { diff --git a/front/src/Phaser/Map/ITiledMap.ts b/front/src/Phaser/Map/ITiledMap.ts index c4828911..27fe9f45 100644 --- a/front/src/Phaser/Map/ITiledMap.ts +++ b/front/src/Phaser/Map/ITiledMap.ts @@ -34,7 +34,7 @@ export interface ITiledMap { export interface ITiledMapLayerProperty { name: string; type: string; - value: string|boolean|number|undefined; + value: string | boolean | number | undefined; } /*export interface ITiledMapLayerBooleanProperty { @@ -63,7 +63,7 @@ export interface ITiledMapGroupLayer { export interface ITiledMapTileLayer { id?: number, - data: number[]|string; + data: number[] | string; height: number; name: string; opacity: number; @@ -114,7 +114,7 @@ export interface ITiledMapObject { gid: number; height: number; name: string; - properties: {[key: string]: string}; + properties: { [key: string]: string }; rotation: number; type: string; visible: boolean; @@ -130,12 +130,12 @@ export interface ITiledMapObject { /** * Polygon points */ - polygon: {x: number, y: number}[]; + polygon: { x: number, y: number }[]; /** * Polyline points */ - polyline: {x: number, y: number}[]; + polyline: { x: number, y: number }[]; text?: ITiledText } @@ -149,7 +149,7 @@ export interface ITiledText { underline?: boolean, italic?: boolean, strikeout?: boolean, - halign?: "center"|"right"|"justify"|"left" + halign?: "center" | "right" | "justify" | "left" } export interface ITiledTileSet { @@ -160,14 +160,14 @@ export interface ITiledTileSet { imagewidth: number; margin: number; name: string; - properties: {[key: string]: string}; + properties: { [key: string]: string }; spacing: number; tilecount: number; tileheight: number; tilewidth: number; transparentcolor: string; terrains: ITiledMapTerrain[]; - tiles: {[key: string]: { terrain: number[] }}; + tiles: Array; /** * Refers to external tileset file (should be JSON) @@ -175,6 +175,11 @@ export interface ITiledTileSet { source: string; } +export interface ITile { + id: number, + type?: string +} + export interface ITiledMapTerrain { name: string; tile: number; From bed45a831031f99c2af5d5fb7f11a4c773814dca Mon Sep 17 00:00:00 2001 From: jonny Date: Mon, 10 May 2021 00:31:54 +0200 Subject: [PATCH 08/46] cherry pick conflicts --- front/src/Api/IframeListener.ts | 59 +++++++++++++++--------------- front/src/Phaser/Game/GameScene.ts | 5 --- 2 files changed, 29 insertions(+), 35 deletions(-) diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index 715eddc0..f97e80ae 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -1,18 +1,19 @@ -import {Subject} from "rxjs"; -import {ChatEvent, isChatEvent} from "./Events/ChatEvent"; -import {IframeEvent, isIframeEventWrapper} from "./Events/IframeEvent"; -import {UserInputChatEvent} from "./Events/UserInputChatEvent"; +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"; -import {isOpenTabEvent, OpenTabEvent} from "./Events/OpenTabEvent"; -import {ButtonClickedEvent} from "./Events/ButtonClickedEvent"; -import {ClosePopupEvent, isClosePopupEvent} from "./Events/ClosePopupEvent"; -import {scriptUtils} from "./ScriptUtils"; -import {GoToPageEvent, isGoToPageEvent} from "./Events/GoToPageEvent"; -import {isOpenCoWebsite, OpenCoWebSiteEvent} from "./Events/OpenCoWebSiteEvent"; +import { HtmlUtils } from "../WebRtc/HtmlUtils"; +import { EnterLeaveEvent } from "./Events/EnterLeaveEvent"; +import { isOpenPopupEvent, OpenPopupEvent } from "./Events/OpenPopupEvent"; +import { isOpenTabEvent, OpenTabEvent } from "./Events/OpenTabEvent"; +import { ButtonClickedEvent } from "./Events/ButtonClickedEvent"; +import { ClosePopupEvent, isClosePopupEvent } from "./Events/ClosePopupEvent"; +import { scriptUtils } from "./ScriptUtils"; +import { GoToPageEvent, isGoToPageEvent } from "./Events/GoToPageEvent"; +import { isOpenCoWebsite, OpenCoWebSiteEvent } from "./Events/OpenCoWebSiteEvent"; import { isLoadPageEvent } from './Events/LoadPageEvent'; +import { isUpdateTileEvent, UpdateTileEvent } from './Events/ApiUpdateTileEvent'; /** @@ -32,7 +33,7 @@ class IframeListener { private readonly _goToPageStream: Subject = new Subject(); public readonly goToPageStream = this._goToPageStream.asObservable(); - + private readonly _loadPageStream: Subject = new Subject(); public readonly loadPageStream = this._loadPageStream.asObservable(); @@ -88,33 +89,31 @@ class IframeListener { } else if (payload.type === 'closePopup' && isClosePopupEvent(payload.data)) { this._closePopupStream.next(payload.data); } - else if(payload.type === 'openTab' && isOpenTabEvent(payload.data)) { + else if (payload.type === 'openTab' && isOpenTabEvent(payload.data)) { scriptUtils.openTab(payload.data.url); } - else if(payload.type === 'goToPage' && isGoToPageEvent(payload.data)) { + else if (payload.type === 'goToPage' && isGoToPageEvent(payload.data)) { scriptUtils.goToPage(payload.data.url); } - else if(payload.type === 'openCoWebSite' && isOpenCoWebsite(payload.data)) { + else if (payload.type === 'openCoWebSite' && isOpenCoWebsite(payload.data)) { scriptUtils.openCoWebsite(payload.data.url); } - else if(payload.type === 'closeCoWebSite') { + else if (payload.type === 'closeCoWebSite') { scriptUtils.closeCoWebSite(); } - else if (payload.type === 'disablePlayerControl'){ + else if (payload.type === 'disablePlayerControl') { this._disablePlayerControlStream.next(); } - else if (payload.type === 'restorePlayerControl'){ + else if (payload.type === 'restorePlayerControl') { this._enablePlayerControlStream.next(); } - else if (payload.type === 'displayBubble'){ + else if (payload.type === 'displayBubble') { this._displayBubbleStream.next(); } - else if (payload.type === 'removeBubble'){ + else if (payload.type === 'removeBubble') { this._removeBubbleStream.next(); - }else if (payload.type === 'loadPage' && isLoadPageEvent(payload.data)){ + } else if (payload.type === 'loadPage' && isLoadPageEvent(payload.data)) { this._loadPageStream.next(payload.data.url); - } else if (payload.type == "getState") { - this._gameStateStream.next(); } else if (payload.type == "updateTile" && isUpdateTileEvent(payload.data)) { this._updateTileEvent.next(payload.data) } @@ -144,7 +143,7 @@ class IframeListener { const iframe = document.createElement('iframe'); iframe.id = this.getIFrameId(scriptUrl); iframe.style.display = 'none'; - iframe.src = '/iframe.html?script='+encodeURIComponent(scriptUrl); + 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'); @@ -168,8 +167,8 @@ class IframeListener { '\n' + '\n' + '\n' + - '\n' + - '\n' + + '\n' + + '\n' + '\n' + '\n'; @@ -186,14 +185,14 @@ class IframeListener { } private getIFrameId(scriptUrl: string): string { - return 'script'+crypto.createHash('md5').update(scriptUrl).digest("hex"); + 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+'"'); + throw new Error('Unknown iframe for script "' + scriptUrl + '"'); } this.unregisterIframe(iframe); iframe.remove(); diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 138ca5ae..9ed86716 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -86,11 +86,6 @@ import EVENT_TYPE = Phaser.Scenes.Events import { Subscription } from "rxjs"; import { worldFullMessageStream } from "../../Connexion/WorldFullMessageStream"; import { lazyLoadCompanionResource } from "../Companion/CompanionTexturesLoadingManager"; -import {TextUtils} from "../Components/TextUtils"; -import {LayersIterator} from "../Map/LayersIterator"; -import {touchScreenManager} from "../../Touch/TouchScreenManager"; -import {PinchManager} from "../UserInput/PinchManager"; -import {joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey} from "../Components/MobileJoystick"; import { TextUtils } from "../Components/TextUtils"; import { touchScreenManager } from "../../Touch/TouchScreenManager"; import { PinchManager } from "../UserInput/PinchManager"; From 8db72d2dfd4e707fa07e982d4afb549bc286303c Mon Sep 17 00:00:00 2001 From: jonny Date: Mon, 10 May 2021 01:21:37 +0200 Subject: [PATCH 09/46] refactored to Array of tile --- front/src/Api/Events/ApiUpdateTileEvent.ts | 5 +++-- front/src/Phaser/Game/GameScene.ts | 26 ++++++++++++---------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/front/src/Api/Events/ApiUpdateTileEvent.ts b/front/src/Api/Events/ApiUpdateTileEvent.ts index 8a53fbe5..094596a4 100644 --- a/front/src/Api/Events/ApiUpdateTileEvent.ts +++ b/front/src/Api/Events/ApiUpdateTileEvent.ts @@ -3,13 +3,14 @@ import * as tg from "generic-type-guard"; export const updateTile = "updateTile" -export const isUpdateTileEvent = +export const isUpdateTileEvent = tg.isArray( new tg.IsInterface().withProperties({ x: tg.isNumber, y: tg.isNumber, tile: tg.isUnion(tg.isNumber, tg.isString), layer: tg.isUnion(tg.isNumber, tg.isString) - }).get(); + }).get() +); /** * A message sent from the game to the iFrame when a user enters or leaves a zone marked with the "zone" property. */ diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 9ed86716..5687c7e5 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -868,18 +868,20 @@ ${escapedMessage} })); this.iframeSubscriptionList.push(iframeListener.updateTileEvent.subscribe(event => { - const layer = this.Layers.find(layer => layer.layer.name == event.layer) - if (layer) { - const tile = layer.getTileAt(event.x, event.y) - if (typeof event.tile == "string") { - const tileIndex = this.getIndexForTileType(event.tile); - if (tileIndex) { - tile.index = tileIndex + for (const eventTile of event) { + const layer = this.Layers.find(layer => layer.layer.name == eventTile.layer) + if (layer) { + const tile = layer.getTileAt(eventTile.x, eventTile.y) + if (typeof eventTile.tile == "string") { + const tileIndex = this.getIndexForTileType(eventTile.tile); + if (tileIndex) { + tile.index = tileIndex + } else { + return + } } else { - return + tile.index = eventTile.tile } - } else { - tile.index = event.tile } this.scene.scene.sys.game.events.emit("contextrestored") } @@ -898,7 +900,7 @@ ${escapedMessage} } - private getIndexForTileType(tileType: string): number | undefined { + private getIndexForTileType(tileType: string): number | null { for (const tileset of this.mapFile.tilesets) { if (tileset.tiles) { for (const tilesetTile of tileset.tiles) { @@ -908,7 +910,7 @@ ${escapedMessage} } } } - return undefined + return null } private getMapDirUrl(): string { From 46996f70497666bf79f5f3dde624252634eb3ae9 Mon Sep 17 00:00:00 2001 From: jonny Date: Mon, 10 May 2021 01:27:17 +0200 Subject: [PATCH 10/46] moved event trigger out of index array --- front/src/Phaser/Game/GameScene.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 5687c7e5..674087e0 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -883,8 +883,8 @@ ${escapedMessage} tile.index = eventTile.tile } } - this.scene.scene.sys.game.events.emit("contextrestored") } + this.scene.scene.sys.game.events.emit("contextrestored") })) let scriptedBubbleSprite: Sprite; From a6ba8d41b9a9c7d73cec0452b313c34bfd9e38b4 Mon Sep 17 00:00:00 2001 From: GRL Date: Mon, 10 May 2021 11:19:18 +0200 Subject: [PATCH 11/46] implement show/hide layer with scripting --- front/src/Api/Events/LayerEvent.ts | 10 ++++++++++ front/src/Api/IframeListener.ts | 15 ++++++++++++++- front/src/iframe_api.ts | 21 ++++++++++++++++++++ maps/tests/iframe.html | 31 +++++++++++++++++++++++++++++- maps/tests/iframe_api.json | 25 +++++++++++++++++++++--- 5 files changed, 97 insertions(+), 5 deletions(-) create mode 100644 front/src/Api/Events/LayerEvent.ts diff --git a/front/src/Api/Events/LayerEvent.ts b/front/src/Api/Events/LayerEvent.ts new file mode 100644 index 00000000..f854248b --- /dev/null +++ b/front/src/Api/Events/LayerEvent.ts @@ -0,0 +1,10 @@ +import * as tg from "generic-type-guard"; + +export const isLayerEvent = + new tg.IsInterface().withProperties({ + name: tg.isString, + }).get(); +/** + * A message sent from the iFrame to the game to show/hide a layer. + */ +export type LayerEvent = tg.GuardedType; diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index 7e51a281..0820785a 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -12,6 +12,7 @@ import {ClosePopupEvent, isClosePopupEvent} from "./Events/ClosePopupEvent"; import {scriptUtils} from "./ScriptUtils"; import {GoToPageEvent, isGoToPageEvent} from "./Events/GoToPageEvent"; import {isOpenCoWebsite, OpenCoWebSiteEvent} from "./Events/OpenCoWebSiteEvent"; +import {isLayerEvent, LayerEvent} from "./Events/LayerEvent"; /** @@ -52,6 +53,12 @@ class IframeListener { private readonly _removeBubbleStream: Subject = new Subject(); public readonly removeBubbleStream = this._removeBubbleStream.asObservable(); + private readonly _showLayerStream: Subject = new Subject(); + public readonly showLayerStream = this._showLayerStream.asObservable(); + + private readonly _hideLayerStream: Subject = new Subject(); + public readonly hideLayerStream = this._hideLayerStream.asObservable(); + private readonly iframes = new Set(); private readonly scripts = new Map(); @@ -73,7 +80,13 @@ class IframeListener { const payload = message.data; if (isIframeEventWrapper(payload)) { - if (payload.type === 'chat' && isChatEvent(payload.data)) { + if (payload.type ==='showLayer' && isLayerEvent(payload.data)) { + console.log('showLayer 2'); + this._showLayerStream.next(payload.data); + } else if (payload.type === 'hideLayer' && isLayerEvent(payload.data)) { + console.log('hideLayer 2'); + this._hideLayerStream.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); diff --git a/front/src/iframe_api.ts b/front/src/iframe_api.ts index 18d8d172..0b9fac46 100644 --- a/front/src/iframe_api.ts +++ b/front/src/iframe_api.ts @@ -9,6 +9,7 @@ import {ClosePopupEvent} from "./Api/Events/ClosePopupEvent"; import {OpenTabEvent} from "./Api/Events/OpenTabEvent"; import {GoToPageEvent} from "./Api/Events/GoToPageEvent"; import {OpenCoWebSiteEvent} from "./Api/Events/OpenCoWebSiteEvent"; +import {LayerEvent} from "./Api/Events/LayerEvent"; interface WorkAdventureApi { sendChatMessage(message: string, author: string): void; @@ -24,6 +25,8 @@ interface WorkAdventureApi { restorePlayerControl() : void; displayBubble() : void; removeBubble() : void; + showLayer(layer: string) : void; + hideLayer(layer: string) : void; } declare global { @@ -88,6 +91,24 @@ window.WA = { } as ChatEvent }, '*'); }, + showLayer(layer: string) : void { + console.log('showLayer'); + window.parent.postMessage({ + 'type' : 'showLayer', + 'data' : { + 'name' : layer + } as LayerEvent + }, '*'); + }, + hideLayer(layer: string) : void { + console.log('hideLayer'); + window.parent.postMessage({ + 'type' : 'hideLayer', + 'data' : { + 'name' : layer + } as LayerEvent + }, '*'); + }, disablePlayerControl() : void { window.parent.postMessage({'type' : 'disablePlayerControl'},'*'); }, diff --git a/maps/tests/iframe.html b/maps/tests/iframe.html index 23bfb479..4c7cd044 100644 --- a/maps/tests/iframe.html +++ b/maps/tests/iframe.html @@ -3,7 +3,7 @@ @@ -21,5 +21,34 @@ document.getElementById('chatSent').append(chatDiv); })); +
+ +
+ + diff --git a/maps/tests/iframe_api.json b/maps/tests/iframe_api.json index fa138500..db840b3f 100644 --- a/maps/tests/iframe_api.json +++ b/maps/tests/iframe_api.json @@ -1,4 +1,11 @@ { "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, "height":10, "infinite":false, "layers":[ @@ -49,6 +56,18 @@ "x":0, "y":0 }, + { + "data":[0, 0, 93, 0, 104, 0, 0, 0, 0, 0, 0, 0, 104, 0, 115, 0, 0, 0, 93, 0, 0, 0, 115, 0, 0, 0, 93, 0, 104, 0, 0, 0, 0, 0, 0, 0, 104, 0, 115, 93, 0, 0, 0, 0, 0, 0, 115, 0, 0, 104, 0, 0, 0, 0, 0, 0, 0, 0, 0, 115, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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":"Metadata", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, { "draworder":"topdown", "id":3, @@ -78,11 +97,11 @@ "x":0, "y":0 }], - "nextlayerid":6, + "nextlayerid":7, "nextobjectid":3, "orientation":"orthogonal", "renderorder":"right-down", - "tiledversion":"2021.03.23", + "tiledversion":"1.4.3", "tileheight":32, "tilesets":[ { @@ -100,6 +119,6 @@ }], "tilewidth":32, "type":"map", - "version":1.5, + "version":1.4, "width":10 } \ No newline at end of file From 841bf29764305e1fbdccf15eebb85aab5a9237fe Mon Sep 17 00:00:00 2001 From: GRL Date: Mon, 10 May 2021 11:20:07 +0200 Subject: [PATCH 12/46] auto update show/hide layer --- front/src/Phaser/Game/DirtyScene.ts | 1 + front/src/Phaser/Game/GameScene.ts | 34 ++++++++++++++++++++++++----- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/front/src/Phaser/Game/DirtyScene.ts b/front/src/Phaser/Game/DirtyScene.ts index 03ec9a95..e88e11f6 100644 --- a/front/src/Phaser/Game/DirtyScene.ts +++ b/front/src/Phaser/Game/DirtyScene.ts @@ -35,6 +35,7 @@ export abstract class DirtyScene extends ResizableScene { this.events.on(Events.RENDER, () => { this.objectListChanged = false; + this.dirty = false; }); } diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 65129787..6939721e 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -91,6 +91,7 @@ import {touchScreenManager} from "../../Touch/TouchScreenManager"; import {PinchManager} from "../UserInput/PinchManager"; import {joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey} from "../Components/MobileJoystick"; import {waScaleManager} from "../Services/WaScaleManager"; +import {LayerEvent} from "../../Api/Events/LayerEvent"; export interface GameSceneInitInterface { initPosition: PointInterface|null, @@ -839,7 +840,7 @@ ${escapedMessage} this.popUpElements.set(openPopupEvent.popupId, domElement); })); - this.iframeSubscriptionList.push(iframeListener.closePopupStream.subscribe((closePopupEvent) => { + this.iframeSubscriptionList.push(iframeListener.closePopupStream.subscribe((closePopupEvent) => { const popUpElement = this.popUpElements.get(closePopupEvent.popupId); if (popUpElement === undefined) { console.error('Could not close popup with ID ', closePopupEvent.popupId,'. Maybe it has already been closed?'); @@ -857,26 +858,48 @@ ${escapedMessage} }); })); - this.iframeSubscriptionList.push(iframeListener.disablePlayerControlStream.subscribe(()=>{ + this.iframeSubscriptionList.push(iframeListener.disablePlayerControlStream.subscribe(()=>{ this.userInputManager.disableControls(); })); - this.iframeSubscriptionList.push(iframeListener.enablePlayerControlStream.subscribe(()=>{ + this.iframeSubscriptionList.push(iframeListener.enablePlayerControlStream.subscribe(()=>{ this.userInputManager.restoreControls(); })); let scriptedBubbleSprite : Sprite; - this.iframeSubscriptionList.push(iframeListener.displayBubbleStream.subscribe(()=>{ + this.iframeSubscriptionList.push(iframeListener.displayBubbleStream.subscribe(()=>{ scriptedBubbleSprite = new Sprite(this,this.CurrentPlayer.x + 25,this.CurrentPlayer.y,'circleSprite-white'); scriptedBubbleSprite.setDisplayOrigin(48, 48); this.add.existing(scriptedBubbleSprite); })); - this.iframeSubscriptionList.push(iframeListener.removeBubbleStream.subscribe(()=>{ + this.iframeSubscriptionList.push(iframeListener.removeBubbleStream.subscribe(()=>{ scriptedBubbleSprite.destroy(); })); + this.iframeSubscriptionList.push(iframeListener.showLayerStream.subscribe((layerEvent)=>{ + console.log('showLayer 3'); + this.setLayerVisibility(layerEvent.name, true); + })); + + this.iframeSubscriptionList.push(iframeListener.hideLayerStream.subscribe((layerEvent)=>{ + console.log('hideLayer 3'); + this.setLayerVisibility(layerEvent.name, false); + })); + } + private setLayerVisibility(layerName: string, visible: boolean): void { + console.log('visibility'); + const layer = this.Layers.find((layer) => layer.layer.name === layerName); + if (layer === undefined) { + console.warn('Could not find layer "' + layerName + '" when calling WA.hideLayer / WA.showLayer'); + return; + } + layer.setVisible(visible); + this.dirty = true; + } + + private getMapDirUrl(): string { return this.MapUrlFile.substr(0, this.MapUrlFile.lastIndexOf('/')); } @@ -1207,7 +1230,6 @@ ${escapedMessage} * @param delta The delta time in ms since the last frame. This is a smoothed and capped value based on the FPS rate. */ update(time: number, delta: number) : void { - this.dirty = false; mediaManager.updateScene(); this.currentTick = time; if (this.CurrentPlayer.isMoving()) { From 8edd29abaab1c1d671e8cc9cca3bb465e2aec43d Mon Sep 17 00:00:00 2001 From: GRL Date: Mon, 10 May 2021 14:43:00 +0200 Subject: [PATCH 13/46] suppression console.log --- maps/tests/iframe.html | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/maps/tests/iframe.html b/maps/tests/iframe.html index 135096f8..116bbfd9 100644 --- a/maps/tests/iframe.html +++ b/maps/tests/iframe.html @@ -2,9 +2,6 @@ - @@ -22,17 +19,15 @@ }));
- +
From 973b3405ef3ff54809e110f6a6c09fc2e54ed9fe Mon Sep 17 00:00:00 2001 From: GRL Date: Mon, 10 May 2021 15:10:11 +0200 Subject: [PATCH 14/46] documentation of show/hide layer --- docs/maps/api-reference.md | 29 +++++++++++++++++++++++++++++ maps/tests/iframe.html | 10 +++++----- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/docs/maps/api-reference.md b/docs/maps/api-reference.md index 9891a88a..3a893474 100644 --- a/docs/maps/api-reference.md +++ b/docs/maps/api-reference.md @@ -235,3 +235,32 @@ mySound.play(config); // ... mySound.stop(); ``` + +### Show / Hide a layer + +``` +WA.showLayer(layerName : string): void +WA.hideLayer(layerName : string) : void +``` +These 2 methods can be used to show and hide a layer. + +Example : + +```javascript +
+ + +
+ +``` + + diff --git a/maps/tests/iframe.html b/maps/tests/iframe.html index 116bbfd9..c5c30972 100644 --- a/maps/tests/iframe.html +++ b/maps/tests/iframe.html @@ -19,15 +19,15 @@ }));
- +
From cf811c547b615ac9bbca9be19aa84e2aafe5a5f0 Mon Sep 17 00:00:00 2001 From: GRL Date: Mon, 10 May 2021 17:29:50 +0200 Subject: [PATCH 15/46] documentation of show/hide layer simplification --- docs/maps/api-reference.md | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/docs/maps/api-reference.md b/docs/maps/api-reference.md index 3a893474..d7d7f385 100644 --- a/docs/maps/api-reference.md +++ b/docs/maps/api-reference.md @@ -247,20 +247,9 @@ These 2 methods can be used to show and hide a layer. Example : ```javascript -
- - -
- +WA.showLayer('bottom'); +//... +WA.hideLayer('bottom'); ``` From 8e136cebe8a787433a33b13153f1e8f0d9a9f625 Mon Sep 17 00:00:00 2001 From: jonny Date: Mon, 10 May 2021 21:27:17 +0200 Subject: [PATCH 16/46] added callback on playermove - gets quite delayed after walking for a few seconds --- front/src/Api/Events/HasMovedEvent.ts | 19 ++++++ front/src/Api/Events/IframeEvent.ts | 4 +- front/src/Api/IframeListener.ts | 34 +++++++---- front/src/Phaser/Game/GameManager.ts | 7 +-- front/src/Phaser/Game/GameScene.ts | 8 ++- front/src/Phaser/Game/PlayerMovement.ts | 7 ++- .../Game/PlayersPositionInterpolator.ts | 8 +-- front/src/iframe_api.ts | 61 ++++++++++++++----- 8 files changed, 104 insertions(+), 44 deletions(-) create mode 100644 front/src/Api/Events/HasMovedEvent.ts diff --git a/front/src/Api/Events/HasMovedEvent.ts b/front/src/Api/Events/HasMovedEvent.ts new file mode 100644 index 00000000..fef8e731 --- /dev/null +++ b/front/src/Api/Events/HasMovedEvent.ts @@ -0,0 +1,19 @@ +import * as tg from "generic-type-guard"; + + + +export const isHasMovedEvent = + new tg.IsInterface().withProperties({ + direction: tg.isString, + moving: tg.isBoolean, + x: tg.isNumber, + y: tg.isNumber + }).get(); + +/** + * A message sent from the iFrame to the game to add a message in the chat. + */ +export type HasMovedEvent = tg.GuardedType; + + +export type HasMovedEventCallback = (event: HasMovedEvent) => void diff --git a/front/src/Api/Events/IframeEvent.ts b/front/src/Api/Events/IframeEvent.ts index c1ad6955..f28ea85e 100644 --- a/front/src/Api/Events/IframeEvent.ts +++ b/front/src/Api/Events/IframeEvent.ts @@ -1,11 +1,11 @@ - import { GameStateEvent } from './ApiGameStateEvent'; import { ButtonClickedEvent } from './ButtonClickedEvent'; import { ChatEvent } from './ChatEvent'; import { ClosePopupEvent } from './ClosePopupEvent'; import { EnterLeaveEvent } from './EnterLeaveEvent'; import { GoToPageEvent } from './GoToPageEvent'; +import { HasMovedEvent } from './HasMovedEvent'; import { OpenCoWebSiteEvent } from './OpenCoWebSiteEvent'; import { OpenPopupEvent } from './OpenPopupEvent'; import { OpenTabEvent } from './OpenTabEvent'; @@ -30,6 +30,7 @@ export type IframeEventMap = { restorePlayerControl: null displayBubble: null removeBubble: null + enableMoveEvents: undefined } export interface IframeEvent { type: T; @@ -46,6 +47,7 @@ export interface IframeResponseEventMap { leaveEvent: EnterLeaveEvent buttonClickedEvent: ButtonClickedEvent gameState: GameStateEvent + hasMovedEvent: HasMovedEvent } export interface IframeResponseEvent { type: T; diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index fcf4e854..f10d0fc1 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -14,6 +14,7 @@ import { IframeEventMap, IframeEvent, IframeResponseEvent, IframeResponseEventMa import { UserInputChatEvent } from "./Events/UserInputChatEvent"; import { GameStateEvent } from './Events/ApiGameStateEvent'; import { deepFreezeClone as deepFreezeClone } from '../utility'; +import { HasMovedEvent } from './Events/HasMovedEvent'; /** @@ -21,6 +22,7 @@ import { deepFreezeClone as deepFreezeClone } from '../utility'; * Also allows to send messages to those iframes. */ class IframeListener { + private readonly _chatStream: Subject = new Subject(); public readonly chatStream = this._chatStream.asObservable(); @@ -54,12 +56,13 @@ class IframeListener { private readonly _removeBubbleStream: Subject = new Subject(); public readonly removeBubbleStream = this._removeBubbleStream.asObservable(); - + private readonly _gameStateStream: Subject = new Subject(); public readonly gameStateStream = this._gameStateStream.asObservable(); private readonly iframes = new Set(); private readonly scripts = new Map(); + private sendMoveEvents: boolean = false; init() { window.addEventListener("message", (message: TypedMessageEvent>) => { @@ -101,20 +104,18 @@ class IframeListener { } else if (payload.type === 'closeCoWebSite') { scriptUtils.closeCoWebSite(); - } - else if (payload.type === 'disablePlayerControl') { + } else if (payload.type === 'disablePlayerControl') { this._disablePlayerControlStream.next(); - } - else if (payload.type === 'restorePlayerControl') { + } else if (payload.type === 'restorePlayerControl') { this._enablePlayerControlStream.next(); - } - else if (payload.type === 'displayBubble') { + } else if (payload.type === 'displayBubble') { this._displayBubbleStream.next(); - } - else if (payload.type === 'removeBubble') { + } else if (payload.type === 'removeBubble') { this._removeBubbleStream.next(); - }else if(payload.type=="getState"){ + } else if (payload.type == "getState") { this._gameStateStream.next(); + } else if (payload.type == "enableMoveEvents") { + this.sendMoveEvents = true } } @@ -123,11 +124,11 @@ class IframeListener { } - + sendFrozenGameStateEvent(gameStateEvent: GameStateEvent) { this.postMessage({ 'type': 'gameState', - 'data': deepFreezeClone(gameStateEvent) + 'data': deepFreezeClone(gameStateEvent) }); } @@ -234,6 +235,15 @@ class IframeListener { }); } + hasMovedEvent(event: HasMovedEvent) { + if (this.sendMoveEvents) { + this.postMessage({ + 'type': 'hasMovedEvent', + 'data': event + }); + } + } + sendButtonClickedEvent(popupId: number, buttonId: number): void { this.postMessage({ 'type': 'buttonClickedEvent', diff --git a/front/src/Phaser/Game/GameManager.ts b/front/src/Phaser/Game/GameManager.ts index 6047d430..157e8e80 100644 --- a/front/src/Phaser/Game/GameManager.ts +++ b/front/src/Phaser/Game/GameManager.ts @@ -8,12 +8,7 @@ import {SelectCharacterSceneName} from "../Login/SelectCharacterScene"; import {EnableCameraSceneName} from "../Login/EnableCameraScene"; import {localUserStore} from "../../Connexion/LocalUserStore"; -export interface HasMovedEvent { - direction: string; - moving: boolean; - x: number; - y: number; -} + /** * This class should be responsible for any scene starting/stopping diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 7d0d51d3..63efa3e6 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -1,4 +1,4 @@ -import {gameManager, HasMovedEvent} from "./GameManager"; +import { gameManager } from "./GameManager"; import { GroupCreatedUpdatedMessageInterface, MessageUserJoined, @@ -91,7 +91,8 @@ import {touchScreenManager} from "../../Touch/TouchScreenManager"; import {PinchManager} from "../UserInput/PinchManager"; import {joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey} from "../Components/MobileJoystick"; import { PlayerStateObject } from '../../Api/Events/ApiGameStateEvent'; -import {waScaleManager} from "../Services/WaScaleManager"; +import { waScaleManager } from "../Services/WaScaleManager"; +import { HasMovedEvent } from '../../Api/Events/HasMovedEvent'; export interface GameSceneInitInterface { initPosition: PointInterface|null, @@ -631,6 +632,9 @@ export class GameScene extends DirtyScene implements CenterListener { //listen event to share position of user this.CurrentPlayer.on(hasMovedEventName, this.pushPlayerPosition.bind(this)) + this.CurrentPlayer.on(hasMovedEventName, (event: HasMovedEvent) => { + iframeListener.hasMovedEvent(event) + }) this.CurrentPlayer.on(hasMovedEventName, this.outlineItem.bind(this)) this.CurrentPlayer.on(hasMovedEventName, (event: HasMovedEvent) => { this.gameMap.setPosition(event.x, event.y); diff --git a/front/src/Phaser/Game/PlayerMovement.ts b/front/src/Phaser/Game/PlayerMovement.ts index eb1a5d1b..18c3ee0c 100644 --- a/front/src/Phaser/Game/PlayerMovement.ts +++ b/front/src/Phaser/Game/PlayerMovement.ts @@ -1,6 +1,7 @@ -import {HasMovedEvent} from "./GameManager"; -import {MAX_EXTRAPOLATION_TIME} from "../../Enum/EnvironmentVariable"; -import {PositionInterface} from "../../Connexion/ConnexionModels"; + +import { MAX_EXTRAPOLATION_TIME } from "../../Enum/EnvironmentVariable"; +import { PositionInterface } from "../../Connexion/ConnexionModels"; +import { HasMovedEvent } from '../../Api/Events/HasMovedEvent'; export class PlayerMovement { public constructor(private startPosition: PositionInterface, private startTick: number, private endPosition: HasMovedEvent, private endTick: number) { diff --git a/front/src/Phaser/Game/PlayersPositionInterpolator.ts b/front/src/Phaser/Game/PlayersPositionInterpolator.ts index 3ac87397..321396e2 100644 --- a/front/src/Phaser/Game/PlayersPositionInterpolator.ts +++ b/front/src/Phaser/Game/PlayersPositionInterpolator.ts @@ -2,13 +2,13 @@ * This class is in charge of computing the position of all players. * Player movement is delayed by 200ms so position depends on ticks. */ -import {PlayerMovement} from "./PlayerMovement"; -import {HasMovedEvent} from "./GameManager"; +import { HasMovedEvent } from '../../Api/Events/HasMovedEvent'; +import { PlayerMovement } from "./PlayerMovement"; export class PlayersPositionInterpolator { playerMovements: Map = new Map(); - updatePlayerPosition(userId: number, playerMovement: PlayerMovement) : void { + updatePlayerPosition(userId: number, playerMovement: PlayerMovement): void { this.playerMovements.set(userId, playerMovement); } @@ -16,7 +16,7 @@ export class PlayersPositionInterpolator { this.playerMovements.delete(userId); } - getUpdatedPositions(tick: number) : Map { + getUpdatedPositions(tick: number): Map { const positions = new Map(); this.playerMovements.forEach((playerMovement: PlayerMovement, userId: number) => { if (playerMovement.isOutdated(tick)) { diff --git a/front/src/iframe_api.ts b/front/src/iframe_api.ts index cb55f1aa..9a3e63b0 100644 --- a/front/src/iframe_api.ts +++ b/front/src/iframe_api.ts @@ -1,5 +1,5 @@ import { ChatEvent } from "./Api/Events/ChatEvent"; -import { isIframeResponseEventWrapper } from "./Api/Events/IframeEvent"; +import { IframeEvent, IframeEventMap, isIframeResponseEventWrapper } from "./Api/Events/IframeEvent"; import { isUserInputChatEvent, UserInputChatEvent } from "./Api/Events/UserInputChatEvent"; import { Subject } from "rxjs"; import { EnterLeaveEvent, isEnterLeaveEvent } from "./Api/Events/EnterLeaveEvent"; @@ -10,6 +10,7 @@ import { OpenTabEvent } from "./Api/Events/OpenTabEvent"; import { GoToPageEvent } from "./Api/Events/GoToPageEvent"; import { OpenCoWebSiteEvent } from "./Api/Events/OpenCoWebSiteEvent"; import { GameStateEvent, isGameStateEvent } from './Api/Events/ApiGameStateEvent'; +import { HasMovedEvent, HasMovedEventCallback, isHasMovedEvent } from './Api/Events/HasMovedEvent'; interface WorkAdventureApi { sendChatMessage(message: string, author: string): void; @@ -17,15 +18,17 @@ interface WorkAdventureApi { onEnterZone(name: string, callback: () => void): void; onLeaveZone(name: string, callback: () => void): void; openPopup(targetObject: string, message: string, buttons: ButtonDescriptor[]): Popup; - openTab(url : string): void; - goToPage(url : string): void; - openCoWebSite(url : string): void; + openTab(url: string): void; + goToPage(url: string): void; + openCoWebSite(url: string): void; closeCoWebSite(): void; disablePlayerControl(): void; restorePlayerControl(): void; displayBubble(): void; removeBubble(): void; - getGameState():Promise + getGameState(): Promise + + onMoveEvent(callback: (moveEvent: HasMovedEvent) => void): void } declare global { @@ -75,20 +78,44 @@ class Popup { }, '*'); } } +function uuidv4() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { + const r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); +} + +const stateResolvers: Array<(event: GameStateEvent) => void> = [] + +const callbacks: { [type: string]: HasMovedEventCallback | ((arg?: HasMovedEvent | never) => void) } = {} -const stateResolvers:Array<(event:GameStateEvent)=>void> =[] +function postToParent(content: IframeEvent) { + window.parent.postMessage(content, "*") +} +let moveEventUuid: string | undefined; window.WA = { + onMoveEvent(callback: HasMovedEventCallback): void { + moveEventUuid = uuidv4(); + callbacks[moveEventUuid] = callback; + postToParent({ + type: "enableMoveEvents", + data: undefined + }) + window.parent.postMessage({ + type: "enable" + }, "*") + }, - getGameState(){ - return new Promise((resolver,thrower)=>{ + getGameState() { + return new Promise((resolver, thrower) => { stateResolvers.push(resolver); - window.parent.postMessage({ - type:"getState" - },"*") + window.parent.postMessage({ + type: "getState" + }, "*") }) }, @@ -140,10 +167,10 @@ window.WA = { }, '*'); }, - openCoWebSite(url : string) : void{ + openCoWebSite(url: string): void { window.parent.postMessage({ - "type" : 'openCoWebSite', - "data" : { + "type": 'openCoWebSite', + "data": { url } as OpenCoWebSiteEvent }, '*'); @@ -242,10 +269,12 @@ window.addEventListener('message', message => { if (callback) { callback(popup); } - }else if(payload.type=="gameState" && isGameStateEvent(payloadData)){ - stateResolvers.forEach(resolver=>{ + } else if (payload.type == "gameState" && isGameStateEvent(payloadData)) { + stateResolvers.forEach(resolver => { resolver(payloadData); }) + } else if (payload.type == "hasMovedEvent" && isHasMovedEvent(payloadData) && moveEventUuid) { + callbacks[moveEventUuid](payloadData) } } From 2c4c98b0e56c3d064d2a93aa6464b1b8d508b4de Mon Sep 17 00:00:00 2001 From: jonny Date: Mon, 10 May 2021 21:44:15 +0200 Subject: [PATCH 17/46] limited event trigger to max 10 per second --- front/src/Api/IframeListener.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index f10d0fc1..975dde67 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -15,6 +15,8 @@ import { UserInputChatEvent } from "./Events/UserInputChatEvent"; import { GameStateEvent } from './Events/ApiGameStateEvent'; import { deepFreezeClone as deepFreezeClone } from '../utility'; import { HasMovedEvent } from './Events/HasMovedEvent'; +import { Math } from 'phaser'; + /** @@ -63,6 +65,7 @@ class IframeListener { private readonly iframes = new Set(); private readonly scripts = new Map(); private sendMoveEvents: boolean = false; + private lastMoveTimestamp: number = 0 init() { window.addEventListener("message", (message: TypedMessageEvent>) => { @@ -237,10 +240,14 @@ class IframeListener { hasMovedEvent(event: HasMovedEvent) { if (this.sendMoveEvents) { - this.postMessage({ - 'type': 'hasMovedEvent', - 'data': event - }); + if (this.lastMoveTimestamp < Date.now() - 100) { + this.lastMoveTimestamp = Date.now() + this.postMessage({ + 'type': 'hasMovedEvent', + 'data': event + }); + } + } } From 43aad4ab143242086124e4519612145666589eb4 Mon Sep 17 00:00:00 2001 From: GRL Date: Wed, 12 May 2021 14:30:12 +0200 Subject: [PATCH 18/46] phaserLayers managed by Gamemap Implementation of LayersFlattener Implementation of Setting properties of a layer form script Update show/hide layer form script Update unit test of LayersIteratorTest --- front/src/Api/Events/IframeEvent.ts | 2 + front/src/Api/Events/setPropertyEvent.ts | 12 + front/src/Api/IframeListener.ts | 8 +- front/src/Phaser/Game/GameMap.ts | 39 +++- front/src/Phaser/Game/GameScene.ts | 78 ++++--- front/src/Phaser/Map/ITiledMap.ts | 3 + front/src/Phaser/Map/LayersFlattener.ts | 22 ++ front/src/Phaser/Map/LayersIterator.ts | 44 ---- front/src/iframe_api.ts | 14 +- front/tests/Phaser/Map/LayersIteratorTest.ts | 223 ++++++++++--------- maps/tests/iframe.html | 9 +- 11 files changed, 258 insertions(+), 196 deletions(-) create mode 100644 front/src/Api/Events/setPropertyEvent.ts create mode 100644 front/src/Phaser/Map/LayersFlattener.ts delete mode 100644 front/src/Phaser/Map/LayersIterator.ts diff --git a/front/src/Api/Events/IframeEvent.ts b/front/src/Api/Events/IframeEvent.ts index 2e7ccd86..d0994fa5 100644 --- a/front/src/Api/Events/IframeEvent.ts +++ b/front/src/Api/Events/IframeEvent.ts @@ -10,6 +10,7 @@ import { OpenPopupEvent } from './OpenPopupEvent'; import { OpenTabEvent } from './OpenTabEvent'; import { UserInputChatEvent } from './UserInputChatEvent'; import { LayerEvent } from './LayerEvent'; +import { SetPropertyEvent } from "./setPropertyEvent"; export interface TypedMessageEvent extends MessageEvent { @@ -32,6 +33,7 @@ export type IframeEventMap = { removeBubble: null showLayer: LayerEvent hideLayer: LayerEvent + setProperty: SetPropertyEvent } export interface IframeEvent { type: T; diff --git a/front/src/Api/Events/setPropertyEvent.ts b/front/src/Api/Events/setPropertyEvent.ts new file mode 100644 index 00000000..39785bc6 --- /dev/null +++ b/front/src/Api/Events/setPropertyEvent.ts @@ -0,0 +1,12 @@ +import * as tg from "generic-type-guard"; + +export const isSetPropertyEvent = + new tg.IsInterface().withProperties({ + layerName: tg.isString, + propertyName: tg.isString, + propertyValue: tg.isUnion(tg.isString, tg.isUnion(tg.isNumber, tg.isUnion(tg.isBoolean, tg.isUndefined))) + }).get(); +/** + * A message sent from the iFrame to the game to change the value of the property of the layer + */ +export type SetPropertyEvent = tg.GuardedType; \ No newline at end of file diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index 5529d36e..d8e3a8c8 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -12,7 +12,8 @@ import { GoToPageEvent, isGoToPageEvent } from "./Events/GoToPageEvent"; import { isOpenCoWebsite, OpenCoWebSiteEvent } from "./Events/OpenCoWebSiteEvent"; import { IframeEventMap, IframeEvent, IframeResponseEvent, IframeResponseEventMap, isIframeEventWrapper, TypedMessageEvent } from "./Events/IframeEvent"; import { UserInputChatEvent } from "./Events/UserInputChatEvent"; -import {isLayerEvent, LayerEvent} from "./Events/LayerEvent"; +import { isLayerEvent, LayerEvent } from "./Events/LayerEvent"; +import { isSetPropertyEvent, SetPropertyEvent} from "./Events/setPropertyEvent"; /** @@ -59,6 +60,9 @@ class IframeListener { private readonly _hideLayerStream: Subject = new Subject(); public readonly hideLayerStream = this._hideLayerStream.asObservable(); + private readonly _setPropertyStream: Subject = new Subject(); + public readonly setPropertyStream = this._setPropertyStream.asObservable(); + private readonly iframes = new Set(); private readonly scripts = new Map(); @@ -84,6 +88,8 @@ class IframeListener { 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)) { diff --git a/front/src/Phaser/Game/GameMap.ts b/front/src/Phaser/Game/GameMap.ts index 5fe91b62..b8b68e15 100644 --- a/front/src/Phaser/Game/GameMap.ts +++ b/front/src/Phaser/Game/GameMap.ts @@ -1,5 +1,5 @@ -import {ITiledMap, ITiledMapLayer} from "../Map/ITiledMap"; -import {LayersIterator} from "../Map/LayersIterator"; +import {ITiledMap, ITiledMapLayer, ITiledMapTileLayer} from "../Map/ITiledMap"; +import { flattenGroupLayersMap } from "../Map/LayersFlattener"; export type PropertyChangeCallback = (newValue: string | number | boolean | undefined, oldValue: string | number | boolean | undefined, allProps: Map) => void; @@ -11,10 +11,19 @@ export class GameMap { private key: number|undefined; private lastProperties = new Map(); private callbacks = new Map>(); - public readonly layersIterator: LayersIterator; + public readonly flatLayers: ITiledMapLayer[]; - public constructor(private map: ITiledMap) { - this.layersIterator = new LayersIterator(map); + public constructor(private map: ITiledMap, phaserMap: Phaser.Tilemaps.Tilemap, terrains: Array) { + this.flatLayers = flattenGroupLayersMap(map); + let depth = -2; + for (const layer of this.flatLayers) { + if(layer.type === 'tilelayer'){ + layer.phaserLayer = phaserMap.createLayer(layer.name, terrains, 0, 0).setDepth(depth); + } + if (layer.type === 'objectgroup' && layer.name === 'floorLayer') { + depth = 10000; + } + } } /** @@ -58,7 +67,7 @@ export class GameMap { private getProperties(key: number): Map { const properties = new Map(); - for (const layer of this.layersIterator) { + for (const layer of this.flatLayers) { if (layer.type !== 'tilelayer') { continue; } @@ -100,4 +109,22 @@ export class GameMap { } callbacksArray.push(callback); } + + public findLayer(layerName: string): ITiledMapLayer | undefined { + let i = 0; + let found = false; + while (!found && i = new Map(); Map!: Phaser.Tilemaps.Tilemap; - Layers!: Array; Objects!: Array; mapFile!: ITiledMap; groups: Map; @@ -392,7 +392,6 @@ export class GameScene extends DirtyScene implements CenterListener { //initalise map this.Map = this.add.tilemap(this.MapUrlFile); - this.gameMap = new GameMap(this.mapFile); const mapDirUrl = this.MapUrlFile.substr(0, this.MapUrlFile.lastIndexOf('/')); this.mapFile.tilesets.forEach((tileset: ITiledTileSet) => { this.Terrains.push(this.Map.addTilesetImage(tileset.name, `${mapDirUrl}/${tileset.image}`, tileset.tilewidth, tileset.tileheight, tileset.margin, tileset.spacing/*, tileset.firstgid*/)); @@ -402,11 +401,9 @@ export class GameScene extends DirtyScene implements CenterListener { this.physics.world.setBounds(0, 0, this.Map.widthInPixels, this.Map.heightInPixels); //add layer on map - this.Layers = new Array(); - let depth = -2; - for (const layer of this.gameMap.layersIterator) { + this.gameMap = new GameMap(this.mapFile, this.Map, this.Terrains); + for (const layer of this.gameMap.flatLayers) { if (layer.type === 'tilelayer') { - this.addLayer(this.Map.createLayer(layer.name, this.Terrains, 0, 0).setDepth(depth)); const exitSceneUrl = this.getExitSceneUrl(layer); if (exitSceneUrl !== undefined) { @@ -417,9 +414,6 @@ export class GameScene extends DirtyScene implements CenterListener { this.loadNextGame(exitUrl); } } - if (layer.type === 'objectgroup' && layer.name === 'floorLayer') { - depth = 10000; - } if (layer.type === 'objectgroup') { for (const object of layer.objects) { if (object.text) { @@ -428,9 +422,6 @@ export class GameScene extends DirtyScene implements CenterListener { } } } - if (depth === -2) { - throw new Error('Your map MUST contain a layer of type "objectgroup" whose name is "floorLayer" that represents the layer characters are drawn at. This layer cannot be contained in a group.'); - } this.initStartXAndStartY(); @@ -884,15 +875,38 @@ ${escapedMessage} this.setLayerVisibility(layerEvent.name, false); })); + this.iframeSubscriptionList.push(iframeListener.setPropertyStream.subscribe((setProperty) => { + this.setPropertyLayer(setProperty.layerName, setProperty.propertyName, setProperty.propertyValue); + })); + + } + + private setPropertyLayer(layerName: string, propertyName: string, propertyValue: string | number | boolean | undefined): void { + const layer = this.gameMap.findLayer(layerName); + if (layer === undefined) { + console.warn('Could not find layer "' + layerName + '" when calling setProperty'); + return; + } + const property = (layer.properties as ITiledMapLayerProperty[])?.find((property) => property.name === propertyName); + if (property === undefined) { + layer.properties = []; + layer.properties.push({name : propertyName, type : typeof propertyValue, value : propertyValue}); + return; + } + property.value = propertyValue; } private setLayerVisibility(layerName: string, visible: boolean): void { - const layer = this.Layers.find((layer) => layer.layer.name === layerName); + const layer = this.gameMap.findLayer(layerName); if (layer === undefined) { console.warn('Could not find layer "' + layerName + '" when calling WA.hideLayer / WA.showLayer'); return; } - layer.setVisible(visible); + if(layer.type != "tilelayer"){ + console.warn('The layer "' + layerName + '" is not a tilelayer. It can not be show/hide'); + return; + } + layer.phaserLayer?.setVisible(visible); this.dirty = true; } @@ -1001,7 +1015,7 @@ ${escapedMessage} } private initPositionFromLayerName(layerName: string) { - for (const layer of this.gameMap.layersIterator) { + for (const layer of this.gameMap.flatLayers) { if ((layerName === layer.name || layer.name.endsWith('/'+layerName)) && layer.type === 'tilelayer' && (layerName === defaultStartLayerName || this.isStartLayer(layer))) { const startPosition = this.startUser(layer); this.startX = startPosition.x + this.mapFile.tilewidth/2; @@ -1091,27 +1105,29 @@ ${escapedMessage} this.updateCameraOffset(); } - addLayer(Layer : Phaser.Tilemaps.TilemapLayer){ - this.Layers.push(Layer); - } - createCollisionWithPlayer() { this.physics.disableUpdate(); //add collision layer - this.Layers.forEach((Layer: Phaser.Tilemaps.TilemapLayer) => { - this.physics.add.collider(this.CurrentPlayer, Layer, (object1: GameObject, object2: GameObject) => { - //this.CurrentPlayer.say("Collision with layer : "+ (object2 as Tile).layer.name) - }); - Layer.setCollisionByProperty({collides: true}); - if (DEBUG_MODE) { - //debug code to see the collision hitbox of the object in the top layer - Layer.renderDebug(this.add.graphics(), { - tileColor: null, //non-colliding tiles - collidingTileColor: new Phaser.Display.Color(243, 134, 48, 200), // Colliding tiles, - faceColor: new Phaser.Display.Color(40, 39, 37, 255) // Colliding face edges + for (const Layer of this.gameMap.flatLayers) { + if (Layer.type == "tilelayer") { + if (Layer.phaserLayer === undefined) { + throw new Error('phaserLayer of layer "' + Layer.name + '" is undefined'); + } + this.physics.add.collider(this.CurrentPlayer, Layer.phaserLayer, (object1: GameObject, object2: GameObject) => { + //this.CurrentPlayer.say("Collision with layer : "+ (object2 as Tile).layer.name) }); + Layer.phaserLayer.setCollisionByProperty({collides: true}); + if (DEBUG_MODE) { + //debug code to see the collision hitbox of the object in the top layer + Layer.phaserLayer.renderDebug(this.add.graphics(), { + tileColor: null, //non-colliding tiles + collidingTileColor: new Phaser.Display.Color(243, 134, 48, 200), // Colliding tiles, + faceColor: new Phaser.Display.Color(40, 39, 37, 255) // Colliding face edges + }); + } + //}); } - }); + } } createCurrentPlayer(){ diff --git a/front/src/Phaser/Map/ITiledMap.ts b/front/src/Phaser/Map/ITiledMap.ts index c4828911..d381e9d4 100644 --- a/front/src/Phaser/Map/ITiledMap.ts +++ b/front/src/Phaser/Map/ITiledMap.ts @@ -4,6 +4,8 @@ * Represents the interface for the Tiled exported data structure (JSON). Used * when loading resources via Resource loader. */ +import TilemapLayer = Phaser.Tilemaps.TilemapLayer; + export interface ITiledMap { width: number; height: number; @@ -81,6 +83,7 @@ export interface ITiledMapTileLayer { * Draw order (topdown (default), index) */ draworder?: string; + phaserLayer?: TilemapLayer; } export interface ITiledMapObjectLayer { diff --git a/front/src/Phaser/Map/LayersFlattener.ts b/front/src/Phaser/Map/LayersFlattener.ts new file mode 100644 index 00000000..a3b12522 --- /dev/null +++ b/front/src/Phaser/Map/LayersFlattener.ts @@ -0,0 +1,22 @@ +import {ITiledMap, ITiledMapLayer} from "./ITiledMap"; + +/** + * Flatten the grouped layers + */ +export function flattenGroupLayersMap(map: ITiledMap) { + let flatLayers: ITiledMapLayer[] = []; + flattenGroupLayers(map.layers, '', flatLayers); + return flatLayers; +} + +function flattenGroupLayers(layers : ITiledMapLayer[], prefix : string, flatLayers: ITiledMapLayer[]) { + for (const layer of layers) { + if (layer.type === 'group') { + flattenGroupLayers(layer.layers, prefix + layer.name + '/', flatLayers); + } else { + const layerWithNewName = { ...layer }; + layerWithNewName.name = prefix+layerWithNewName.name; + flatLayers.push(layerWithNewName); + } + } +} \ No newline at end of file diff --git a/front/src/Phaser/Map/LayersIterator.ts b/front/src/Phaser/Map/LayersIterator.ts deleted file mode 100644 index 501a5f7b..00000000 --- a/front/src/Phaser/Map/LayersIterator.ts +++ /dev/null @@ -1,44 +0,0 @@ -import {ITiledMap, ITiledMapLayer} from "./ITiledMap"; - -/** - * Iterates over the layers of a map, flattening the grouped layers - */ -export class LayersIterator implements IterableIterator { - - private layers: ITiledMapLayer[] = []; - private pointer: number = 0; - - constructor(private map: ITiledMap) { - this.initLayersList(map.layers, ''); - } - - private initLayersList(layers : ITiledMapLayer[], prefix : string) { - for (const layer of layers) { - if (layer.type === 'group') { - this.initLayersList(layer.layers, prefix + layer.name + '/'); - } else { - const layerWithNewName = { ...layer }; - layerWithNewName.name = prefix+layerWithNewName.name; - this.layers.push(layerWithNewName); - } - } - } - - public next(): IteratorResult { - if (this.pointer < this.layers.length) { - return { - done: false, - value: this.layers[this.pointer++] - } - } else { - return { - done: true, - value: null - } - } - } - - [Symbol.iterator](): IterableIterator { - return new LayersIterator(this.map); - } -} diff --git a/front/src/iframe_api.ts b/front/src/iframe_api.ts index 9f059cd0..a96ad193 100644 --- a/front/src/iframe_api.ts +++ b/front/src/iframe_api.ts @@ -9,7 +9,8 @@ import { ClosePopupEvent } from "./Api/Events/ClosePopupEvent"; import { OpenTabEvent } from "./Api/Events/OpenTabEvent"; import { GoToPageEvent } from "./Api/Events/GoToPageEvent"; import { OpenCoWebSiteEvent } from "./Api/Events/OpenCoWebSiteEvent"; -import {LayerEvent} from "./Api/Events/LayerEvent"; +import { LayerEvent } from "./Api/Events/LayerEvent"; +import { SetPropertyEvent } from "./Api/Events/setPropertyEvent"; interface WorkAdventureApi { sendChatMessage(message: string, author: string): void; @@ -27,6 +28,7 @@ interface WorkAdventureApi { removeBubble() : void; showLayer(layer: string) : void; hideLayer(layer: string) : void; + setProperty(layerName: string, propertyName: string, propertyValue: string | number | boolean | undefined): void; } declare global { @@ -107,6 +109,16 @@ window.WA = { } as LayerEvent }, '*'); }, + setProperty(layerName: string, propertyName: string, propertyValue: string | number | boolean | undefined): void { + window.parent.postMessage({ + 'type' : 'setProperty', + 'data' : { + 'layerName' : layerName, + 'propertyName' : propertyName, + 'propertyValue' : propertyValue + } as SetPropertyEvent + }, '*'); + }, disablePlayerControls(): void { window.parent.postMessage({ 'type': 'disablePlayerControls' }, '*'); }, diff --git a/front/tests/Phaser/Map/LayersIteratorTest.ts b/front/tests/Phaser/Map/LayersIteratorTest.ts index 3b9d0d9b..de95ecef 100644 --- a/front/tests/Phaser/Map/LayersIteratorTest.ts +++ b/front/tests/Phaser/Map/LayersIteratorTest.ts @@ -1,145 +1,148 @@ import "jasmine"; import {Room} from "../../../src/Connexion/Room"; -import {LayersIterator} from "../../../src/Phaser/Map/LayersIterator"; +import {flattenGroupLayersMap} from "../../../src/Phaser/Map/LayersFlattener"; +import {ITiledMapLayer} from "../../../src/Phaser/Map/ITiledMap"; -describe("Layers iterator", () => { +describe("Layers flattener", () => { it("should iterate maps with no group", () => { - const layersIterator = new LayersIterator({ - "compressionlevel":-1, - "height":2, - "infinite":false, - "layers":[ + let flatLayers:ITiledMapLayer[] = []; + flatLayers = flattenGroupLayersMap({ + "compressionlevel": -1, + "height": 2, + "infinite": false, + "layers": [ { - "data":[0, 0, 0, 0], - "height":2, - "id":1, - "name":"Tile Layer 1", - "opacity":1, - "type":"tilelayer", - "visible":true, - "width":2, - "x":0, - "y":0 + "data": [0, 0, 0, 0], + "height": 2, + "id": 1, + "name": "Tile Layer 1", + "opacity": 1, + "type": "tilelayer", + "visible": true, + "width": 2, + "x": 0, + "y": 0 }, { - "data":[0, 0, 0, 0], - "height":2, - "id":1, - "name":"Tile Layer 2", - "opacity":1, - "type":"tilelayer", - "visible":true, - "width":2, - "x":0, - "y":0 + "data": [0, 0, 0, 0], + "height": 2, + "id": 1, + "name": "Tile Layer 2", + "opacity": 1, + "type": "tilelayer", + "visible": true, + "width": 2, + "x": 0, + "y": 0 }], - "nextlayerid":2, - "nextobjectid":1, - "orientation":"orthogonal", - "renderorder":"right-down", - "tiledversion":"2021.03.23", - "tileheight":32, - "tilesets":[], - "tilewidth":32, - "type":"map", - "version":1.5, - "width":2 + "nextlayerid": 2, + "nextobjectid": 1, + "orientation": "orthogonal", + "renderorder": "right-down", + "tiledversion": "2021.03.23", + "tileheight": 32, + "tilesets": [], + "tilewidth": 32, + "type": "map", + "version": 1.5, + "width": 2 }) const layers = []; - for (const layer of layersIterator) { + for (const layer of flatLayers) { layers.push(layer.name); } expect(layers).toEqual(['Tile Layer 1', 'Tile Layer 2']); }); it("should iterate maps with recursive groups", () => { - const layersIterator = new LayersIterator({ - "compressionlevel":-1, - "height":2, - "infinite":false, - "layers":[ + let flatLayers:ITiledMapLayer[] = []; + flatLayers = flattenGroupLayersMap({ + "compressionlevel": -1, + "height": 2, + "infinite": false, + "layers": [ { - "id":6, - "layers":[ + "id": 6, + "layers": [ { - "id":5, - "layers":[ + "id": 5, + "layers": [ { - "data":[0, 0, 0, 0], - "height":2, - "id":10, - "name":"Tile3", - "opacity":1, - "type":"tilelayer", - "visible":true, - "width":2, - "x":0, - "y":0 + "data": [0, 0, 0, 0], + "height": 2, + "id": 10, + "name": "Tile3", + "opacity": 1, + "type": "tilelayer", + "visible": true, + "width": 2, + "x": 0, + "y": 0 }, { - "data":[0, 0, 0, 0], - "height":2, - "id":9, - "name":"Tile2", - "opacity":1, - "type":"tilelayer", - "visible":true, - "width":2, - "x":0, - "y":0 + "data": [0, 0, 0, 0], + "height": 2, + "id": 9, + "name": "Tile2", + "opacity": 1, + "type": "tilelayer", + "visible": true, + "width": 2, + "x": 0, + "y": 0 }], - "name":"Group 3", - "opacity":1, - "type":"group", - "visible":true, - "x":0, - "y":0 + "name": "Group 3", + "opacity": 1, + "type": "group", + "visible": true, + "x": 0, + "y": 0 }, { - "id":7, - "layers":[ + "id": 7, + "layers": [ { - "data":[0, 0, 0, 0], - "height":2, - "id":8, - "name":"Tile1", - "opacity":1, - "type":"tilelayer", - "visible":true, - "width":2, - "x":0, - "y":0 + "data": [0, 0, 0, 0], + "height": 2, + "id": 8, + "name": "Tile1", + "opacity": 1, + "type": "tilelayer", + "visible": true, + "width": 2, + "x": 0, + "y": 0 }], - "name":"Group 2", - "opacity":1, - "type":"group", - "visible":true, - "x":0, - "y":0 + "name": "Group 2", + "opacity": 1, + "type": "group", + "visible": true, + "x": 0, + "y": 0 }], - "name":"Group 1", - "opacity":1, - "type":"group", - "visible":true, - "x":0, - "y":0 + "name": "Group 1", + "opacity": 1, + "type": "group", + "visible": true, + "x": 0, + "y": 0 }], - "nextlayerid":11, - "nextobjectid":1, - "orientation":"orthogonal", - "renderorder":"right-down", - "tiledversion":"2021.03.23", - "tileheight":32, - "tilesets":[], - "tilewidth":32, - "type":"map", - "version":1.5, - "width":2 + "nextlayerid": 11, + "nextobjectid": 1, + "orientation": "orthogonal", + "renderorder": "right-down", + "tiledversion": "2021.03.23", + "tileheight": 32, + "tilesets": [], + "tilewidth": 32, + "type": "map", + "version": 1.5, + "width": 2 }) const layers = []; - for (const layer of layersIterator) { + for (const layer of flatLayers) { layers.push(layer.name); } expect(layers).toEqual(['Group 1/Group 3/Tile3', 'Group 1/Group 3/Tile2', 'Group 1/Group 2/Tile1']); diff --git a/maps/tests/iframe.html b/maps/tests/iframe.html index c5c30972..f9f43f20 100644 --- a/maps/tests/iframe.html +++ b/maps/tests/iframe.html @@ -19,17 +19,20 @@ }));
- +
+ From 39539214df3ae7993bc13af738898ae95fafe2fe Mon Sep 17 00:00:00 2001 From: GRL Date: Mon, 17 May 2021 10:13:48 +0200 Subject: [PATCH 19/46] documentation of SetProperty --- docs/maps/api-reference.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/maps/api-reference.md b/docs/maps/api-reference.md index d7d7f385..6e98dfb5 100644 --- a/docs/maps/api-reference.md +++ b/docs/maps/api-reference.md @@ -252,4 +252,15 @@ WA.showLayer('bottom'); WA.hideLayer('bottom'); ``` +### Set/Create properties in a layer + +``` +WA.setProperty(layerName : string, propertyName : string, propertyValue : string | number | boolean | undefined) : void; +``` + +Set the value of the "propertyName" property of the layer "layerName" at "propertyValue". If the property doesn't exist, create the property "propertyName" and set the value of the property at "propertyValue". + +```javascript +WA.setProperty('wikiLayer', 'openWebsite', 'https://www.wikipedia.org/'); +``` From 9b68faac0e491b58a8a0b30734d740f0b916b34b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Tue, 18 May 2021 09:53:54 +0200 Subject: [PATCH 20/46] Fixing JSDoc --- front/src/Api/Events/MenuItemClickedEvent.ts | 2 +- front/src/Api/Events/MenuItemRegisterEvent.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/front/src/Api/Events/MenuItemClickedEvent.ts b/front/src/Api/Events/MenuItemClickedEvent.ts index dd80c0f2..0735eda4 100644 --- a/front/src/Api/Events/MenuItemClickedEvent.ts +++ b/front/src/Api/Events/MenuItemClickedEvent.ts @@ -5,6 +5,6 @@ export const isMenuItemClickedEvent = menuItem: 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. + * A message sent from the game to the iFrame when a menu item is clicked. */ export type MenuItemClickedEvent = tg.GuardedType; diff --git a/front/src/Api/Events/MenuItemRegisterEvent.ts b/front/src/Api/Events/MenuItemRegisterEvent.ts index 98d4c7d3..a25e5cc3 100644 --- a/front/src/Api/Events/MenuItemRegisterEvent.ts +++ b/front/src/Api/Events/MenuItemRegisterEvent.ts @@ -5,6 +5,6 @@ export const isMenuItemRegisterEvent = menutItem: 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. + * A message sent from the iFrame to the game to add a new menu item. */ export type MenuItemRegisterEvent = tg.GuardedType; From 3edfd5b285c5d2f51eab85c0c1fc865a04ffcc8e Mon Sep 17 00:00:00 2001 From: GRL Date: Tue, 18 May 2021 11:33:16 +0200 Subject: [PATCH 21/46] GameState is now save in cache HasPlayerMoved is send when the player is actually moving on the map every 200ms. --- ...ApiGameStateEvent.ts => GameStateEvent.ts} | 9 +- .../Api/Events/HasDataLayerChangedEvent.ts | 16 ++ front/src/Api/Events/HasMovedEvent.ts | 19 -- front/src/Api/Events/HasPlayerMovedEvent.ts | 19 ++ front/src/Api/Events/IframeEvent.ts | 11 +- front/src/Api/IframeListener.ts | 42 ++-- front/src/Phaser/Game/GameScene.ts | 28 +-- front/src/Phaser/Game/PlayerMovement.ts | 6 +- .../Game/PlayersPositionInterpolator.ts | 6 +- front/src/iframe_api.ts | 109 ++++++--- maps/tests/Metadata/map.json | 230 ++++++++++++++++++ maps/tests/Metadata/script.js | 9 + maps/tests/Metadata/tileset_dungeon.png | Bin 0 -> 9696 bytes 13 files changed, 404 insertions(+), 100 deletions(-) rename front/src/Api/Events/{ApiGameStateEvent.ts => GameStateEvent.ts} (72%) create mode 100644 front/src/Api/Events/HasDataLayerChangedEvent.ts delete mode 100644 front/src/Api/Events/HasMovedEvent.ts create mode 100644 front/src/Api/Events/HasPlayerMovedEvent.ts create mode 100644 maps/tests/Metadata/map.json create mode 100644 maps/tests/Metadata/script.js create mode 100644 maps/tests/Metadata/tileset_dungeon.png diff --git a/front/src/Api/Events/ApiGameStateEvent.ts b/front/src/Api/Events/GameStateEvent.ts similarity index 72% rename from front/src/Api/Events/ApiGameStateEvent.ts rename to front/src/Api/Events/GameStateEvent.ts index 4f4e98ff..418d1ca0 100644 --- a/front/src/Api/Events/ApiGameStateEvent.ts +++ b/front/src/Api/Events/GameStateEvent.ts @@ -1,6 +1,6 @@ import * as tg from "generic-type-guard"; -export const isPositionState = new tg.IsInterface().withProperties({ +/*export const isPositionState = new tg.IsInterface().withProperties({ x: tg.isNumber, y: tg.isNumber }).get() @@ -12,19 +12,16 @@ export const isPlayerState = new tg.IsInterface() }).get() ).get() -export type PlayerStateObject = tg.GuardedType; +export type PlayerStateObject = tg.GuardedType;*/ export const isGameStateEvent = new tg.IsInterface().withProperties({ roomId: tg.isString, - data: tg.isObject, mapUrl: tg.isString, - nickName: tg.isString, uuid: tg.isUnion(tg.isString, tg.isUndefined), - players: isPlayerState, startLayerName: tg.isUnion(tg.isString, tg.isNull) }).get(); /** - * A message sent from the game to the iFrame when a user enters or leaves a zone marked with the "zone" property. + * A message sent from the game to the iFrame when the gameState is got by the script */ export type GameStateEvent = tg.GuardedType; \ No newline at end of file diff --git a/front/src/Api/Events/HasDataLayerChangedEvent.ts b/front/src/Api/Events/HasDataLayerChangedEvent.ts new file mode 100644 index 00000000..7714f978 --- /dev/null +++ b/front/src/Api/Events/HasDataLayerChangedEvent.ts @@ -0,0 +1,16 @@ +import * as tg from "generic-type-guard"; + + + +export const isHasDataLayerChangedEvent = + new tg.IsInterface().withProperties({ + data: tg.isObject + }).get(); + +/** + * A message sent from the game to the iFrame when the data of the layers change after the iFrame send a message to the game that it want to listen to the data of the layers + */ +export type HasDataLayerChangedEvent = tg.GuardedType; + + +export type HasDataLayerChangedEventCallback = (event: HasDataLayerChangedEvent) => void \ No newline at end of file diff --git a/front/src/Api/Events/HasMovedEvent.ts b/front/src/Api/Events/HasMovedEvent.ts deleted file mode 100644 index fef8e731..00000000 --- a/front/src/Api/Events/HasMovedEvent.ts +++ /dev/null @@ -1,19 +0,0 @@ -import * as tg from "generic-type-guard"; - - - -export const isHasMovedEvent = - new tg.IsInterface().withProperties({ - direction: tg.isString, - moving: tg.isBoolean, - x: tg.isNumber, - y: tg.isNumber - }).get(); - -/** - * A message sent from the iFrame to the game to add a message in the chat. - */ -export type HasMovedEvent = tg.GuardedType; - - -export type HasMovedEventCallback = (event: HasMovedEvent) => void diff --git a/front/src/Api/Events/HasPlayerMovedEvent.ts b/front/src/Api/Events/HasPlayerMovedEvent.ts new file mode 100644 index 00000000..28603284 --- /dev/null +++ b/front/src/Api/Events/HasPlayerMovedEvent.ts @@ -0,0 +1,19 @@ +import * as tg from "generic-type-guard"; + + + +export const isHasPlayerMovedEvent = + new tg.IsInterface().withProperties({ + direction: tg.isString, + moving: tg.isBoolean, + x: tg.isNumber, + y: tg.isNumber + }).get(); + +/** + * A message sent from the game to the iFrame when the player move after the iFrame send a message to the game that it want to listen to the position of the player + */ +export type HasPlayerMovedEvent = tg.GuardedType; + + +export type HasPlayerMovedEventCallback = (event: HasPlayerMovedEvent) => void diff --git a/front/src/Api/Events/IframeEvent.ts b/front/src/Api/Events/IframeEvent.ts index 307b09fc..ae0eab34 100644 --- a/front/src/Api/Events/IframeEvent.ts +++ b/front/src/Api/Events/IframeEvent.ts @@ -1,15 +1,16 @@ -import { GameStateEvent } from './ApiGameStateEvent'; +import { GameStateEvent } from './GameStateEvent'; import { ButtonClickedEvent } from './ButtonClickedEvent'; import { ChatEvent } from './ChatEvent'; import { ClosePopupEvent } from './ClosePopupEvent'; import { EnterLeaveEvent } from './EnterLeaveEvent'; import { GoToPageEvent } from './GoToPageEvent'; -import { HasMovedEvent } from './HasMovedEvent'; +import { HasPlayerMovedEvent } from './HasPlayerMovedEvent'; import { OpenCoWebSiteEvent } from './OpenCoWebSiteEvent'; import { OpenPopupEvent } from './OpenPopupEvent'; import { OpenTabEvent } from './OpenTabEvent'; import { UserInputChatEvent } from './UserInputChatEvent'; +import { HasDataLayerChangedEvent } from "./HasDataLayerChangedEvent"; export interface TypedMessageEvent extends MessageEvent { @@ -30,7 +31,8 @@ export type IframeEventMap = { restorePlayerControls: null displayBubble: null removeBubble: null - enableMoveEvents: undefined + onPlayerMove: undefined + onDataLayerChange: undefined } export interface IframeEvent { type: T; @@ -47,7 +49,8 @@ export interface IframeResponseEventMap { leaveEvent: EnterLeaveEvent buttonClickedEvent: ButtonClickedEvent gameState: GameStateEvent - hasMovedEvent: HasMovedEvent + hasPlayerMoved: HasPlayerMovedEvent + hasDataLayerChanged: HasDataLayerChangedEvent } export interface IframeResponseEvent { type: T; diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index 82dd23cf..d6c02516 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -12,10 +12,11 @@ import { GoToPageEvent, isGoToPageEvent } from "./Events/GoToPageEvent"; import { isOpenCoWebsite, OpenCoWebSiteEvent } from "./Events/OpenCoWebSiteEvent"; import { IframeEventMap, IframeEvent, IframeResponseEvent, IframeResponseEventMap, isIframeEventWrapper, TypedMessageEvent } from "./Events/IframeEvent"; import { UserInputChatEvent } from "./Events/UserInputChatEvent"; -import { GameStateEvent } from './Events/ApiGameStateEvent'; +import { GameStateEvent } from './Events/GameStateEvent'; import { deepFreezeClone as deepFreezeClone } from '../utility'; -import { HasMovedEvent } from './Events/HasMovedEvent'; +import { HasPlayerMovedEvent } from './Events/HasPlayerMovedEvent'; import { Math } from 'phaser'; +import { HasDataLayerChangedEvent } from "./Events/HasDataLayerChangedEvent"; @@ -58,14 +59,14 @@ class IframeListener { private readonly _removeBubbleStream: Subject = new Subject(); public readonly removeBubbleStream = this._removeBubbleStream.asObservable(); - private readonly _gameStateStream: Subject = new Subject(); public readonly gameStateStream = this._gameStateStream.asObservable(); + private readonly iframes = new Set(); private readonly scripts = new Map(); - private sendMoveEvents: boolean = false; - private lastMoveTimestamp: number = 0 + private sendPlayerMove: boolean = false; + private sendDataLayerChange: boolean = false; init() { window.addEventListener("message", (message: TypedMessageEvent>) => { @@ -119,8 +120,10 @@ class IframeListener { this._removeBubbleStream.next(); } else if (payload.type == "getState") { this._gameStateStream.next(); - } else if (payload.type == "enableMoveEvents") { - this.sendMoveEvents = true + } else if (payload.type == "onPlayerMove") { + this.sendPlayerMove = true + } else if (payload.type == "onDataLayerChange") { + this.sendDataLayerChange = true } } @@ -133,7 +136,7 @@ class IframeListener { sendFrozenGameStateEvent(gameStateEvent: GameStateEvent) { this.postMessage({ 'type': 'gameState', - 'data': deepFreezeClone(gameStateEvent) + 'data': gameStateEvent //deepFreezeClone(gameStateEvent) }); } @@ -240,16 +243,21 @@ class IframeListener { }); } - hasMovedEvent(event: HasMovedEvent) { - if (this.sendMoveEvents) { - if (this.lastMoveTimestamp < Date.now() - 100) { - this.lastMoveTimestamp = Date.now() - this.postMessage({ - 'type': 'hasMovedEvent', - 'data': event - }); - } + hasPlayerMoved(event: HasPlayerMovedEvent) { + if (this.sendPlayerMove) { + this.postMessage({ + 'type': 'hasPlayerMoved', + 'data': event + }); + } + } + hasDataLayerChanged(event: HasDataLayerChangedEvent) { + if (this.sendDataLayerChange) { + this.postMessage({ + 'type' : 'hasDataLayerChanged', + 'data' : event + }); } } diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 39fa79db..83256cec 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -90,9 +90,9 @@ import { TextUtils } from "../Components/TextUtils"; import { touchScreenManager } from "../../Touch/TouchScreenManager"; import { PinchManager } from "../UserInput/PinchManager"; import { joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey } from "../Components/MobileJoystick"; -import { PlayerStateObject } from '../../Api/Events/ApiGameStateEvent'; +//import { PlayerStateObject } from '../../Api/Events/GameStateEvent'; import { waScaleManager } from "../Services/WaScaleManager"; -import { HasMovedEvent } from '../../Api/Events/HasMovedEvent'; +import { HasPlayerMovedEvent } from '../../Api/Events/HasPlayerMovedEvent'; export interface GameSceneInitInterface { initPosition: PointInterface | null, @@ -164,7 +164,7 @@ export class GameScene extends DirtyScene implements CenterListener { currentTick!: number; lastSentTick!: number; // The last tick at which a position was sent. - lastMoveEventSent: HasMovedEvent = { + lastMoveEventSent: HasPlayerMovedEvent = { direction: '', moving: false, x: -1000, @@ -632,11 +632,11 @@ export class GameScene extends DirtyScene implements CenterListener { //listen event to share position of user this.CurrentPlayer.on(hasMovedEventName, this.pushPlayerPosition.bind(this)) - this.CurrentPlayer.on(hasMovedEventName, (event: HasMovedEvent) => { - iframeListener.hasMovedEvent(event) + this.CurrentPlayer.on(hasMovedEventName, (event: HasPlayerMovedEvent) => { + //iframeListener.hasMovedEvent(event) }) this.CurrentPlayer.on(hasMovedEventName, this.outlineItem.bind(this)) - this.CurrentPlayer.on(hasMovedEventName, (event: HasMovedEvent) => { + this.CurrentPlayer.on(hasMovedEventName, (event: HasPlayerMovedEvent) => { this.gameMap.setPosition(event.x, event.y); }) @@ -870,7 +870,7 @@ ${escapedMessage} this.userInputManager.restoreControls(); })); this.iframeSubscriptionList.push(iframeListener.gameStateStream.subscribe(() => { - const playerObject: PlayerStateObject = { + /*const playerObject: PlayerStateObject = { [this.playerName]: { position: { x: this.CurrentPlayer.x, @@ -889,15 +889,12 @@ ${escapedMessage} pusherId: remotePlayer.userId } - } + }*/ iframeListener.sendFrozenGameStateEvent({ mapUrl: this.MapUrlFile, - nickName: this.playerName, startLayerName: this.startLayerName, uuid: localUserStore.getLocalUser()?.uuid, roomId: this.RoomId, - data: this.mapFile, - players: playerObject }) })); @@ -1158,7 +1155,7 @@ ${escapedMessage} this.createCollisionWithPlayer(); } - pushPlayerPosition(event: HasMovedEvent) { + pushPlayerPosition(event: HasPlayerMovedEvent) { if (this.lastMoveEventSent === event) { return; } @@ -1188,7 +1185,7 @@ ${escapedMessage} * Finds the correct item to outline and outline it (if there is an item to be outlined) * @param event */ - private outlineItem(event: HasMovedEvent): void { + private outlineItem(event: HasPlayerMovedEvent): void { let x = event.x; let y = event.y; switch (event.direction) { @@ -1227,7 +1224,7 @@ ${escapedMessage} this.outlinedItem?.selectable(); } - private doPushPlayerPosition(event: HasMovedEvent): void { + private doPushPlayerPosition(event: HasPlayerMovedEvent): void { this.lastMoveEventSent = event; this.lastSentTick = this.currentTick; const camera = this.cameras.main; @@ -1237,6 +1234,7 @@ ${escapedMessage} right: camera.scrollX + camera.width, bottom: camera.scrollY + camera.height, }); + iframeListener.hasPlayerMoved(event); } /** @@ -1286,7 +1284,7 @@ ${escapedMessage} } // Let's move all users const updatedPlayersPositions = this.playersPositionInterpolator.getUpdatedPositions(time); - updatedPlayersPositions.forEach((moveEvent: HasMovedEvent, userId: number) => { + updatedPlayersPositions.forEach((moveEvent: HasPlayerMovedEvent, userId: number) => { this.dirty = true; const player: RemotePlayer | undefined = this.MapPlayersByKey.get(userId); if (player === undefined) { diff --git a/front/src/Phaser/Game/PlayerMovement.ts b/front/src/Phaser/Game/PlayerMovement.ts index 18c3ee0c..5680d7de 100644 --- a/front/src/Phaser/Game/PlayerMovement.ts +++ b/front/src/Phaser/Game/PlayerMovement.ts @@ -1,10 +1,10 @@ import { MAX_EXTRAPOLATION_TIME } from "../../Enum/EnvironmentVariable"; import { PositionInterface } from "../../Connexion/ConnexionModels"; -import { HasMovedEvent } from '../../Api/Events/HasMovedEvent'; +import { HasPlayerMovedEvent } from '../../Api/Events/HasPlayerMovedEvent'; export class PlayerMovement { - public constructor(private startPosition: PositionInterface, private startTick: number, private endPosition: HasMovedEvent, private endTick: number) { + public constructor(private startPosition: PositionInterface, private startTick: number, private endPosition: HasPlayerMovedEvent, private endTick: number) { } public isOutdated(tick: number): boolean { @@ -18,7 +18,7 @@ export class PlayerMovement { return tick > this.endTick + MAX_EXTRAPOLATION_TIME; } - public getPosition(tick: number): HasMovedEvent { + public getPosition(tick: number): HasPlayerMovedEvent { // Special case: end position reached and end position is not moving if (tick >= this.endTick && this.endPosition.moving === false) { //console.log('Movement finished ', this.endPosition) diff --git a/front/src/Phaser/Game/PlayersPositionInterpolator.ts b/front/src/Phaser/Game/PlayersPositionInterpolator.ts index 321396e2..53578884 100644 --- a/front/src/Phaser/Game/PlayersPositionInterpolator.ts +++ b/front/src/Phaser/Game/PlayersPositionInterpolator.ts @@ -2,7 +2,7 @@ * This class is in charge of computing the position of all players. * Player movement is delayed by 200ms so position depends on ticks. */ -import { HasMovedEvent } from '../../Api/Events/HasMovedEvent'; +import { HasPlayerMovedEvent } from '../../Api/Events/HasPlayerMovedEvent'; import { PlayerMovement } from "./PlayerMovement"; export class PlayersPositionInterpolator { @@ -16,8 +16,8 @@ export class PlayersPositionInterpolator { this.playerMovements.delete(userId); } - getUpdatedPositions(tick: number): Map { - const positions = new Map(); + getUpdatedPositions(tick: number): Map { + const positions = new Map(); this.playerMovements.forEach((playerMovement: PlayerMovement, userId: number) => { if (playerMovement.isOutdated(tick)) { //console.log("outdated") diff --git a/front/src/iframe_api.ts b/front/src/iframe_api.ts index 17c489ca..c2e91ea5 100644 --- a/front/src/iframe_api.ts +++ b/front/src/iframe_api.ts @@ -9,8 +9,9 @@ import { ClosePopupEvent } from "./Api/Events/ClosePopupEvent"; import { OpenTabEvent } from "./Api/Events/OpenTabEvent"; import { GoToPageEvent } from "./Api/Events/GoToPageEvent"; import { OpenCoWebSiteEvent } from "./Api/Events/OpenCoWebSiteEvent"; -import { GameStateEvent, isGameStateEvent } from './Api/Events/ApiGameStateEvent'; -import { HasMovedEvent, HasMovedEventCallback, isHasMovedEvent } from './Api/Events/HasMovedEvent'; +import { GameStateEvent, isGameStateEvent } from './Api/Events/GameStateEvent'; +import { HasPlayerMovedEvent, HasPlayerMovedEventCallback, isHasPlayerMovedEvent } from './Api/Events/HasPlayerMovedEvent'; +import { HasDataLayerChangedEvent, HasDataLayerChangedEventCallback, isHasDataLayerChangedEvent} from "./Api/Events/HasDataLayerChangedEvent"; interface WorkAdventureApi { sendChatMessage(message: string, author: string): void; @@ -26,9 +27,14 @@ interface WorkAdventureApi { restorePlayerControls(): void; displayBubble(): void; removeBubble(): void; - getGameState(): Promise + getMapUrl(): Promise; + getUuid(): Promise; + getRoomId(): Promise; + getStartLayerName(): Promise; - onMoveEvent(callback: (moveEvent: HasMovedEvent) => void): void + + onPlayerMove(callback: (playerMovedEvent: HasPlayerMovedEvent) => void): void + onDataLayerChange(callback: (dataLayerChangedEvent: HasDataLayerChangedEvent) => void): void } declare global { @@ -84,41 +90,75 @@ function uuidv4() { return v.toString(16); }); } - -const stateResolvers: Array<(event: GameStateEvent) => void> = [] - -const callbacks: { [type: string]: HasMovedEventCallback | ((arg?: HasMovedEvent | never) => void) } = {} - - -function postToParent(content: IframeEvent) { - window.parent.postMessage(content, "*") -} -let moveEventUuid: string | undefined; - -window.WA = { - - onMoveEvent(callback: HasMovedEventCallback): void { - moveEventUuid = uuidv4(); - callbacks[moveEventUuid] = callback; - postToParent({ - type: "enableMoveEvents", - data: undefined - }) - - window.parent.postMessage({ - type: "enable" - }, "*") - }, - - getGameState() { +function getGameState(): Promise { + if (immutableData) { + return Promise.resolve(immutableData); + } + else { return new Promise((resolver, thrower) => { stateResolvers.push(resolver); window.parent.postMessage({ type: "getState" }, "*") }) + } +} + +const stateResolvers: Array<(event: GameStateEvent) => void> = [] +let immutableData: GameStateEvent; + +const callbackPlayerMoved: { [type: string]: HasPlayerMovedEventCallback | ((arg?: HasPlayerMovedEvent | never) => void) } = {} +const callbackDataLayerChanged: { [type: string]: HasDataLayerChangedEventCallback | ((arg?: HasDataLayerChangedEvent | never) => void) } = {} + + +function postToParent(content: IframeEvent) { + window.parent.postMessage(content, "*") +} +let playerUuid: string | undefined; + +window.WA = { + + onPlayerMove(callback: HasPlayerMovedEventCallback): void { + playerUuid = uuidv4(); + callbackPlayerMoved[playerUuid] = callback; + postToParent({ + type: "onPlayerMove", + data: undefined + }) }, + onDataLayerChange(callback: HasDataLayerChangedEventCallback): void { + callbackDataLayerChanged['test'] = callback; + postToParent({ + type : "onDataLayerChange", + data: undefined + }) + }, + + + getMapUrl() { + return getGameState().then((res) => { + return res.mapUrl; + }) + }, + + getUuid() { + return getGameState().then((res) => { + return res.uuid; + }) + }, + + getRoomId() { + return getGameState().then((res) => { + return res.roomId; + }) + }, + + getStartLayerName() { + return getGameState().then((res) => { + return res.startLayerName; + }) + }, /** * Send a message in the chat. @@ -273,8 +313,11 @@ window.addEventListener('message', message => { stateResolvers.forEach(resolver => { resolver(payloadData); }) - } else if (payload.type == "hasMovedEvent" && isHasMovedEvent(payloadData) && moveEventUuid) { - callbacks[moveEventUuid](payloadData) + immutableData = payloadData; + } else if (payload.type == "hasPlayerMoved" && isHasPlayerMovedEvent(payloadData) && playerUuid) { + callbackPlayerMoved[playerUuid](payloadData) + } else if (payload.type == "hasDataLayerChanged" && isHasDataLayerChangedEvent(payloadData)) { + callbackDataLayerChanged['test'](payloadData) } } diff --git a/maps/tests/Metadata/map.json b/maps/tests/Metadata/map.json new file mode 100644 index 00000000..8967ed02 --- /dev/null +++ b/maps/tests/Metadata/map.json @@ -0,0 +1,230 @@ +{ "compressionlevel":-1, + "height":10, + "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, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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":1, + "name":"start", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "data":[46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 33, 34, 34, 34, 34, 34, 34, 35, 46, 46, 41, 42, 42, 42, 42, 42, 42, 43, 46, 46, 41, 42, 42, 42, 42, 42, 42, 43, 46, 46, 41, 42, 42, 42, 42, 42, 42, 43, 46, 46, 41, 42, 42, 42, 42, 42, 42, 43, 46, 46, 41, 42, 42, 42, 42, 42, 42, 43, 46, 46, 41, 42, 42, 42, 42, 42, 42, 43, 46, 46, 49, 50, 50, 50, 50, 50, 50, 51, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46], + "height":10, + "id":2, + "name":"bottom", + "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, 52, 52, 52, 0, 0, 0, 0, 0, 0, 0, 52, 52, 52, 0, 0, 0, 0, 0, 0, 0, 52, 52, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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":4, + "name":"metadata", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":5, + "name":"floorLayer", + "objects":[], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }, + { + "data":[1, 2, 2, 2, 2, 2, 2, 2, 2, 3, 9, 0, 0, 0, 0, 0, 0, 0, 0, 11, 9, 0, 0, 0, 0, 0, 0, 0, 0, 11, 9, 0, 0, 0, 0, 0, 0, 0, 0, 11, 9, 0, 0, 0, 0, 0, 0, 0, 0, 11, 9, 0, 0, 0, 0, 0, 0, 0, 0, 11, 9, 0, 0, 0, 0, 0, 0, 0, 0, 11, 9, 0, 0, 0, 0, 0, 0, 0, 0, 11, 9, 0, 0, 0, 0, 0, 0, 0, 0, 11, 17, 18, 18, 18, 18, 18, 18, 18, 18, 19], + "height":10, + "id":3, + "name":"wall", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }], + "nextlayerid":6, + "nextobjectid":1, + "orientation":"orthogonal", + "properties":[ + { + "name":"script", + "type":"string", + "value":"script.js" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":32, + "tilesets":[ + { + "columns":8, + "firstgid":1, + "image":"tileset_dungeon.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"TDungeon", + "spacing":0, + "tilecount":64, + "tileheight":32, + "tiles":[ + { + "id":0, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":2, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":3, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":4, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":8, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":9, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":10, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":11, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":12, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":16, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":17, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":18, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":19, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":20, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }], + "tilewidth":32 + }], + "tilewidth":32, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/maps/tests/Metadata/script.js b/maps/tests/Metadata/script.js new file mode 100644 index 00000000..f3ac255a --- /dev/null +++ b/maps/tests/Metadata/script.js @@ -0,0 +1,9 @@ + + +WA.getMapUrl().then((map) => {console.log('mapUrl : ', map)}); +WA.getUuid().then((uuid) => {console.log('Uuid : ',uuid)}); +WA.getRoomId().then((roomId) => console.log('roomID : ',roomId)); + +WA.listenPositionPlayer(console.log); + + diff --git a/maps/tests/Metadata/tileset_dungeon.png b/maps/tests/Metadata/tileset_dungeon.png new file mode 100644 index 0000000000000000000000000000000000000000..fcac082c33704b31451bc8a58af5982c06586aa6 GIT binary patch literal 9696 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 Date: Tue, 18 May 2021 15:41:16 +0200 Subject: [PATCH 22/46] implementation of DataLayerEvent update GetGameState to add nickname to the returned data update GameMap to separate phaserLayer and mapLayer --- front/src/Api/Events/DataLayerEvent.ts | 7 ++--- front/src/Api/Events/GameStateEvent.ts | 1 + front/src/Api/Events/IframeEvent.ts | 5 ++-- front/src/Api/IframeListener.ts | 34 +++++++++++------------ front/src/Phaser/Game/GameMap.ts | 26 +++++++++++++++++- front/src/Phaser/Game/GameScene.ts | 36 ++++++++++++------------- front/src/Phaser/Game/PlayerMovement.ts | 1 - front/src/Phaser/Map/LayersFlattener.ts | 5 ++-- front/src/iframe_api.ts | 36 ++++++++++++++++--------- maps/tests/Metadata/script.js | 10 +++---- 10 files changed, 94 insertions(+), 67 deletions(-) diff --git a/front/src/Api/Events/DataLayerEvent.ts b/front/src/Api/Events/DataLayerEvent.ts index 8d2ffa23..096d6ef5 100644 --- a/front/src/Api/Events/DataLayerEvent.ts +++ b/front/src/Api/Events/DataLayerEvent.ts @@ -2,7 +2,7 @@ import * as tg from "generic-type-guard"; -export const isHasDataLayerChangedEvent = +export const isDataLayerEvent = new tg.IsInterface().withProperties({ data: tg.isObject }).get(); @@ -10,7 +10,4 @@ export const isHasDataLayerChangedEvent = /** * A message sent from the game to the iFrame when the data of the layers change after the iFrame send a message to the game that it want to listen to the data of the layers */ -export type DataLayerEvent = tg.GuardedType; - - -export type HasDataLayerChangedEventCallback = (event: DataLayerEvent) => void \ No newline at end of file +export type DataLayerEvent = tg.GuardedType; \ No newline at end of file diff --git a/front/src/Api/Events/GameStateEvent.ts b/front/src/Api/Events/GameStateEvent.ts index 418d1ca0..72e40898 100644 --- a/front/src/Api/Events/GameStateEvent.ts +++ b/front/src/Api/Events/GameStateEvent.ts @@ -18,6 +18,7 @@ export const isGameStateEvent = new tg.IsInterface().withProperties({ roomId: tg.isString, mapUrl: tg.isString, + nickname: tg.isUnion(tg.isString, tg.isNull), uuid: tg.isUnion(tg.isString, tg.isUndefined), startLayerName: tg.isUnion(tg.isString, tg.isNull) }).get(); diff --git a/front/src/Api/Events/IframeEvent.ts b/front/src/Api/Events/IframeEvent.ts index 3ba5529f..e267fe90 100644 --- a/front/src/Api/Events/IframeEvent.ts +++ b/front/src/Api/Events/IframeEvent.ts @@ -10,7 +10,7 @@ import type { OpenCoWebSiteEvent } from './OpenCoWebSiteEvent'; import type { OpenPopupEvent } from './OpenPopupEvent'; import type { OpenTabEvent } from './OpenTabEvent'; import type { UserInputChatEvent } from './UserInputChatEvent'; -import type { HasDataLayerChangedEvent } from "./HasDataLayerChangedEvent"; +import type { DataLayerEvent } from "./DataLayerEvent"; import type { LayerEvent } from './LayerEvent'; import type { SetPropertyEvent } from "./setPropertyEvent"; @@ -37,6 +37,7 @@ export type IframeEventMap = { showLayer: LayerEvent hideLayer: LayerEvent setProperty: SetPropertyEvent + getDataLayer: undefined } export interface IframeEvent { type: T; @@ -54,7 +55,7 @@ export interface IframeResponseEventMap { buttonClickedEvent: ButtonClickedEvent gameState: GameStateEvent hasPlayerMoved: HasPlayerMovedEvent - hasDataLayerChanged: HasDataLayerChangedEvent + dataLayer: DataLayerEvent } export interface IframeResponseEvent { type: T; diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index 48441d34..600ff0a6 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -13,11 +13,10 @@ import { IframeEventMap, IframeEvent, IframeResponseEvent, IframeResponseEventMa import type { UserInputChatEvent } from "./Events/UserInputChatEvent"; import { isLayerEvent, LayerEvent } from "./Events/LayerEvent"; import { isSetPropertyEvent, SetPropertyEvent} from "./Events/setPropertyEvent"; -import { GameStateEvent } from './Events/GameStateEvent'; -import { deepFreezeClone as deepFreezeClone } from '../utility'; -import { HasPlayerMovedEvent } from './Events/HasPlayerMovedEvent'; +import type { GameStateEvent } from './Events/GameStateEvent'; +import type { HasPlayerMovedEvent } from './Events/HasPlayerMovedEvent'; import { Math } from 'phaser'; -import { HasDataLayerChangedEvent } from "./Events/HasDataLayerChangedEvent"; +import type { DataLayerEvent } from "./Events/DataLayerEvent"; @@ -72,11 +71,12 @@ class IframeListener { private readonly _gameStateStream: Subject = new Subject(); public readonly gameStateStream = this._gameStateStream.asObservable(); + private readonly _dataLayerChangeStream: Subject = new Subject(); + public readonly dataLayerChangeStream = this._dataLayerChangeStream.asObservable(); private readonly iframes = new Set(); private readonly scripts = new Map(); private sendPlayerMove: boolean = false; - private sendDataLayerChange: boolean = false; init() { window.addEventListener("message", (message: TypedMessageEvent>) => { @@ -138,21 +138,26 @@ class IframeListener { this._gameStateStream.next(); } else if (payload.type == "onPlayerMove") { this.sendPlayerMove = true - } else if (payload.type == "onDataLayerChange") { - this.sendDataLayerChange = true + } else if (payload.type == "getDataLayer") { + this._dataLayerChangeStream.next(); } } - - }, false); } + sendDataLayerEvent(dataLayerEvent: DataLayerEvent) { + this.postMessage({ + 'type' : 'dataLayer', + 'data' : dataLayerEvent + }) + } + sendFrozenGameStateEvent(gameStateEvent: GameStateEvent) { this.postMessage({ 'type': 'gameState', - 'data': gameStateEvent //deepFreezeClone(gameStateEvent) + 'data': gameStateEvent }); } @@ -268,15 +273,6 @@ class IframeListener { } } - hasDataLayerChanged(event: HasDataLayerChangedEvent) { - if (this.sendDataLayerChange) { - this.postMessage({ - 'type' : 'hasDataLayerChanged', - 'data' : event - }); - } - } - sendButtonClickedEvent(popupId: number, buttonId: number): void { this.postMessage({ 'type': 'buttonClickedEvent', diff --git a/front/src/Phaser/Game/GameMap.ts b/front/src/Phaser/Game/GameMap.ts index 0c5d804a..f95bfa0f 100644 --- a/front/src/Phaser/Game/GameMap.ts +++ b/front/src/Phaser/Game/GameMap.ts @@ -1,5 +1,7 @@ import type {ITiledMap, ITiledMapLayer, ITiledMapTileLayer} from "../Map/ITiledMap"; import { flattenGroupLayersMap } from "../Map/LayersFlattener"; +import {iframeListener} from "../../Api/IframeListener"; +import TilemapLayer = Phaser.Tilemaps.TilemapLayer; export type PropertyChangeCallback = (newValue: string | number | boolean | undefined, oldValue: string | number | boolean | undefined, allProps: Map) => void; @@ -12,13 +14,14 @@ export class GameMap { private lastProperties = new Map(); private callbacks = new Map>(); public readonly flatLayers: ITiledMapLayer[]; + public readonly phaserLayers: TilemapLayer[] = []; public constructor(private map: ITiledMap, phaserMap: Phaser.Tilemaps.Tilemap, terrains: Array) { this.flatLayers = flattenGroupLayersMap(map); let depth = -2; for (const layer of this.flatLayers) { if(layer.type === 'tilelayer'){ - layer.phaserLayer = phaserMap.createLayer(layer.name, terrains, 0, 0).setDepth(depth); + this.phaserLayers.push(phaserMap.createLayer(layer.name, terrains, 0, 0).setDepth(depth)); } if (layer.type === 'objectgroup' && layer.name === 'floorLayer') { depth = 10000; @@ -89,6 +92,10 @@ export class GameMap { return properties; } + public getMap(): ITiledMap{ + return this.map; + } + private trigger(propName: string, oldValue: string | number | boolean | undefined, newValue: string | number | boolean | undefined, allProps: Map) { const callbacksArray = this.callbacks.get(propName); if (callbacksArray !== undefined) { @@ -127,4 +134,21 @@ export class GameMap { return undefined; } + public findPhaserLayer(layerName: string): TilemapLayer | undefined { + let i = 0; + let found = false; + while (!found && i { + iframeListener.sendDataLayerEvent({data: this.gameMap.getMap()}); + })) + } private setPropertyLayer(layerName: string, propertyName: string, propertyValue: string | number | boolean | undefined): void { @@ -909,21 +912,21 @@ ${escapedMessage} layer.properties = []; layer.properties.push({name : propertyName, type : typeof propertyValue, value : propertyValue}); return; - } - property.value = propertyValue; + } + property.value = propertyValue; } private setLayerVisibility(layerName: string, visible: boolean): void { - const layer = this.gameMap.findLayer(layerName); - if (layer === undefined) { + const phaserlayer = this.gameMap.findPhaserLayer(layerName); + if (phaserlayer === undefined) { console.warn('Could not find layer "' + layerName + '" when calling WA.hideLayer / WA.showLayer'); return; } - if(layer.type != "tilelayer"){ + if(phaserlayer.type != "tilelayer"){ console.warn('The layer "' + layerName + '" is not a tilelayer. It can not be show/hide'); return; } - layer.phaserLayer?.setVisible(visible); + phaserlayer.setVisible(visible); this.dirty = true; } @@ -1131,18 +1134,15 @@ ${escapedMessage} this.physics.disableUpdate(); this.physicsEnabled = false; //add collision layer - for (const Layer of this.gameMap.flatLayers) { - if (Layer.type == "tilelayer") { - if (Layer.phaserLayer === undefined) { - throw new Error('phaserLayer of layer "' + Layer.name + '" is undefined'); - } - this.physics.add.collider(this.CurrentPlayer, Layer.phaserLayer, (object1: GameObject, object2: GameObject) => { + for (const phaserLayer of this.gameMap.phaserLayers) { + if (phaserLayer.type == "tilelayer") { + this.physics.add.collider(this.CurrentPlayer, phaserLayer, (object1: GameObject, object2: GameObject) => { //this.CurrentPlayer.say("Collision with layer : "+ (object2 as Tile).layer.name) }); - Layer.phaserLayer.setCollisionByProperty({collides: true}); + phaserLayer.setCollisionByProperty({collides: true}); if (DEBUG_MODE) { //debug code to see the collision hitbox of the object in the top layer - Layer.phaserLayer.renderDebug(this.add.graphics(), { + phaserLayer.renderDebug(this.add.graphics(), { tileColor: null, //non-colliding tiles collidingTileColor: new Phaser.Display.Color(243, 134, 48, 200), // Colliding tiles, faceColor: new Phaser.Display.Color(40, 39, 37, 255) // Colliding face edges diff --git a/front/src/Phaser/Game/PlayerMovement.ts b/front/src/Phaser/Game/PlayerMovement.ts index b70124b3..2369b86b 100644 --- a/front/src/Phaser/Game/PlayerMovement.ts +++ b/front/src/Phaser/Game/PlayerMovement.ts @@ -1,4 +1,3 @@ -import type {HasMovedEvent} from "./GameManager"; import { MAX_EXTRAPOLATION_TIME } from "../../Enum/EnvironmentVariable"; import type { PositionInterface } from "../../Connexion/ConnexionModels"; import type { HasPlayerMovedEvent } from '../../Api/Events/HasPlayerMovedEvent'; diff --git a/front/src/Phaser/Map/LayersFlattener.ts b/front/src/Phaser/Map/LayersFlattener.ts index 3ea8a449..c5092779 100644 --- a/front/src/Phaser/Map/LayersFlattener.ts +++ b/front/src/Phaser/Map/LayersFlattener.ts @@ -14,9 +14,8 @@ function flattenGroupLayers(layers : ITiledMapLayer[], prefix : string, flatLaye if (layer.type === 'group') { flattenGroupLayers(layer.layers, prefix + layer.name + '/', flatLayers); } else { - const layerWithNewName = { ...layer }; - layerWithNewName.name = prefix+layerWithNewName.name; - flatLayers.push(layerWithNewName); + layer.name = prefix+layer.name + flatLayers.push(layer); } } } \ No newline at end of file diff --git a/front/src/iframe_api.ts b/front/src/iframe_api.ts index 6734388f..a2fbb70b 100644 --- a/front/src/iframe_api.ts +++ b/front/src/iframe_api.ts @@ -13,7 +13,7 @@ import type { LayerEvent } from "./Api/Events/LayerEvent"; import type { SetPropertyEvent } from "./Api/Events/setPropertyEvent"; import { GameStateEvent, isGameStateEvent } from './Api/Events/GameStateEvent'; import { HasPlayerMovedEvent, HasPlayerMovedEventCallback, isHasPlayerMovedEvent } from './Api/Events/HasPlayerMovedEvent'; -import { HasDataLayerChangedEvent, HasDataLayerChangedEventCallback, isHasDataLayerChangedEvent} from "./Api/Events/HasDataLayerChangedEvent"; +import { DataLayerEvent, isDataLayerEvent } from "./Api/Events/DataLayerEvent"; interface WorkAdventureApi { sendChatMessage(message: string, author: string): void; @@ -40,10 +40,11 @@ interface WorkAdventureApi { getUuid(): Promise; getRoomId(): Promise; getStartLayerName(): Promise; + getNickName(): Promise; onPlayerMove(callback: (playerMovedEvent: HasPlayerMovedEvent) => void): void - onDataLayerChange(callback: (dataLayerChangedEvent: HasDataLayerChangedEvent) => void): void + getDataLayer(): Promise } declare global { @@ -105,7 +106,7 @@ function getGameState(): Promise { } else { return new Promise((resolver, thrower) => { - stateResolvers.push(resolver); + gameStateResolver.push(resolver); window.parent.postMessage({ type: "getState" }, "*") @@ -113,11 +114,11 @@ function getGameState(): Promise { } } -const stateResolvers: Array<(event: GameStateEvent) => void> = [] +const gameStateResolver: Array<(event: GameStateEvent) => void> = [] +const dataLayerResolver: Array<(event: DataLayerEvent) => void> = [] let immutableData: GameStateEvent; const callbackPlayerMoved: { [type: string]: HasPlayerMovedEventCallback | ((arg?: HasPlayerMovedEvent | never) => void) } = {} -const callbackDataLayerChanged: { [type: string]: HasDataLayerChangedEventCallback | ((arg?: HasDataLayerChangedEvent | never) => void) } = {} function postToParent(content: IframeEvent) { @@ -136,14 +137,21 @@ window.WA = { }) }, - onDataLayerChange(callback: HasDataLayerChangedEventCallback): void { - callbackDataLayerChanged['test'] = callback; - postToParent({ - type : "onDataLayerChange", - data: undefined + getDataLayer(): Promise { + return new Promise((resolver, thrower) => { + dataLayerResolver.push(resolver); + postToParent({ + type: "getDataLayer", + data: undefined + }) }) }, + getNickName() { + return getGameState().then((res) => { + return res.nickname; + }) + }, getMapUrl() { return getGameState().then((res) => { @@ -345,14 +353,16 @@ window.addEventListener('message', message => { callback(popup); } } else if (payload.type == "gameState" && isGameStateEvent(payloadData)) { - stateResolvers.forEach(resolver => { + gameStateResolver.forEach(resolver => { resolver(payloadData); }) immutableData = payloadData; } else if (payload.type == "hasPlayerMoved" && isHasPlayerMovedEvent(payloadData) && playerUuid) { callbackPlayerMoved[playerUuid](payloadData) - } else if (payload.type == "hasDataLayerChanged" && isHasDataLayerChangedEvent(payloadData)) { - callbackDataLayerChanged['test'](payloadData) + } else if (payload.type == "dataLayer" && isDataLayerEvent(payloadData)) { + dataLayerResolver.forEach(resolver => { + resolver(payloadData); + }) } } diff --git a/maps/tests/Metadata/script.js b/maps/tests/Metadata/script.js index f3ac255a..c857d783 100644 --- a/maps/tests/Metadata/script.js +++ b/maps/tests/Metadata/script.js @@ -1,9 +1,9 @@ -WA.getMapUrl().then((map) => {console.log('mapUrl : ', map)}); +/*WA.getMapUrl().then((map) => {console.log('mapUrl : ', map)}); WA.getUuid().then((uuid) => {console.log('Uuid : ',uuid)}); -WA.getRoomId().then((roomId) => console.log('roomID : ',roomId)); - -WA.listenPositionPlayer(console.log); - +WA.getRoomId().then((roomId) => console.log('roomID : ',roomId));*/ +//WA.onPlayerMove(console.log); +WA.setProperty('metadata', 'openWebsite', 'https://fr.wikipedia.org/'); +WA.getDataLayer().then((data) => {console.log('data 1 : ', data)}); \ No newline at end of file From b509471140c10126e5c6864f98ed7e22e4543b10 Mon Sep 17 00:00:00 2001 From: GRL Date: Tue, 18 May 2021 17:05:16 +0200 Subject: [PATCH 23/46] documentation documentation of onPlayerMove documentation of getMap documentation of getGameState --- docs/maps/api-reference.md | 94 ++++++++++++++++++++++++++++++++++++++ front/src/iframe_api.ts | 34 ++++++++------ 2 files changed, 115 insertions(+), 13 deletions(-) diff --git a/docs/maps/api-reference.md b/docs/maps/api-reference.md index 6e98dfb5..8eb00397 100644 --- a/docs/maps/api-reference.md +++ b/docs/maps/api-reference.md @@ -260,7 +260,101 @@ WA.setProperty(layerName : string, propertyName : string, propertyValue : string Set the value of the "propertyName" property of the layer "layerName" at "propertyValue". If the property doesn't exist, create the property "propertyName" and set the value of the property at "propertyValue". +Example : + ```javascript WA.setProperty('wikiLayer', 'openWebsite', 'https://www.wikipedia.org/'); ``` +### Listen player movement + +``` +onPlayerMove(callback: HasPlayerMovedEventCallback): void; +``` +Listens to the movement of the current user and calls the callback. Send a event when current user stop moving, change direction and every 200ms when moving in the same direction. + +The event has the following attributes : +* **moving (boolean):** **true** when the current player is moving, **false** otherwise. +* **direction (string):** **"right"** | **"left"** | **"down"** | **"top"** the direction where the current player is moving. +* **x (number):** coordinate X of the current player. +* **y (number):** coordinate Y of the current player. + +**callback:** the function that will be called when the current player is moving. It contains the event. + +Exemple : +```javascript +WA.onPlayerMove(console.log); +``` + +### Getting the map + +``` +getMap(): Promise +``` + +Return a promise of an ITiledMap that contains the JSON file of the map plus the property set by a script. + +Example : +```javascript +WA.getMap().then((data) => console.log(data.layers)); +``` + +### Getting the url of the JSON file map + +``` +getMapUrl(): Promise +``` + +Return a promise of the url of the JSON file map. + +Example : +```javascript +WA.getMapUrl().then((mapUrl) => {console.log(mapUrl)}); +``` + +### Getting the roomID +``` +getRoomId(): Promise +``` +Return a promise of the ID of the current room. + +Example : +```javascript +WA.getRoomId().then((roomId) => console.log(roomId)); +``` + +### Getting the UUID of the current user +``` +getUuid(): Promise +``` +Return a promise of the ID of the current user. + +Example : +```javascript +WA.getUuid().then((uuid) => {console.log(uuid)}); +``` + +### Getting the nickname of the current user +``` +getNickName(): Promise +``` +Return a promise of the nickname of the current user. + +Example : +```javascript +WA.getNickName().then((nickname) => {console.log(nickname)}); +``` + +### Getting the name of the layer where the current user started (if other than start) +``` +getStartLayerName(): Promise +``` +Return a promise of the name of the layer where the current user started if the name is different than "start". + +Example : +```javascript +WA.getStartLayerName().then((starLayerName) => {console.log(starLayerName)}); +``` + + + diff --git a/front/src/iframe_api.ts b/front/src/iframe_api.ts index a2fbb70b..4fdb0a03 100644 --- a/front/src/iframe_api.ts +++ b/front/src/iframe_api.ts @@ -14,6 +14,7 @@ import type { SetPropertyEvent } from "./Api/Events/setPropertyEvent"; import { GameStateEvent, isGameStateEvent } from './Api/Events/GameStateEvent'; import { HasPlayerMovedEvent, HasPlayerMovedEventCallback, isHasPlayerMovedEvent } from './Api/Events/HasPlayerMovedEvent'; import { DataLayerEvent, isDataLayerEvent } from "./Api/Events/DataLayerEvent"; +import type {ITiledMap} from "./Phaser/Map/ITiledMap"; interface WorkAdventureApi { sendChatMessage(message: string, author: string): void; @@ -44,7 +45,7 @@ interface WorkAdventureApi { onPlayerMove(callback: (playerMovedEvent: HasPlayerMovedEvent) => void): void - getDataLayer(): Promise + getMap(): Promise } declare global { @@ -114,6 +115,16 @@ function getGameState(): Promise { } } +function getDataLayer(): Promise { + return new Promise((resolver, thrower) => { + dataLayerResolver.push(resolver); + postToParent({ + type: "getDataLayer", + data: undefined + }) + }) +} + const gameStateResolver: Array<(event: GameStateEvent) => void> = [] const dataLayerResolver: Array<(event: DataLayerEvent) => void> = [] let immutableData: GameStateEvent; @@ -137,41 +148,38 @@ window.WA = { }) }, - getDataLayer(): Promise { - return new Promise((resolver, thrower) => { - dataLayerResolver.push(resolver); - postToParent({ - type: "getDataLayer", - data: undefined - }) + + getMap(): Promise { + return getDataLayer().then((res) => { + return res.data as ITiledMap; }) }, - getNickName() { + getNickName(): Promise { return getGameState().then((res) => { return res.nickname; }) }, - getMapUrl() { + getMapUrl(): Promise { return getGameState().then((res) => { return res.mapUrl; }) }, - getUuid() { + getUuid(): Promise { return getGameState().then((res) => { return res.uuid; }) }, - getRoomId() { + getRoomId(): Promise { return getGameState().then((res) => { return res.roomId; }) }, - getStartLayerName() { + getStartLayerName(): Promise { return getGameState().then((res) => { return res.startLayerName; }) From 96545c618a3a6fb71db728017ce868d80e28cf01 Mon Sep 17 00:00:00 2001 From: GRL Date: Thu, 20 May 2021 08:58:05 +0200 Subject: [PATCH 24/46] Adding maps for test metadata Documentation of metadata functions/methods --- docs/maps/api-reference.md | 14 +- maps/tests/Metadata/customMenu.html | 15 + maps/tests/Metadata/customMenu.json | 279 ++++++++++++++++++ maps/tests/Metadata/floortileset.png | Bin 0 -> 81856 bytes maps/tests/Metadata/getGameState.html | 42 +++ maps/tests/Metadata/getGameState.json | 279 ++++++++++++++++++ maps/tests/Metadata/getGameState2.html | 40 +++ maps/tests/Metadata/getGameState2.json | 273 +++++++++++++++++ maps/tests/Metadata/playerMove.html | 12 + maps/tests/Metadata/playerMove.json | 254 ++++++++++++++++ maps/tests/Metadata/script.js | 9 - maps/tests/Metadata/setProperty.html | 12 + maps/tests/Metadata/setProperty.json | 266 +++++++++++++++++ maps/tests/Metadata/showHideLayer.html | 21 ++ .../Metadata/{map.json => showHideLayer.json} | 84 ++++-- maps/tests/iframe.html | 30 +- 16 files changed, 1571 insertions(+), 59 deletions(-) create mode 100644 maps/tests/Metadata/customMenu.html create mode 100644 maps/tests/Metadata/customMenu.json create mode 100644 maps/tests/Metadata/floortileset.png create mode 100644 maps/tests/Metadata/getGameState.html create mode 100644 maps/tests/Metadata/getGameState.json create mode 100644 maps/tests/Metadata/getGameState2.html create mode 100644 maps/tests/Metadata/getGameState2.json create mode 100644 maps/tests/Metadata/playerMove.html create mode 100644 maps/tests/Metadata/playerMove.json delete mode 100644 maps/tests/Metadata/script.js create mode 100644 maps/tests/Metadata/setProperty.html create mode 100644 maps/tests/Metadata/setProperty.json create mode 100644 maps/tests/Metadata/showHideLayer.html rename maps/tests/Metadata/{map.json => showHideLayer.json} (70%) diff --git a/docs/maps/api-reference.md b/docs/maps/api-reference.md index 8eb00397..01d3e636 100644 --- a/docs/maps/api-reference.md +++ b/docs/maps/api-reference.md @@ -356,5 +356,17 @@ Example : WA.getStartLayerName().then((starLayerName) => {console.log(starLayerName)}); ``` +### Add a custom menu +``` +registerMenuCommand(commandDescriptor: string, callback: (commandDescriptor: string) => void) +``` +Add a custom menu named "commandDescriptor" in the menu that call the callback when clicked. - +Example : +```javascript +let chatbotEnabled = false +WA.registerMenuCommand('help', () => { + chatbotEnabled = true; + WA.onChatMessage ... +}); +``` diff --git a/maps/tests/Metadata/customMenu.html b/maps/tests/Metadata/customMenu.html new file mode 100644 index 00000000..59f579ba --- /dev/null +++ b/maps/tests/Metadata/customMenu.html @@ -0,0 +1,15 @@ + + + + + + + + + \ No newline at end of file diff --git a/maps/tests/Metadata/customMenu.json b/maps/tests/Metadata/customMenu.json new file mode 100644 index 00000000..49840d0b --- /dev/null +++ b/maps/tests/Metadata/customMenu.json @@ -0,0 +1,279 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":10, + "infinite":false, + "layers":[ + { + "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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":1, + "name":"start", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "data":[33, 34, 34, 34, 34, 34, 34, 34, 34, 35, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 49, 50, 50, 50, 50, 50, 50, 50, 50, 51], + "height":10, + "id":2, + "name":"bottom", + "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, 0, 0, 0, 0, 0, 0, 0, 0, 101, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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":"exit", + "opacity":1, + "properties":[ + { + "name":"exitUrl", + "type":"string", + "value":"showHideLayer.json" + }], + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "data":[0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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":4, + "name":"metadata", + "opacity":1, + "properties":[ + { + "name":"openWebsite", + "type":"string", + "value":"customMenu.html" + }, + { + "name":"openWebsiteAllowApi", + "type":"bool", + "value":true + }], + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":5, + "name":"floorLayer", + "objects":[ + { + "height":217.142414860681, + "id":1, + "name":"", + "rotation":0, + "text": + { + "fontfamily":"Sans Serif", + "pixelsize":9, + "text":"Test : \nWalk on the grass, an iframe open.\nResult : \nOpen the menu, a new sub-menu is displayed.\n\nTest : \nExit the grass\nResult : \nOpen the menu, the submenu has disappeared.\n\nTest : \nClick on the 'HELP' menu.\nResult : \nChat open and a 'HELP' message is displayed.\n\nTest : \nWalk on the red tile then open the menu.\nResult : \nYou have exit the room to another room, the submenu has disappeared.\n", + "wrap":true + }, + "type":"", + "visible":true, + "width":305.097705765524, + "x":15.1244925229277, + "y":103.029937496349 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":7, + "nextobjectid":2, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":32, + "tilesets":[ + { + "columns":8, + "firstgid":1, + "image":"tileset_dungeon.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"TDungeon", + "spacing":0, + "tilecount":64, + "tileheight":32, + "tiles":[ + { + "id":0, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":2, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":3, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":4, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":8, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":9, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":10, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":11, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":12, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":16, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":17, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":18, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":19, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":20, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }], + "tilewidth":32 + }, + { + "columns":8, + "firstgid":65, + "image":"floortileset.png", + "imageheight":288, + "imagewidth":256, + "margin":0, + "name":"Floor", + "spacing":0, + "tilecount":72, + "tileheight":32, + "tilewidth":32 + }], + "tilewidth":32, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/maps/tests/Metadata/floortileset.png b/maps/tests/Metadata/floortileset.png new file mode 100644 index 0000000000000000000000000000000000000000..82de9c908ad4a15eeea48995f758f1f0d95a00ac GIT binary patch literal 81856 zcmV)XK&`)tP)gWyg6Y_MGMJTkT8lJ9dB| zAg)7+;*jFVBhA?67_q;9)L-{VB&tp6u%l-VU*YmqP zZX!)m*a7&+g*n9V;ClwX8IKtWf*_CBT*FstK&R}({MPUF4UC+Dl^`%fP0|SEQr?c+ z!1rWKW*h^*fVp9(*NJ#@9yl%_tk1u_7i_Q zbLI@X-LC9Aj!9qqEiiqu@gBbC_xhequi0fpW~i}&ujSI z%*>4Re{gUhee?V6?QN7wCBX;Y-|O`zIKylC`P8XXXt&$)9sA(#J3Bkl&)nRce6R3Z zt(JV&F|!Ze!+w^RmydmKZ^mX2)oj~DNJlCJL6F#78~AzCS121h>US|14M4J1 zDsjw_`Q6T+*L%L5tAU>XN#*|64}OGyvGQM|+vz?8Kh4>-{jA~3MmG0w-ld={`-=N#dj5nz91i7v zA|E%eGMufgE!k)P)J9mBE?p8hd~yFa8jT6E9S5I?#?Oa`hw@o*f}i=G_qc8P^X=QW zC-3+6_HgmyMVUKcvtJiJ8}}dAi17J68`p{bDE=J>pZ&Q_$A27rojVDG5=+7j6}VC$ zr6_y|I(gxg=4K)@7fUHQDG;wyE}(Imu1dKVP7P6k}uvWX?BSAnFTe-x#gqC&BlyQCi0f?N=}t%nSU% za4=baa)eI{KQ@v~keD?q8R8I{^$5qvrFkJZy17U^jm%&qqY=EMkj>wFDjMJp@b*vmJzUN_(W1+PG&BAX5^f@w}n}l_jW%h;S49p zr`PZ|&Xavm`e5IR^e%k<<#qg>FRlmSu@8=w-?{J!OBw9r;1dS#)9*h1_~VH*!~Lxi z59c0|(ok$;hg=~2%Lyv=nMQ67TE8O@PErYjE;8>`sbTtlU07Z>9L6YBOn)etYYo>h z{5LY2E+&yn&BITLY-aOuGa43TmQea76QVjW62fG@Gg3q%uu?$=A4WV3%;p@Cka)t4 z3~!@yVa8Sv2_sHN7-sV)qKlXg&cSe|vE8KPhG{nbIJKfbr2)cO3RlsKI{3%)|2N|I zj5s+mM5Jgoz|bI7d4!*DzkzRt|0=s5kIbJ4+v{}={5r6*^Zj7ZH)_lX#eC5qZwGJgzK*J2Lpd;Ftbx0CYSXN5i0{7r zSNPqz@5uc4n_d41`usW1MmmRDFk?4r>^=1S6~5VCi1Q@Ku}B2%xG7^_Dz1pMFc|lR zK!=7{lCUOROQSYeUgtgX{0X0Xi2Fy?M{amNV_74!VHI&yt^71i#dIP7MQU;Y=JI-~F1$yH7ZHsFXklSN#Q$mdDh04F5=X*P_{x;| zm$0=Fmg3%V@U=fS!XyDA(K&neEbiUAC;f9xgwe5MSGPS*>^hFKE3ueHepf8fK_7Sj zL_LW4o;CwcBoO3R`QJ;k1)^9Ua3hXnlaafp8u7 zzBhrNl8BJRaA6I9qHUmWxOrp{focZ{2c-hvGYd{tL~Id$hPkJyf zEOz5Sj|V-lv+Y>ftaM6^yZ-{!KDg^Gm=G%BD_Y>4Fmhq*Je+S9P+_z;o^Q9q{b0?7V%yTjl z6(#&(G%(!RtVis9iszrAl^hBY?MH?v8~7csg%@XEL2uN-c4rIMx*y{C+RH{InzjA@ zlIKr&e6PB7cTEv71nHW!!bmU zrlHBeOzdx|Q3)(RG8;HD2XTn!-Qf`^u$PH7B8^yXdj?DK?9>mI`7eg75grB*8sdgy0SNLila}i776FC(r zc`f@;8yXQC2sY4 z{UP|FK3vxm@JSkhsl7n&S3>Yfn-C2epgenjDjUViHVk2SR^O*6PV`MPv6O#(tHV?D zhnV$@x?wnelG;9K7dT?Ni6FHVB`va(&?sV4{72%O#W44K!m|vQkA&0Hu8^7+y9x}E zIbXVr++hiYzgk4;Ob)xN_u(JrNrnvK9%6tV3S}dKw2ioE_6!lL^Vn= zzqX2>j(&vg`E^83_b_f5)$2iouSVa(r+fbPHzIn^?_xGukoh)(2KJ*}JSf~Tu$!pt z*YRIY{3*V%_TS*y%8O5Y{&(-*#mdTxY*IxaZfLcaxSWs8B?xM@S8jV;Bf=61cnzTNH_(Al8MX-4TA=0{lMr%NoYL;DP4(ya3p#ZJYkm>sRi+oOx%3r^ZA_7os39L_^?-r z$bFdXqY@g`2SVk*qp&C@t^San7-9@Khk?OBkEmA#IeD0C; zr#ewK_sn7eo$GCpdUm62S-+*;NnBrf$1Z2jtk-^sGsAOuuJkfK?elLZO|gH>hl^+r zTOy5Ywl;7(zK)hbet#iBaj$};l;X!re~T-f=VWZ2qXN0lUkQ%t`HFDlRKO8oIl3IG?JK zj8uqBIY?U2zuhbjs*FwSZ|Tv_hJ$Mqfshd z5c#>h!<+HfIbQcHP2RM~G zi}hQ#F+NPN@N~t9;DPCXgy#mIKNk7j`TL>W>{J1$;>-mLVi+WW+w48Sx_29`YU>fC zy4ZYLh{yE$hm=s3jPzi*`|j-rz=@tn-iM8yqvy7lL&2=$Z_KK+q>6ssq_q{RRDedY%0#GN~v5{ zjc!kB%4x|#iCEK3nHBq@g`Hee)sKnvlo_sx803)3Wj|~9oR^CCt{$%Jb4Ih@wtKM5CisiJF*oVHdz>3+R7P9o96 zJ&%(XEuD%tVvb4FZd#j3Tw=rB(@bQK#4>INKz)o<+q$yE+HZdMlB1v>_8-vTg|QKd z6X{3b^F0xpJ_xh1={dN%`wMJ+{RYymhxf0)gMYX1?{VPmqJ%1+3O*#8gVx5qU=>4JAXl9^RF0G4Dt_+-ajxBNgivx zU*PTMzke)JeY|;Hbo%M_=lq{48R=ljNAF%22TNTfqXc({w^1x*o63CtYWLfism}?a zp{nyqpFdrT)>e>;<-ePD@b3N_cxV4DGz?kkT-t(fBr&>c(@oRYh~WZ7c?Ns&wh%Z< zQSSWNrp4?}(|E5U&L?e30N99#^qWsx^?x1kl}Qpw)my;(NR-%*x(D6$pnXJ%0LQ9$ zrjh~QyYOAYrYfQunsr_-{7@G{eV1&?eIK_FYM@XZL){QMIM;pjxcha0UP!K-h}bTi zTP$1F;6@Q%^!LnqkL-UNG-M5ikxb0GheAJZ5Smmw5kzO48#%rwHh^)a``f-rIL0*o zBpZ0E`#L@zUdI<3U&CH=AHy4lAYAsb-P(k==%E-Joj)yNKfjIqpk#;(#QM(fTH~e_ zh0WIPZ4QjI0(91fD4Z-CgdSrLJ17{bVN~g()U7@=e~ytHpO$&gNCSzVpc{4YX8Sd~ z(|Yp>NL7%vI`oY=8P#xqd>2Iq>M*D$4RO!AgLKYtkM0<+Bwxi^`xXv!+ZY^<@U6!0 z<3jl|21(z{tBg4OfY37LWNHRS3{s8 zta{m8TLRaQ+*_pq7rq)NUE2=tQ`JVL5w#zLE;Z(0T(M_bKhvh%LOGb~8Pi6LC*7}` zOTvHyUjls!Vdft4ESDA|nMCy^nP(ug#nZKyWI;+I2H6P0I2wpmoMgf?gun>n!hNjn z4q(-@!+vcg4qk#d{vhFRY5+ypyzW&%cc~>fgh2GcVx+o`pX!$V_7;V-*Zi zkdtntcX1_t4sA5?=a>G2fPNfO&5joh`WFO&g|0;@&gL)3+HAykvESLlV0(=H!9MzC zE^igzKqY5HJwL=BEdE<@O>P*1Q8VPFXlC?jon74`g!m{|DBNKoLI? z51@Iaa;@cNp_Ly*CE$0tNeJx4@G0E8b_eCEkKAPs^_d)cR}J@EOL21c6#k*{S0bW6 zv;8@PFjXNmzTsqdch^uW)$oOjUq-1=!g9JII(X6L;Hi)W?m%hv=L-#Kl`L z8q}K?spUbkj)ip5AXNz){dHVB_z-6|U&QQ{D&DRA>{z6ld;c{0wE@O0BUz*+YZNl- z4hh?ecM|1F75-TtuliracsRm(Zw-y&EN(?tu~=L|Kj~p?)@%-oX16?S(ETLOf7w5Q z`+*@?b5G-M?)+!;-|gY0xv%2Q^he^0@e*I`CFA`>Vg&ZWE!4s~Iygiveg2W>KMn6! zZLVMbag7|7O_73gqhU-rr~bz4D0=_8;B&pzD5zHZiCrk;&?@#TUTIXJB0rl;bTraX z4V>zBWK$_Z;cFAm$3%aeLfmPtt)DeA%Z<*`P2~3R%kI~QKO9DxOHQQ*8{sGM?34b~ zkMBt=zSs#;dwtCBjd*Xl`;qI9W&&gqLqh9)Ml5-MlBn?}!KLt7qi=tVTR*yn%5z3U z9eX%EcLrUvI32HzbkD$U#L`dCe9lEB^MJ-!uB~9UI4`2Ds6c+C-Peq;RK$2=jA4F={ECmu)hk#IPKa8=hf?vZReSpR>!$xz z8j<&tBB~oY>v}zi7@Ab0$m5RfvxcuB$I3wUvkRX{u4;fvBQDq1;2!p&@YNH*&m>~j z!0t_4f2!)x3qUyP>I{{#sQBj^gWN1E{hmPG@7+*h6<4O+JQH!>xnDVspIQPChSbMY z{PZ$+Uowdipmm?-I!5iO8lY-NApTMkB5Tmfdq=x;@7TbPqCCE} z@_V>@a}DjE8NK*K4)>FLs2Dx4=#??-#K;?DqFZo0Fbf{#Mf7F}Y>z400jf<2T%i&VX+kJs<~ z2zi54qm2Z%68Qp!(NDQ%rMW-mGmC5@1UQS~4%bv}4BY|vqvv?}^yj;g+jm+9F zdnY7tXq?#m4Bhab zAz&UZ=baL_$OXJjOXQi!>NLF&T}<_+T0U|6fvF=|@_;R0jd$9)_;IKNfv_m&KD`CeY4{lup)QfMOffLIb^tQ1f-3pE-JvN0O|lMh9| z&leh5&(X)NC`OO9~5?9A> z;_JC@W4pH{NWa_Wgue*NBsuT9~*L`&fxXE#y!if1a# zO&j2~51G3@d=>vRi*LHA%TM)1QYqD6G(}SN3>~1l7?mSwPPQ((4M?!5x2fT9p5*SE zO%oKv)hE{biB;bsK9QOt<;e6Ci;IIPu_cM>^Tjn;6(d5DWHA5~_r*<^bKnmQ4C6l$NN=A69@{IWAJBH(Jcek)qT7ftA#W+|m&S0gs zD$;-uLqo*2_BPQQc5$YD4kxRpajtg}N!y@=fw@#zkp!G^JQ9gT*1wdK?8Ao9lZo_= z@(d25J=B6a_J_NuhIK3ymrxnhFrptYp*LUw+sOk-M;7F#5!7SkS17K@&bTf1fOg!% zGFHV_aWLA$cr-SIs)i_xCIKOz=J}V6#E>)3iO-+)4Dz`G&Q#9}{8uYqc?kZ3zbx>1 z-^u(L?Dn@Ff$wfg)mL@jxY=@UQ<2$ikINF&YY9umYBtvfpt`XlhMQ*lS;FTvssbp? zC&AaKKXnbN!SFcv#}O)i=N25%3`I>hmW)JQ6!k@_ex{T|MF`d1-0VR%E1E~LkH4<%pWTIT~4f+F@Cn^;MI&j=}j z1SPGMPa+;Bll2etp@c5G&!6!3OzESas|#EFDN_4VWWa_e2FIIq{Mg!);-*PL1*zr^H+m4~9Qrsq$tDM&XiVAvl%vi?*@n2+B}dlDx=F982m&6udJ$oeO?a?jAH zLG84KT$snz(YsiC`UBkQp2t5=zk^}i#~UmEFAg5;;XC6$#6q|%MABVM4)}^UiZ5>6 z43(fYIZ5}6;*y(!thO07XcE2_4&isx@OA$k2S0SJy~>$&GrQJ%x9Q@zlXC|=-KOtX z!Xkr{vR$DdFVz4E(BYxkv~Ux5ZWfKck4YkcRZ1SYjbXMA(nS z?zy4&$@zCEojZBs=)mJoOyBVLJVm5Gz6~;YtW0#{C?8 z%$l4@q!z$>cw3fWcVPoRzWROqv-&qgg>ct@m<&+zN@8bVF^`*_kMM5k4Q$sQ;FZCb zFqc~t_);k4*8tz$XWBk(T)?OG*}Kn6E#hjG|5bvNJ}G= z*O*QsMI?6#l)~4)n*OVOCk`E1-|-E zoIhc>>(4-)S$|&OdY&x?!hboV8ZjN&hwrTa7#rao+g7#U#mletg>2#LcliGAqj!)ZM(Vf|NYiny0mE_k2pOOZ-wd&*C zFYJTah^*A3jlZk4-sRrvMpaLNtFox!PeX;(UFh0V6kpX4F5&a4_bKwboAq&Gkanck ztKsiBV)3}9;!awyWxAwnS5^VYPTWw#kQhKTilAA5{71x3vwy^6TI~}lkYV!xRMe-A zpOA|)KOnV8$?JU~dSO5{6R6FnvL!m}Q2;LjyY}H|g zGbf&wDS7f5cG5QP_1ExH`IX7~GrTwSY>YbL68^x5+!a5|+9Of(l(D`s8mF*f6kht_|$w~fv49^NjzhQ`G*Ub*$# zA_Zv`p{4w)1Q=Dks#UjSOg?fcJ~Q}C7GnD}dxy={aetMjY6#bzX}RhW{G?f}zY2Wb zqexG}Lv@4jnIi1YU8Qif321>4#hGdNdatJVYF>vM_@vQ*TF`_w7&bOGECFyu|Kr5k z>86H3eQ2My5QrN&Sdch=QYugMN*Rxhf0Hs_FbE)3WhC$?6)C_HA5RdPb^=xlX60Wm zOCDnK54ksSd-ilLHYqG7EQwJ;Yhpr3YOy%z&9z6o4I1Sttnc^Jk+ zAwE=N&fp?8-noy?*&T^uWDG;xG$W~)Ira@V85)9e-pqF3<6l<)TN}BU&6BVWl3h`S z+CdZVop}wHuD)ztudNU#@}~vHts!&O%^2gZ5&z40zVz~E2ET*OWRp&Zi$AITQ;>i9 zSAn@Lf-v2I{y5bxrJX;j25*4bXEFjkpOwNoE8ayi1#cwGKw?X!%@+Y>G`i8-W6Z{^Tk)i z)zu3+$i+rH_iX{uU?TFnB`_Xsv;(k!hpvA)EQ@QAVZtOnd9Q$?mlb@-2tvs#izVK! zpD*RdIHRxBzK*~6;7^gC4aFcyRUsVtsN`yh8)HN}LwsZRU!#{Z(hHW*N)Du|Pt|MS zO5+(^TmJy9;x_hAZR2M3eTj3JTU){V!?&@$aPOfiLSKo#3G0^qY~T|iG&4^{aP@tw zt>Cz(#XPl;=i{0h!PM8TdzLVj6SJC*a#oF3`E|f2erc?R23K($)A0GdO4x4vj2q7E z!e<|@i0?++@;k}<&*1Yrg-6vRRMB54g-&=n3B+w`BvIs7%c2|h`=sU?7pbBoLtFA& z;3G5kXCl5Qp};oTMG)Mx@d0USr2@JThpbE-`Zn}8NwQK~M^R7|8>G5!)+F07iKVEe z&OdPG?>SR`$R=X`ogQAm)#P1VpL-WQe}Gb4l*~F-3MNv=)`yvm|4H-Lx`Z}OnQ;n) zFSYt?2^0IEjTAs~R5r*@BP7*?N?McO36B9nok1J_-Rd9WZ`S_;-NfjaX1>+>3`UJU z23uo%ap^Y^mB*QF!943UXGT0LNkJtef-`cjeWNK=g!ZBxS-*wuDsHX*!p_^NB6O+t zw5SQ66@1;$Cr+FYQJjsU@h>y1apUv4X&2dabN4qr3|b@q{{8!t+EhyF21`^M2>jWec zGR)ZaDC4%Qr^ot$bfMFBV9ySK%`AcvwIq8e<1E!U!ei&J9)~pJ<(}o}%xtUe&mwAc z;j>cqrWtq6AUz2f?|o$cj_cx~`5(pHvn~PqXJsHt0-|Somg*DX>zQYQ`3Mb`3XhN0 zpUF7+K>@FKeqiPk;Pm2YyfXh4Z1o?AN2It^Mzvg(IY>T(`6mR65?|U9mEz38IYe6# zHa}hmP7Yn0lSGilKz^?kex?($^b7%w zz|{;tH*P`m8&vOSQ`-PfDAK;9D5&}rT^EQe(j4ar(1CEWK?JhtYsVKgjxv1u^^}OC`z*fmrmjet+v# z;5U(#Ct{7E4aYv3{DhJVs-HZW^!|) zXMd^oo5HCXkG~adNHreX9H?4E8Q~j8BeCRPE#uJ={U*B0dn zw9gX05g1+=cF;J1$38_&Ohj z?;cp9aVQ#F!TYra!Q^N#_lDz)Fw?4l>jG4SQFp_qsRy8hCp8G}%ZWac>t`+WBF2wJ zyk{N1zz6go4A<}{u0n$lB0Y#ClG!ChN!BS-zaQyZB z-w0n5L!Rpy9EJn*7a3l=0&mBkSX_UV`M1(0s&Pf?8qzDE)_!}9&Dd)MRqV&RsChNX zP863=SP+Bd$hXy)*6^{|3tr5>Do1fyV}~b3!!zM|)VX~;KAd6s;;!Rhj3)Z_Yi zO$wdd#&!5cr9ZBU&{yltAF zYf7vpzPOEgFAZOc_~e-6y1a+*HCxVwuZBVG+hr2UU0t`E=Ef*|?c+H3BnWy?h$5pN zlA@K1-2-^sc!g>B8p%6d(#(x=ThvyP?e zvQ*u9#Dn4;83L#0f4{#jasR{NNFx5&Z{Jf#<+*e!AfpjKem){+(e5e!<5} zxi6UhGQY4!7}@p|p$};CuF^QHvZe4;Rrh2K5Lu_RSZ|pC1QbJVF_K zB^*4

I3QPd|;1KKe+$S7ZA$eEuatqvXL5UkxfEfnnc-#p_jq;8;}+d8i(MQYY8P zui~vMw(IYjll-gJ6OeoTq3FY*80NB5g^c)5?k_Z6&g}CuH((atn_7{c20uv<3T%x& znh02RDoQl;HqA~APO`&5l8G@;bgv|mbMl0ma{i13@DlOAPvQ>DYk#mS#;xI@p`OFY zh_YvJx_DkhW#-54^>(ly>|&|7io79WEu$tJj`ro8zSYvnhvvW2-$p++B4K`B+-8K& zq#D{SyFkdWXqr2@{|kB?pdy?6)9!HUqTe%zM>N!E?{P9J?xw1j1XI4qz_ z0_fS0WrqIFMvE8}yEuL76kZNq5rVMZwN->}4nM@H$~ie}aAYL48E;Nb2>vYLYaKiy zqL$Ytr#y~}PY5d>mZTo{8yjJhqpH}fhBv~{gqUggM0h>;PZ7H{NrjX5H?>L*rc%oBn*09XGqGS z8G6&f5UzBf1r6CWA6btY{-pWjs?+FuiDzbY6rQ>}8r!0va!7#H-N)H6U9`~WjMzJp zLK~hG5bgDNJTgUL>d&kPz_ZS5Wg!v|dIb{6FtjHP=Xu06_3Y4|1uzg_X@Hr=jHJp^ zk`NA!p+v*UskjC~i(yIJTCAlT zXBGe*^+u?cYd963L8srwetTce{NwTJjdIWsDK3J$ zC9;nDT?1-V*QY41#y91l)9}@{#h2y>Xyn{+fkG+~>7J*wKx9@ggIe;5W7F_eDs=NA zG$mPbG<^RP!&iI4G<>%LGYy>zUrq<++3jAz6OlV%MIM73W#4Z4hHrE0B?y5TZ|1>C zXsY3yM2oC(sG%~#Awv0HzrwyhgG8kw`Nc(RLuGtZqn(AN8{@AZ{T$2A{Mt#i8K)#^(Qez zgRa72qlA0CHH725kvx`BuheDzUHI+%AsW$~z+dm&!De;cs4WZddqxBejOrAZ3^EsR z(nvJ9ECR8fuVXP?wnwzvY%DC~=H*$@+Y!Yh5#{G+7tm_9q##SRWXMIOF$rqneR4o+ z$OBK>=z;YNmL>)$;mYPg@o0E0sm4ZTqzFyXTajN8%Uyp(6g85$n}2%!ndn8yM(s3w zQ4Ht$f9-2uoAj-}Yo%+|zukncW-zY?B`O& zAZN`I2n*BRlV+Z`B(a2nM1$A1_JQu9{k=A0?{f<+nrYhrqBIMRN{`Z`#gA&lUnUN9 z&4!-Eyqv=q6wSlTQ);oQ1|^QEBcE?_eQ)X_7v6{k2biuLd=ha8=!!PJ#br%|&=cbdB~?$wK}7ZX~YWtFit{0H!^0 z#|0i87m@1{EzX~FbVK&Vl_%}JWC2`O zilWpc*}V+4?OATkapn!;(~VDVF5JIhkvoo#2*+ge29^Mj`x{|J?yokCzGTm`1f?m? ze^N@zAcGni-2e=}Mv`S{<{281v0Xxxk5DoLphaG)%mijl3_@~k=sqll#R(->eT(na zI3)4@+<5g4&Zd`8%9kY8{-c?L1uQ6rBFtl8+v1%ni2 z26oXVCzYt^hNEy8>fsUqWH7nYG@{?U{ho-*byRWP-~?(*FgmK00yL&ah(K&RBr=nD zkQh*v81}7%z($FpKhC5Ihq357n*I84QPc5J$ul=NNEdPt_R#g(s2Jpk5M@?v!(sED z#I;j(795JD!4QL*o?D^N?H|n zTw+wcg~2NNjXuK7I>KciYx_5GA-!zoUzd!>v%y7-3PY(lL}Emvq!>-DJ?=YK716&6U;9<5$3=ek zbRd@ixN$a*Yi^Y7w79RtMhk|DG(v7qot_4_L4SbWpqF{RvaGgL7f*cJ23iof3lSXD8f$oTb}3;+;P9M;0}sJZX@i(yz;5qTRkVVp6Ipg;hCQ zkoWF{o2VppA&zl2vEw+Cil?)KMUgCMd=y08^RRvA5XITN=;$j?EsOo+)WLZyRF)8q z@<@wj?eA65d#ZzOv12%D9dA{Bh*8tbV=lt(U>ghJ5~?s<+#BF_a1GT#9h=y|g1>~V zr45YDl#2C|5IsAVK#Y*gv?TJ=&LBlQQ=7Icgx@?AIgrG&=NZyj+x1#_e&%zA(Dv|? z-5=s=`CZg5mhh#Ie@6m{CYgn~Rf%pC#$oOsMrOTuTwCltas4M*pIpiGNCZ}5F^&9c zS7CFD)I4tel>oR%sh)vpuZ!-L;|z~>UwIF4YmL@ZqqwV=D`8TkcVDiH)3qbG;{0*V z75QBP;1;Hl{p`REX(i`B>iQJ_MU+TkbLKP4oYm`n5$jq}Q{vMZu|_{JeZOIP6avhm z5v+&Dv{>qnOm7v7x|hY(ig*7=G!a8zoax`O<<^9jWCpu*$|m61oSy9L080RPkC9{r zsa3q6zVG3M%8ZH8QBdn-Onyf6e}5cEVNmMY zm0sN%z<7Fbs!@rL*4)<(#caPrZ%2+0Z$~Yhm_Lcl+6nZBJ?!`P(R}9+^Nj_J=f^0H zOCs(wHo)%&7~C*w$kQR(_gfe@M)2BZEWHx$4ep>`nvv4w!(tCBIE7xJV+cw?Dh)q} z&*SyrdpH%Jm-!Q!DY0-~Bm&Lk0OxX-Fo^qTjt+2R@R1b!V2N)YWuBxq%=$3h$I09| ziDH!O#mE|ddtn=Io%sh``S2CH_N`FPGHk{NSRbz0NVNbVdquDe}39gC_+>P1lLU>f-q*(oVa8&icqacLNWn`XXqZg!>kTz4^u*_D?rameITwi}qu>}8e5Zd4 zCI1B!iuSZzk)+I^XVV3%JNl8$U62ex@7UZxd3b@*vw2)t5@K_(flm1lbLCl-;?o$< zjnN)9QSZ;8&?#fzh(mEBmbie!)DVGtMI>jDEj=t2Yv|aB?4~0YD{$3PCvWVyp+iCdzF%uaM9x;hl5t~6`K;9rW0|? z#=S!$I^``zzqX;^qhu(XH8MISQ&+i(NF-!&cpzt-X4~CNBu%oQ7MsLlO$R7M?!&p zgP@m_=TI1x(CjzGz1OT6Ng&)uZ@+{6;TA52Por$~o?y?2?T3c4&lwW4Yc@u2ghIur zWhWE-#{Sn)>&zi3#n>7?z+$k9&HO!_OwJj-{ZOI{y}l8jYc`jmJ#L8(zZfjbnFCd$ z!_$kfRl18#p^3_V9e=(0r}*;iZ{t$oY0QUpxkeN!2Fj8A6}i(Q#&KaetD5Zn{ke3eh{@+6&YU zFzp^x(OrLYo2nq%w_Bc_%~dxzUh3cs$WiXhh`nibz)h2OZ3wfNB6YyevL{^D~!)m3`$} z&B4zgTLr*;WF0!c5M+xt#r2)%_>e@H*$L9@{688C`5pg`pK!6c+H_-rM-;(|;#NnHNq3 z=&y~?{y0MIWJT7wIkS88EV9hP-zt9_l|okO$Z<_Qu1!k=lStAaI2IwF)ZFz~$w3K< z5&<_!$jy3H?yL3(cRZSG!#15#tbs-vG3(}zxGI2){Hj9ewaVf39_8kohl=-V>8BmT zB@(K3xV09f4;q0a0BGXBPu*pnNCmujCO0qHa4ZU0mV+|0d}o_8D^E$H?vK0z*`389_OVxe zfKz)HQ8v;^5*lP#8KJShgy)uC#A~g;N7^uRF9s5|_f+>KEEP{-p|pr*!AK6tzGU!K z)4F&X23{YBw61aiidj;Y`Bw}o64pG32a=?-*82tCe*XJ{)ago$?a>Ad{lMtV03UDO zz-s;!x@jBl?!SR|_TQQuWgf;MiiR=rj9;&F2 z)o0Z8bCFqHd~U?slbXB!(}ptlMO~l$D>7>k|7gFPrM(0HVxJH`L3s`o>PO5|lgTE2Wl zBq%|U_=z3BaT!X`Pa3q`Xyw@Iw^x-wk@%Ob z|8%6E-Z!1D%WK_|-J0gBu0oAK)M6frta`s9yTW#BaJnu(7fFfGn)|QnhxV;U`sujc zaD9~sl+dXv;KnE@?9d*Q9f^R zNlT8Arp20ieyk<3Q>AuCDT{Gpk(o$bFmoHl59{l?At)*A-8tgksxpwsG7yZ?fa3v@ z0H82$gNDSU5oG@VOdU|-z&dwnKE~D4Kf{;yzlq!^CxjrB0;NDBm^GVv#y;G93+d3T z!(o7?-xO(wqJKVbEBk2GUKR)49B+tg@LchdU2JGrr+pY&Jy`4pxe$N-)SuuBH-1NS z|BcaomMlOsJ;0meAB)R%tG2R!}=^h=;(7JbWqt+xTen7f5mm&XzA2WHtJ{ z5%+sRTi`QISDv9k?s#Z6ZY31SWIx`+JeI_3vytA#mH0Uc9QyN1|3TJe=0=s4MtF;6 zE!rW*zAai(9nFXo_@KFgh23*F`P2&DpLq*04X{Cs-d-Q0Ei<#8L88L}t`^^yXh3?5 z{$TOnit%w{d{>Hs6j6SN8(Z->nnVml#aD(3H5i z<+}9*Ecad_sg~JRL{^KtOIW58<=lQ$0&op|>N(J$CHAL5Q|>&Jkh!VC8Z@Qz(Cd^K zJw#;4{e4pZB%LHAB=9x?7 z+{Eh8()UCZ%Blh-N8XJ z%lm$IhJ#8S9MAjr-@z-DSIs6L3MY(=TI3sI)jyY`d$3?uq{tiluHGFjT zXGYhqid!$Y?c?u*|4T|~rCDX&cTfK#Ui$ghF)A8eGA^QDZR2A33eJQV&?wGG1S4Iw z?{=8KrUQpLNq zpBbcP(3jDJx!5FNIS-2;oI|(Lk^F<(n>P`?8{x&dui&lp#|ST&jav&OH)Ql6#me{; ziunR&%X6YaEE*k-WzSg(Yz~WbKRsm7lZ-a*lgJhQVFQtHgs5!paak_so{pAtiGcf8 z36C~c5>S;ulZf0%K!xLG-?_-F$wit_q(wVaO3>gRmkVnjZVUnclFPf{#I8ZlRWDo* zgh~M#aF0Id~7{A%Z`ck8nVl4eE%p+*RdT=ba)#4k__pUqo-Ls zfaDUyM}b7u1#2@<^M&Ic zEAF_BxsTB@qPu61UnK)(0xp(Uu@&6Mv&%2wuWtPXn(vhH^6b~})8vP?K6By=0Uz%t z*p43Tu$NSxN zoXuUFAdl+r(}*kkPn=Oc+ybp`JqfMAqa4?*%{R^Ur|)->-{t789b#HKptnFdvbypp z@@v0pR8;Qo`row>sH+Aj(z-~`reoBxX|97FB%*FTtxKpL4EOv%RbkYCsoy zH=`8Uh^^mVif>6oPNuK0q7F0=!b$;t_MS!~#(dc)k$8*l_v- z+q+xOGt3L3vO?DXz*IGQg%WTtMIJAvCH6>*l3d`SGux{Wd<@ zdk=&00PXrAN{yoE>Q#TnaPV`et=A1Kqs=!e$TdRjd)w5*(C)UeI)6d}uYPd(FY!Ac z|MxguIFIekb);rp*27ykpBg>i5TpkMBYGb!U_02rbG=t^wf7!QM(1#EyoQ5dPdp*B zrCIc%K3>1`BjgRS8f};v=ZDCz1UOSTV~9-=y+H?ehihUB8V_TcKTCx5+eYP?KQF2v z31y3fB3G3M@ND>!)Now(PFS@u8Bb!;)tIJ9MAoAONwDIRQe;v~wu>-ypYi+s{XGMw zWF(H_MBjFUcXVIs{Yp4oBzL*-H1g|l_JpT_H_Yi*(O(IGA|0Dl+{yg=F5(|YoRk=_ zsSfYvJ}9EA+mwG%>eWyf6BVFBD#p-9=Ag4M2kY}OU?3NW!HMbt zOK%knlCxM3-G0o^?+tsxAts&*iToMUS09~j2S0B81D+2)FXAhY6WjOp4cDv6 zQC;Oi3B6Gt6~89dcLw1cU_&3< z=6tXql)W`NL~s%~fBz{ALL&(nPFdNl;Y$5^sTI)8x1=2T-TXB}uySHxJDHxt;>wC4 z0v+rGw~a*5!{49(PpH4UVi#xR$zdG#$Lms{WT&vf+-9Uktk3TiQOcFX7}p>7aq!>} zSK|*Pr-Q_+TrEi;l-Kcadg-*acO87E-9HX44mu2p0 zFpcW!~q4ls;fmUZplt`D8FWqU?bu z$WQBkoE7yDj_s6LpSbXXBaTj~fVBZQS6*9d9N7;FWhXK)vm-=9dmSYW<_gdTASuY^ zJNn$+gCNubOOaNz9*UwyO<~Ocr0Ak=#m9WNj2R~=pB)=jB7Gny9iOS57o>W* z`~^uDf6%`#_t74)GulSOpOb9K8{Mmte6;MHlkf+6lTflYOXGNY%y0B18&@pY`niA7WuGX2}WwIs=^^@C`mKFDB;#D@={D! z-@b5qSmaf!zaoEE#fBRN4GeOF3D;oE!H(aPR9|LP zp3a{`GN0gf_a^3zczwHfLpD042fAW;-@WiQ+HuR!izW2R9Yli&UqAg#eE0s}pnbcI z+6jBC6(a__2Q7KesLgtR7S9bokDYKEyV0g3A~D66c8qugZvzv}waX5|(Hm0Xm>MhlV%X$?RPtWlj<&Kxk@V|j7e*0{);BC5LHRF_vT zfO2LxrI+uODX>D6n*-o-ay4*14(Z+R6{cGA^}VVzTII+kDtu`rC|6BTH|cTp0w)Au zWH>U7IP%~P_OR}rO#rg>>uCX(LK!?sCp&q!oG(vAbn16J8(v)6H13QeF#^#iE~TNh zkV0}aN$cYrNgzUeItsOuimredF1(X-G{&Y$P|$=Z#rMI<_>pAf3#1ZQR z*n*UDy0KYw68Nq70p2})6N_^zlKDxG0aKD!gHz}j!b4RlNOEE%r4jjlbVqXhKZ@SP z_@%bt`giaz@jswFZsNPEe}@0K`M=`d4gM!wC|aZEWxS!<&_(yTvX_~1Rkb)Ur7NJM3OfF3w zT{(E-1O?f9EfG++9uZlQUSkEK2uG1*k+uBW2asI{t7^#ohHecUJxs+sH>1kk7~0%O zEp5DJCO?Pnpo7%tP4pJ*kM@w{B9xLc8u6^9&^WSaOXy}2qHd6!hjcQIidA-5KmQ;w zB6L*2lHb5?V-s`HBG!Z3D2_@v@b^Uudyu;;wF3L-N)e7TbLWhtaTy=)+!XHyi4spP zJ~=#(SGr%uQu(Av5%-ccoJlW=bVdYcUIz92fq7mZcHYB>#ka6i+rk%zUlV%(PbQ-J zM;|^tKvWG_7){QvH~0VT-e2OC`j_Bu6)-HekiTd);xNI@;)j+A%#ZPBKmQZ_o25TO z&%mK!+4WI>+Vz(->P*~MLzf222*Ts20uPbhy`N>tSoBmgZtza)PqWo}E5cEv$0zPVefam8w zZ`8092c0sr~%KVs16A)nQC z=sdH7|Kr;Kg8y;h-%r+&M2vw}d@vF$6GxwT{gvCPJwS=EFsXd*p+2-9dI7rKju^Tr zfx5{uu0MX-ILE(=bgGAIFpcWtyoSG%Fe!IegP`^`O>A6sMJ%k>T4K!ef;2q@8jH=bGZEUCCqKFqPSVJ`3Ma&`C^Qp z=D&x<-E(La_wl90-xNJQU(DgX?%TK*-$C9Gm)G}xi2J{B71gAU^B=s33%G)hgKK!Z z@ERHy%XsP5*QBO^RPnhm#6o^qqzl&aBPVB`Lc)B9)Y^4+#+$gh^FETf6#EzMW2b%( zgWCymYi72y1El2`gDi^CtH<#9 z)Tx0)a;#SvpfS@Bm!`xUIO>5*09@{`-@6ff%HiF_9Jl_y5(*_2(=V4Wxrjbpdq4?^ zYvtGT4cOHEr~ybSDYzy1-Lh!zi%pW>n9L*9P$GNIJr96n@edmMc&dvwKJ<>{ird^} zEvhKU)#8N8))pmCO@ppjhS(^fG~k z!Hbd~kQES39fF~MsS5Z`DK$!#L8uXkM^#y^&g6;llUDGp;RY&=GQuc_S7*M0#r_IB zqpOcYqxT!BsFfVz!Pto5r5I;BPf6cmFEQ&$(K&C_rKS3cBu%x@lyG2{06W2gosE6e z_GhfI29d-PNG$@HLn1l*@3h~<&FZ_T7Hh)I*YB)jadiRdi4@D*7x2OO=QwC>V|jiF zC+}Snn*onBYmN`FTwaxfOn zf4B4tQ3|18VGEU{r8J(~D@Pe9uEQTTFv~Z+*2_W-tcf5stgE)<_ zVXfe1jq+Ym@&Qt{6>ze!$whoAVrwX{!0|I~?l1sXl=rjx0lwoNWN9Nh9n{)yH-Niw zobP>`Ktuzgnp0Umi^al<=+rcNk@_-GhdS|@;Jgv}A7Jjm zst{5Z)t~{C+*y3#LI8cUrbS6HE{)4L(LIOF;yQNYE%b9OtoF`|QLYlz1X)>=kA%3> z>|k^^MfXx$2oLK7+}yZ<&n>@*?+yNoq!KUBEP{szUGbhntK7u3(Ju_QFG=xI=3>y* z7ZiOg=T~v7@&S@00iFJZ%T>&v(~yo%Qr z{tn|yJw%-t!>tHkh`ufag0>%eEJ*NQD!qy~KX?uK+0f=pFj#ElN!&r$2r%5~;+gxO z$DvV6c;CHv4NXHlPkR?0S^sHdpOymD7^j3oxwCHghlzkKBc|%+z5DmY^{6^E$KvYd zN(5X4XP;W8+cmgNH!V-@_NQsRZlxcW0J!!4T+ZyKD66eRLyGyGp75iRf!YpS31zyP z(R7~^0>G4Ck5zkUnDMP%O3{}YTrB!Ajz@NdsXulqbkhF zW?*SBda~867zqovDgq_HxNrURUKaLCV!-rYTKQECn4|*7M_z$RI-#Lp+?*Dr0A91d55qYD_uhveY*fILm074*<*bw0UurGdYo{XD} zQ?aTKjowS7uNn^d!{*=N+44)+i*}^S&&k3WYnU=bQc`~VLmbq1&^28Dd~zPm-l4e8 z2><(ge=E|*T)2qkg;m_SwgzVN1{F`d8caXFvvC&>+V}DFg{Kks5|oQ23qJ{QHoSt?-PfKl}}@Hza3MDDe3G|lg+jG1k|w~X5> zA6g{qn~ilC;`Hz=PF2sKn3jdJvRv9TjTdp){0Drs@eTaw!S}QEH@a`dh~x7Cnh(0T zFmn-6V~kV&IV&os6|*k&iF^2H{g04qIvD2vWA9CREXlI`zH7sqe0C4_n1`IZv#P7B znqA_c7TF|4NP<8CS_sglAp#O0K!Co1w9rO?7Fy{OXrZM*hzki4MH0=P)U0ArU6s{U zIYee;#_03x{zfy?{hf2q^BWe=BRnSeY*pPvq`TWQvt!rZJ@fyZYs36~i^9iDJB}Hk zXsBMTbiD#SCivI)f^YfS4eeKzW@Bwhn$d$3NXmf~zeRbwHHwA%V z+S^j0#*{H8!tQV9nxz!?Jupol2U6{4%=ob-K&(PCSQ3mX=z&*iDQ2b(EHVi}H53R_ z8}@}+p#gAGGtJ~PFJ+GpyY9d}@ILZY$!AOTfIx{g#4j&d9xgNh4a|*c4seJ1erp4s z2GCAbp11~C)EZ2zftdgsPEK|NG67%#KobU_0uMJV=5T_Y2Ksz&`BS%;FS^B}W!Em- zZo1=5!2JW4%{Ij103zKx>`O~9%-gM*b=U5C={v|>w{_?~?flGr()-tLer3kZjON^v z-Z!#+*py%Nywx%(WDZvjZ%SoSd$;ZSQ=5_vpef1P_4{tL(D$0Z>&@#s?t?oYxL^F@ z=WgydrzF2UduiHz^^1Eh@#lArXWU_D&&{nb3;hREU%9pBBlr37&t2m;N|*PW?(xo- zZp$Zk{^t_om|AK|Ao9u4BRA>|-Ib+|o1dK*ZTq-0I^c*+K%8prWfz&EU zA3v}8*{p+~(glaI*swK}Y?K*HKATV+JtdM$A-vom(zilJN`Bo!f_iKcW zWCRB?Qq9<>g*BaVxBKqLcmAdOt?YN*weD><;}%?#yl@^mu=w}G4oXDPA5YzL!vk+3 zesR;4ZNGR&4Pk6RQ=jhq-2L{{@4Cy`HEEgMZ}hxV)Dbu}LZr1h<6WgCsgdaRj>LtM z9gesB`JMb>Z?$9&Yv}+EUUaR_hZce+#&a7LV zedv}4SEQf7kE%a))mL7mU!g$Bxi8m0bN4%6x_p>RL(bMh)Aj$ja#J^FB<}NG=Ob4w zdy8mw%N<=FcuS*@Enp1U-CS(=1ym78wL-?m;n!4mZKl+)h z6WWhq>HWh?|B2uhJ{kN(ij7vXtL`_Oza`)!%bxUyC&fVV*4uF$AZDPwyaMm$88+}tn-WaaAfPg^A^`ht$7et z^X6<*>%3uZ>Lx!P{f_&~-Ji%F3%}vM=dS0sMe`Cfkfy2r!P!WI_rRAxz_2^e6pzsV zvwz`cevr$KJD-j|@qYddx4qJJ?NQs^yne%N9&WnD&XU``ci`rm3*u`dh}*+yx1H>` z5329D`=c*ix|Rrth#+I-1=MJ}6amjW`A^^dM{f1tv3u0K@20(&`_lbV{B??qez*KR z_w~V-Zl|&4`d;At?%dyUzcu{>=>#;FEV!knYTct$qt8Do{zy{B|8Dy~atq#ivWA|- zGJd!9Z%cESANBv(eUpCWKF)tzzD|TBN}TG9-GE8yQF~`~@Kmhz<3^`r0lD6~*Ux(O#I{QwLGY??e# zl3`LOk$ehy(DK8OgK z_y2Wkf41r_-SN-7=%hJFRi=^^85|81T%lp^dpmi+sZ)Yv{x3hoWpRe`!vs3dDSHJ>$C;z~GlK+LL|HMsS z_cp`I$ldk=!~6`;}M_BR6Hx|6@>rn@t; zw?Mf(a$oL#?vCzyhMQH4M$s)~4+7Gae4ks#o8Izhxt-x7*G&fQC+&YFrNIcszj^8J zxf_jJ(nnw}(^h2CfO4peyW8K9jz<4p`#+Yf1A{J0YlFw`-Oh*NPSUXCYI;L90+IIH zVf*tet)AL}y1t<6f6q+M2;y&XVbN`EZOWSE{XN)!;8xdH-7i1=h3-sM=!T$IyvDzE zH%j?ITh;c)#K*=_f zjWZN=yuze(X+4-|g2j$ow_~e`5CJgo0-%{_w->pIxB-PQ-xN*vs}pIXC85O_%TG~- znqE${nrs-Fq)$mB?uuY(q`9VK!rpl#38}Q}g}nf)mK*o~E2EsfX?YO>eP#SnumZ3K zKn@}RnnA;x1;9UqY8JL7u%FToFvJ9~1_W=-HeLcA(cN#Z&)jtDN2_kQSxTkeadj*j zoHQp;=YoChlOtEnjNIYg!BgE{Q+ct8yVLkUf?mDRkz4l+`b+m0?(6$syUvmq7>~V0 zaYKFI&Aknm9C_cl({byM*WAskw`BY9_3jhbSe=r1!yo4Vw+P#*1n`IVrSCp*mzS=1 z!80ornTMl2?-F=znhpgtG&P!$Qr%%TkjlNQb2r_mvp;nYpFDD%1#g&d&Wg)2?`U_H zO55)%H!iy$B!u_LrZlS&8J{x}GKAig4YfZ8vyKW&_lHYBC@%*3)Kgz!68bwo5 z$Lt#Q7e_ymUJ320<1RO^1uf=HU~gHn^*RFtBlqI@%sh_s@G-_DHmgq>YF6&f?RNy! z!y=odC!fC$@djK>dJnJ#`-T@3BmaE~T-~3yIJY=P2b#|fb9#hB4!S1Gni?A_I){RA3GXWIEDd~#kKEuUaU z1@I`x;jc<9yGXBGUYeLGNeFrjyNamtFBeSzAZ+-Vg}kUpRew*8$%_m%+)dADt#&Jb zG7`7wea~JuH2QEV46H^2X60`*J{y2i0mwU`@MmvZONS8v$NT>8fd&h$zo66y4E1{6 z6FmUfhmdMPDiHT~)CJ&+fq^AZf*{Kh_x<$Wa?RPcI~pFkueR>G`PQOanYk>_-}8>| z<=M;LpKiEI)n(V3ZhI@HCofk5Tiyhpsb>8B)Uvx7x|Hgmdc8+&k*FQrw zUC8Dmx7~aG8=rRjZdcp|;K<(@{*Eh-O1D{UxSLaV+{W>m>(S{sBZr!JAAHgMX8Aq0 zf4t|u+P^F2KLubP6yKF?$NSz|+U{*hNjM7-GrSqLQ~zr33n>_TcmBii`!}s==5TBs zSaA%)u<}>}vZ+RP5}~IzfMg2%F|4Yqm#?^K?lc6vVfVJKyKxZCDgf&_ z^g5%(X1sIm8EqXi@uf?b#^=P=K%Bl~*t>{@Lku0Z``Y>Ss>XWI&`f_TT(g*f@ag}w z{@=SdIhOv;lsovT*OYs;_RRaIT=0MWk8hOz8y#Li{CxBiY0&9nNW{@8=MPVrf9OLf z9moB5^5>8K@Y!p-lj+;|(dHkW{9);GyJk1K#^9FmIE;Sx*Z%m?pN#+f&j$asTXxsz z;p6}DC8b^zd@P^Q!M~gI+?=-*_K#J2fbXcn{dSL)FbfK6g6sli#Vo*XzZb|+7cmru zV<7ld+wi=C`~*`KQNk`qqCX z#)Z%SS&WK~NrE8^78*=wW+x3cHa0e%I;S3)o;2*7NuQ(rwzjs$3l9y;f)}eQl&i)_ zH%6T5dS*JO9G?Wzu|^(dQNUEpmpOj$!3P&ge^MO5?jKtLu^^g6Z&KiN((}}D783;k z+g+D~(^k^+NB^_ZwZEIXx$m~!{$JMWkh$gL_x8r<*GK!NPO^sL?)cg}RlIg1u zIhB!LVI%=V;jB_J{wy+wuU^$wVl0y7<>iTs`Lq!S?ItZcJ|pkVz+HkSf7{MGu}RFR zqvhhjmOXdO5T`A4qb>X31r1A^uzUCJ2?O4|dGkW)U%7H+oRYNr#vOd)YDdPTU}T+s z{CgcE5epW^GXh}dp2;h&Af=fe!F=*hP@0YP*I=6_lGFB+X| zEtj;aGYf!=oPXsl@N_nOIcJww&mDry^VWOPem;E~T3DN@*e{Nw&@5CIvK8V0k*znp zv@xj*O+09*$#o_GpcnhLR@4(C@apDkoEm$crZc~nIl_GR@85TyefHV6fE>pMf2qIiv5!v zlwUOBEvRg9$GLH10s?Pwnd>C&bDH%~2dEeWGGaeDZVd7~2Ao7EJ{AY*5MJMW^NsuZ z>#xNfVRO!lrXM5r;*?{|=yuOosK&x)674uMQ4e6oR>LdD00?#ofhSE@Jn=8A?ZwxQrRlp?F!9Q`pZydY}*aCAM;%AG0 zuM3G6x&UVx#g_MD{#oD6|LvCaj8C_lln4b1PJkin|NLO$;wv_soBf@p8}$k|*nIIu zjy6usF2uvHx1-Ijo6g&=yxDgPAI`e+uyTXDrKB?#Zq2#!c5l2t{mamQY7TF6{%5dK z(Dv1l*ACOE6umqZ;Y&__oa({@*I?sb4eXE8Q?}shi_I2QY>qnjtTvB*=y-6B0b8uq z6~3=sZ#(Ch#*1mzg-ZO>?mg-E`1vf#&vh`LRnHmoz7GAbzWPe$Vrj#drC(1O8Yn3) z4YvEnDMiMlcO5^Q2{XG-oH4M^UP%DR*4&;EH~;rz*S^$r*>ulM6`3@kpvx`gr0K$B z^Owzn!IqB>PWKNdjTc7O6Qy^w+Lgs0cgH<=aO_sTzo4gu{pm*sgUR>eyU0$`UfUmkB`#V{dZ_crDk&*_xNd;PiM0YlLU6!RX;hjsNet^d082_k;}`RDGV zk3M=O`Z2Bg`{!S5nV8>i$1O%MY3}y7#THHzWX~+otP#o%hbP);>iwoW{_=!jQp{Qw z;kI+V&4J6io}vD*bPMlmn_@P^NKP?j(Te@`myd8gH@Y6*f7bi)YibuyEo;}5h4)WX zG5gbJ)Q9yatH*Ea{GWbEb!W;wue~T-fi<;Y3SFuKZYAzE@+`7MRyXA6UV?2h@dK2`S z->cHMd&HZZTAOsU6zW+3qhtPkoF+^MXl~9k0-!b3aP9AFXWQ-n=g1|6 z_xrBrvI*wy7ZIYwKaX^ZqO{bj=h3cT*n2~lwNkhL4UHF7+q~*{spz`Ak(1|2MC*4& z0E~|PiyW5D+<&z1&fR@)PE5Ydw`Kk>8WPW-(lK#Qm268(>}@JMPrLAG=ASg;0FqpC zN~&wpb+)h!%|`J~-9j)EH}=2l=5DODj0q<_V|>meKRsUbu~{FN%)So&xX30xepdSN zeeD_Q8HVSLxFp(s#%8}+Ua?TO`%BEBe#67B`!1Pkxz>lN_^X$HFz+uMxD+?>i)SCe z0&AGIyLS)N^cp?{uAy5q7xeRr!T409TPT-5xtwr7mq zOD6y8>%%LD(Qct;ekg^0l~HF{#v~>flZ_UgExdRiyLgyY5o^Lp3>YugNuwV0ivPtX zZj7OinQu~c+{@C(wLzwxsN+8U-4$o z7nEuG8wIFqbA=s7`s2>7OFx+P=J&B|pIsjk@H$1SzRY;l==#_^MJvvDEx1>@!>uFN zn9X#rzNC~!naT9CG%tGo)E`u}Sl%m*%(-YUm$TEaEwo8F<)`9kO|NuhfBLwyYcp$k26!>d^;HmfuM=G#Pp)#ZC0XVfotk}i z&0!pD9jbdU!7P6p^H1BMa_dD}U3wRI`t4M=2dfWT4=*Vg&P?Au%jB`CdRofQ&a0cr zc5bX`>~A~I7U)?9EZ({zhGt!P)0kZe1r&kiA$Wy76AHP4EvIyz05Z}Ukh=WL`ofwlg>$G7)AV#i< zX&FIi<34pp=d{Id8gr6aUe~U5e|wUUsGnc|_pg`{0QIfGpGQBY zC9U}bBCzg8oHpvU3X=pvOgqMSFyxzs@v72)f%~cJ_J(fiR@3c2I&!1I$gO;|S-1XYly<=Pmk4?Z)Z?U=n=-d=DQ!90Pm^^jY6O zH2?bgx)e3p_FNQFAp|&%E5LPF2QQ)@Gq43E<2GV18gUCSdj}Tav(84Rr7@qz0YGcP zOY45_`dcGcwF)uG*Z$2J7XM2i@D4sX!P;B>zz@4zE1_H$k7_U8ZE9 z?b+#ggSkK28_C?0R_$6U7KhIQeO_nSKJ>8`e-ZQBar>+u{Hn(Z;`Y^bCN%5AvIoX+ zlg5jlD>lFFXKaesgMW4UFgM^HLkMExBLpnRZgm4-`)}R4B`yI>Pk|JmD$chSUyKJ2 z9=L1Qu02EFe0Zz>w>=N6OVMdUpw85BLlK)xTw@TA7~GXtp+wY7>#`MIcvzR&vi>AG zUv?|E`{R?&6&&k58VN||tOz`*cMSvPuhXv&i>}*dA$l|SMi#y-fHThjZP9<`SThRh z0ND)zPrk$PI*)SyxGUA@%}9Yd*1T?BPcpO5>QmeOVGy)yEEJ4J{hwVkX-u*JCLN3a zjx}TbnmTO|eldNtIu(qV{4l5O7=-4xNW8S!4)2Xp=hf9!xj)sD%zG03_+8blg?d15 z(%fG(>R68Y`^qN;)TZvV+~}ZigKcF7_Mc1E8ZNyuboo>!;F``{TUundE@xfNL#9$`Ctgj6n4bm3r#ExsxWt>J|2Btw=g{w@VjZySlJs$WL(I=G zX02oX8Fe9050Xt+h_cW^@_NU|Y*vrs|;q4#V-umkE zk7Jnrl=|Ac<34Eo_}xvSKj~TH=b7~0>(BkXaato08xx~DY!=aeuj$HnO1J;Xu^Vh@ zYvs_ww)QPi2@ zW#|*&!ves>Cl^#^=Pz2c@x5Or zRTk5$|9e^bFe^n=e2!Z{V~^9G<$3huNKuh~+he6Rv^HS!fKk~=BsoVO~!!slBUVb)? zu9QxgAe05a2^8P!(0}12Cgnq$uh;5H@GA5JuHa?pKQGuP_k0e6KQ9truYhC3TDLA3 zCf(P5pA>gEZA@ZhyY^M-Q`u+F&p%=WuR_0Wg~U<)y174T{%0MNpvsJ#-FV7f{~~4+ zH~ntQbyk{g>yMAz(OS<<9nQO!XYRr?IcaD%`aE3iy1CoalD8i8hORZ;boq@^;sK+F zPRwJGVC1y()E(^}%fd}tXRjoCnejGId~a+dZX8blT=X{P|GWXsGQ}S|M1WyUuJIUuTM*_{MkEG?&!XPc)E|&$EO9=@q?aQ z{0&9G(9oha6!Y4ch|il8JgeXTRq0!5kU(GkoxUu6 zn}=ZlY~R58ye8fMMI#pIF;>8aP(cV3-lW|4qkYltQ!8!Pxz==5r*I3`=H0>Op&NSB zd2l%L7Qm4N`SNz=hDRfJwAyoh@8hHSli(RxYKk;gV=;Auts>g<@69g$i$?KH8VEg@ z?>y#zDt+j%k7KTQ0V6w!z8wSG&H&-|A`nVFEjVeB8}ru#Uh(yO4m}3JG>+NsInQg$ zJ)Ub6T#FgQ7Q1ao!aXg|&LEJ-d9O-8HrZc?e%$4U^AG~30f;93tbb22!`pMj_o}-Z z;~-$Rf-B&yf%`|IwK~f!SA+oH)clk?7#_&JT@n-U7xoW&?(p%kG^P}gO!OSQho(2X zi39Y$4nAM`b{77Hqxh_fFIp$W9GaJ3asEjVptUsPsjbQ<&_~)d7jjqhy+%KNapxgx z2?jIJ+#)tJ>r7oYMeSHUP&mnljjyGR7WBxE8>7K2wtI~+kTJ|@y%vkn;aK_;(LmAZM@F>^O?*Q z-KUwBK%Tl%69m91^auC-d&t7~^Bg70LLi!r5P%sBx@!x?7@^)XKh`qwyxNip1LfnJ9mN6vr_D8$y>~mTxb)tp_`5k}PT;dGV#a8Q(pa(0=O$@rR+; z#8oTN-F}l7Yrg4)DG@62@SN`XCZEc6lSi+~&#FDgD83OI*H7-!`O>%Xvh(lEwq>tB zspt29Q^gq57epY6==4r#PYpK1tU5;j`RB&W8iRG>ion>+u3r=TuyJeadY<24sVwOD zdh0UGKhw5W6myz}jmIQQBxVp8jr+!X58B3@hSz^p`e^d^-h1!0=+^~ky}v@8{v<7L z*VW?{b&I0DY3W!1tp9S`Ext47_BQr4S3h65{Wb6R4~FiQhHMgi_RthJ8jd%l4`wYg2gJRDc^|sxUG_ajx{rPMzFy%$lk81+r0x0`V zVn9+|Sj6<}X}&nkX_kZuUjsdj1pooOoiFy?%oVQ*`-6f!XaSDjZ$0WP^3DRM$cE1l zLNJ3ZP?+SjYCs$DrW!H*I7)6^dk|V|&jk(UY;Ma%(l;QIZADhkB-CAi`geJnfVN?7 z4DUVe?_id}$=%6QpSHggAGiF?4&Km)brT8vn*NLE54)o`p2B3oSxU+l2uxv3E$TVv zSP;}LggCD~$sa}t)B{yteDQ_k(-AnfKcShc3>f7sb2u$9#`T!qFaQyG5PWEP{)Qnh zn!de@*e_>H!cWBS*Zz)k`;+hiX70y*78q96od0*%{@%%#3t}(TvbRy;eG#NTrUT}i zIGtWvC73FhNQr+*_)nw-y?H8-Q>14zXpK_p{T+*d8}r4NUNcyrjYDXX-e0(xDerGK zym{-z(Qs6`<8I|Pw!J3ue($vZjAP6@3v-|9FmWeAuHAhv#r$#3BY2twz|F%aBR4;r z`G1K!?p1DYzjTM*e{MHZ*J^N&)NSw4cRdmMJb#*0uGvW4+UCd|9#w93IvLa7J1E`d zOPM@7!WzM!`=3wN3U}v5?p8Mnd9FOy`#)NHmcF$okTu^31Hm8u=LEEcpt8k1wJ>I3RB=VP5a~fhlM*h^nSY+BrwVFNhM5lX(@Nz zUg?$=GMUe;|NO`chfdow-%uNIz<4n5tY_Ayr*+&N$T^3HmAiQ@m*czpg}+zk9_~dE&GhT2Z6f1wu`X;C{|2{9JBD8pa%>{bedHPK^)=n&b^XqM4z6l0kp?&bd z2SSH`SQltPvkoTF$0w)UHuisChdvBwn)odA(f(KjZ;Czw<}~{DIkfo<;edO@{NoY3 zRHroxwR0wY4qTb{YQogOMqUV4!uVMLtyjX}2!)J0$?vIG6;?T-+U z&snvJXZT7HaDo#ky#z-PV9u1IalW(kzjH?{2NrJ9@{7|G9(YZOFgQHafY*xGxLZS z?m!sTQ*jIPnY+AvLZ7+MOnb&Z4A517UaPI|oLAjyJGbZOEyCbf=Ll{7m6e%zSOt~q z`)5W__^-j=`&s&(R_12rQek`qQ99Nl+G(gLDtS+=Zx$nq4Z+W%psEe#hbhb#ex1R6 zS$N#nKrhg<$~Rn%m_8Ol95{Ou^v&n~Hqu8Z7!Co;!v;d&EU}}r3==lCgAu|80mt9D z;*LYX2C18>loV7EHkJ_6t4j5cDhV4-q#-vR0zgQTkaVVxpGqRt)FZ!2G5b?l!~s;2 zRPuY`g~-kxi;^8wz3B`jnZ=nWVyPT$rNgg#cO) zIb`pIALEAq+)&_!Q4&m0U)O9G;#lm_j!|yBv z%T=Q#&=5v0Qm3=$Sw;?$WgD>c7arB#uR0YFuW#x6p2OX`{lDV;)E zd`U;^DlFWh3P{yz+)@k1XVL3%Wikgc&Vsy691LxoJW-?~{(t=;;hid(_yx%=Dm5G16 z_sDAk5jNgm>zA&(TZ&bZdyV{XeI!Cjp09G^qHqq{TEZPe^|?F#ew|h&mWa{cKU8bu z`qhRAG~x-}z88RAAYfS-eco6){@&Fc7Q;EDTU1>CuFM}8+AX1R72qM;CB3`UGK zcoX!Ejz#uOGha0QiypZJ6)WM+O#F4>U(EN629>NJOn)__ZGn!;MBIZcMBS<+RqzQF zfI*aqIKgyv0oTlZB{txHdd6c>u3S2if(Kq}q9M^xpy4w7T$s>nRMCU!<%tL2Eg<7eY1W72MT4To8uCi*f4BW3tIH=2@_*yf@?6|fu}juP9(Olv8kGUb9*TF zUYhp|e^iL2ad{bkJ{4hAY*!KwU>YC4-w+d=$a||%ioZQS>)h85hw>gaz1F{e1=wfm zU5rXH1w0!J4dd~1x}8X%4`G89c6?k3eamFPtgOE|&*YQnbF3wTu@tla#P97k0V@DE0XdA&;j7r*J;9Hg1~O1ejPZY+VOw6C28_3@Hf?wj^kI5S zQC>8COOKw{fG!P8iOb|g6>3RI|D|Z(hA_Y7`XyAZuD+Q35+W2gJ;V-@EbMP*3pX%% zrT(=j{!BH#U;?B)j+1MNFA%IEzz2hn#=;MVvCyDsFc=E0hyOn}o5^+f@=J@UFetYr zIEbd*!h%>)zkh2-bN*M~&4h8VFs53naS;NT%?O1r?+=7AGe5!I<3cn(^ndY1UzmtY z!>4zL!qknxpyEq6RrnAvgmFpl9eGRR-CMc)^h>n@B%{zKW>E-^pwmi){&1veKCBPu zUtRI$V4jNL@p?i6iT583WWJN=6PQNOnfpeR|I%V6c!PIt=I--*BZp<#@f7pl3OSpk*!s~W&l(|#mr!AczgQT=7G_W9#vdn>^JiBnpM_7^WC zfN#*4nC9wtsWmY>or#&xb(%AG!W`2b4Q>&<`|PWMTlS3B^h|+v+}?vZ)pwqq;rvXZ z`P_$N3d$)Yjn*Bc;;SF}$_zznuoSv+!HE`7nsXcE!b-X~LpCW`1Dr6`y+54V;sfm=Q=-<4mxP);q zwhF)}k;s*#1bP3Ly(Iz=3ihya5 zoJ$B1?^E{%rT6=DclX{v=FW2=j8yZd?&b}(Z0>$DkboXRNdyEz&IgZ&VtL>W@ZEqj z;I+r}hmxB;`66u%@hlb>!GZ#bSf`KAKRvBNgWGfkbMe;K+NtBTeFDyN{HoHA>o@X* zZWT!`a8#Oj>P|T)nqPFe`1M)P>^U?4J7gd70MqS|{Kl%z)K$=Uup&duDdZ;m8Y z$7g8Ry^(58nE29yXQDj~vXvp&Df9LJv3qxxSMuBz6daATpod@|bO~f)3EaAti=}}; z;`vBHl1(xCfPG^6^WLAob|n)bGqmT=!)r~P2$x`3Ia@zEZ`JUeyR?Yg^DOkc~Fl-nJbt zA+iTa71DyG!h?%|39t?J@c#>KUA5LPJVO>zm@_v+C7Ze!RN_g+TEx&2lv~$gBFyZ= zN+atC1?GYOoTuRL3kiM^RnJ5qDF_J$=IJRKBCQ5;u={8v*8=Y))4)GW@>|z4W$40* zJ7SPtiGWlJ_!;s!ZpfUvCI}1ynmj6?B%uDdCS)zpz63A@x8G68kFeX=noOT_xDU^) zH5ZDikhrVt9<0n>2CYwu6u})MO0jY%LaNipiU8hm*gb#q*tFh@nti@)`WH1Wnm!}8 zzVf`$^yd6X^XVyd!?`dgDZ!%D;GSq#zp%?7wW%E#4hN!vn$3n3_ee@mP-L_OFN6U` zQgovlsUhZfUs8gNj2>yk3vw~pmlvC2mSdJ$!3#)3Ok}hl_;$4A;(YGzKOPEmAizv3 zf)_BqoWGPk}P_CQ0C+mf{30N(>tJXp$RtADDrVsP4TuP;u zp<`vwiKaUxb($JpLxY9l4h%G{7q6`(2nMW^_utLj-Fu^n^e=K;Br{y33vki&p9%s> znQxwInQcb(S6c*u6z~Z1Yq5^NeSrp7r4;-i!HZ(381yQ4X|CxkY4zl+lWd~cf6&CE zzH4Xg0NO!tw8Xatl_uu$K!GiMhGs)!!(7UcEaXHq8K%GF(nB>I3mjp=Aizf@9sgks z@YrMrxR*{^4IB`6Xls}pS3s;3PltQM==UFw+=uTpL?|KU5KL0y41Qjan;KIfe$Z^F zx!uULt|60vp4QrP74-;@R<$IT=>Pk-wYrQn9U7U}#{Cg$CUEXQD%^+f=3{f-B`0AS z=r@}x2-L+ik2fz}jDrcoU9IaBV7L;_KEWBV1c%hNy4P{!&ZF4opT~&y%NcCi`3>=jb zxh@JV1=fB5ikT;ZK0W0^fDx^(&MrPS3mxr9@C-&Fx{t5j3mS}N44fFsNp4`X4TW9D2-?Z-B*6sN3$y1^KOjLh%$Ph=qrWP~ zjX@GjoN6d$u#^awiX@cgmU1^`hPD?bhh43kht}b-b36M(xi=;{i@G^V#KfMRO{F%U zwq*za%x}dCs4(Cfu=Q0~*ZBVw1MMCvj-I#x!A8@ZxC^A?9u?|Z;CE91WGFmLcP-XQ zg%LsfVXc70$7(=efJnsO-#;>wipC$Bp#Y*x5bpDPT2Y42ji93x89{;okW%PoC3C?F z(vC*eQ}o-dT%Om=eqsK~>MfpSevWl}>K;(Zc=sE9pU@|6LHz@k#nJJ_#sGdjE^drB z2h~(ks6ZQ0tibA5pT7)Q0BP_Om|ss1AfGfDJD-^QH29F8)ky4yGHj?})q%D~wM$SX z7y}j|2)j+`B!)2?GfvQQbbi&x zI16By{i2|A&qzvqD?tR5((X$bI#m>(R`n9~%TySY*7+w5X|__JXjQ2H*F<$_>}b(z z4wXr$KdQt$|IYg@x8cp@qpnu}0Zu|N0F~8}TkRXzY-qY{G}LxojXZQ63dJ>OT&xXp z>fD!N8yFU?5A#n?YxG_r#eVWlt&GD8poU;$Yv^fe@C{9Uc_o!bqQh`M1j(?dguxLI z5+qGRGt;347NN(vG&Kd>gP%{VL5g*{3VK?l{5VTQ8}ml+$$MfQ1jw_~z~Yt5C-jMl zm@9DF;63c^W0@4*B{`vwV4?Y_eAAUOKVQU(Kla7?uVVneieuaqRDmRg0D6*A6W!zG zkd#5o1kLZXOYk@=Ko^0~c9?tUmNkk5tj5Jc$+&_*?olP7*a-8z&>|oI-=zhj@(NOc z`Oxs|n;JBPv3K?hDHuvZlgv@r_g*^aX4>dN>h}HdVTg`j@#mw}jj1FqFw*RXbo$-U zo&Y>1@Zo+xl>ppOKsk*-TT6doK4Jh;|KCcaph(J(Ls>FRbsU%##se-)vlFZkPI~aA86-c>p3_#qx69mL&LCK z5~xizkV(wpqQtP01Q)!;#~nlC`29EJwWE6T@; zL{Mu|q+<`0SmB?V=$g~7B!veCK!k!*JbN{3hlBmqXE>AW9RP!%=?f!!z9Lf1kW#f6qHUi`;3~_1i*;acZmV`YtW8F-G>1; zfaZpIxPE$Car<-yrZyi_9cCQRsPmG}@A676a9ytb`r$~Md3vFA_v?{F(=omAx$)!Y zyhk-HEy@%3aCIp6T-zM_&r@xRS~+R{kq4J$##NFrPz;p{m?Y#m1t8>~b#AsHu;rLO zf@fnZk$Qoc{*?4SNJ9OCqA(?$Ijv1d(jb_42MCX;sgs?3u3x_{ExPKv%${W-J#X0V zHZTASo>o+J!+G@R(YWg_3`cVe+br{K(PyqOJAc!S$UeVF`i%2#3C8^R9;8*7n(oT3 zNxEIxhOk$_%A)S* zBYw4CtXtcKXt3GYM1;WB=13SAW~OH!Kpg?}^fh=!u6t!U7Y3rBhxPpMiPqNRFL(Qz z@*<*7P?02u6ao#j67VYixfc%Yj?zbP%+2PZYY$qp5P$x-n+i0Vxd=kg(f3a~9gu27 z>(c_Py{#!r1P(n1hGB;uf|;BL{W(9ktzCWIxe(Zs^5Q~EkFhvZxT>E%D-4`U`j3Jp z*|f#aH>PiM!p!G$T&7l2Q;hP)OAV&VV$2c|dX#_D2$aU0H0r!@Anlo!5G#U)0%U>4f?kIH^S%JL?0nYOR)140@f0YJt1J~gjI5qQcQHG9++=y zTQT&DA!2`Y>_qdbP*F4=n#)Rg@vn*M(?<}dg~@RfXbpyTHwF@@E(`up`xwm^UV{&{ z^f(Iv3fzW`O~vzHU1^Hj@x)sjME7M2uw4CTo{OTIMuYO*~wY>sM026Og~3 zolc|wE-FDd*kdD9> z{WnwC=yM;K0xJO~dDHX}Krs5Zjs8W9*lNiuLF!pvUmh)l0KAqi6_j6A191V+_Gzd{ zD+1hAZW8tYR1i@TFaeqgNJGF+lxM1bKUc~1Nr>1Vha#8YDp1`=J8)WHEd;2rwEa#~ zi5YMXT90Zx0)0ftc~r`2p@XlDz!223zdx=6z48zYM{K9nNy0W@%G}VG*g_Ci2pY7$ z>==|Oif`xZfe0SJKirdmCA8QB5RES7!=dx=kj_B2a%~9WpOJh#X*k>wY2rD@i_XLX zpiqczLXD70Y_&84(jRK19xI1tEt|WO>91{#B>TZ!1!d_JP{c799>K(SGS$)j6}T25 zljoU$h0r`OrkSN^Da=$X16d8H)U4azld$adbFCM?_1I(KqkWc^mSksMYmjN(a&6hZA+-7tz2`%a2w{ zWW$ikX;x3Qj$i;po>U=zp!))-J5foAt}N3^wGjd_X*y}{fxpV)RL!m0o~%F_9;ww- zSS))1+TMCBY>I^VX-q@_5ObFtIPL=NzUU2zFcD;8q4oG$Y@{v%Sp)7lJEMg}Fz#3P z6^O*|%&j9((t13~}!`V~}|F-+24#_;Xv(c0Fb;Rt3z(@#W=Z5eVz+>%wd?eU7t%NSdZ^ zj6MU`!|1aM55HsepqcCRc_!NgU9J9GJDkTrCZo!^j~1(o!M6=7tP+cA-JMQiFvq2!v5th~`E+ zkIE`&`LLgzAObZYpl!btJ5YrEFz^zs|y>QgP11r9u&1)!0W zuDLMG40`~2AwXjd%wM|kYI?2`bRGk3uD~PB91u+0+zs28l*u?qNU;u#ML-Vl&R(G3 z2-}I#>g&Vzn*O|jH|?8pFVb#;GW72eEzjiIp+_ISHp0v3@AjPZKM-r7tj1-~2n>V+ z-G&I-$yQ%OHTAg{kY3(1v5Y%68{Vp56VP065C}ztAnwlIp{6Al7wDzv-1ekoIR~w57C?Q(rmZo|X`eCpj%~MHG%eNuObQL#FYi3*!`yrh z&C}>VZ`YnW4et3O`nCnxB>J`kQQR`zwkVGow=VcN*MwcY!KyaHdHNVF!14Q`#dWq7 zSx!Aa7yv*yFs_uohW-5j{0L|#$#qjtQh!M%0l%TQ-E*V}LSWc;!%*Fq7GYQ5P^@DD zszDlLG?1-`GJpMGq^+pgiK#y*!Y)1OxbjW{V6x?x2F1{ZWDD5!*0KXqd+$%4&Qr-= zTw2V9VXy|sm*aDzAsYcMj6mU$5jN}nLIjFM(V-7h5U*; z_Oz%NY&X><(oHu~Z{9lzT5@0Lqk*S0rH>FnQ=`#oYO4JV6z~LnVSIh1^2`#1BpcGy za%s|c+*`CrI}>+E(S(}j!$(@PM+_;^)&5pX}!gv2;VBhnzW*AHEZ2deGR2xbb$ z{%P!|PFmomwNCq-b;N=;7l4HbquIhXZF^qy5kO{&e;epaDlX5*=|bmJXq?tCrQT4b zZUNLShWKyZeceLI?{9ohb}RkpqmSgKyZ(2i4<;L56n+Z-KK$^*aq8f~g9mQ=V0#?Z zga{MMs!Yr~Cj8T%{!}Ened*u*?swgizt8IGs$1LM9`D(H_n&;~y8fIY=9|}`o8D%h z^zD?M3RUNIQ9pK!m;3@mt6x8^hQZP`2v{L~X+bCWLSi$?F$sE~p{zHBXqY;?U}7lYUc zj+L;@1ieeL>=)$c`$r-edj7Nhz^F(59I^|r%96Jl@>Cn0VkB>Q`R_n2egE5$PncxUe@1w53p3B@ zf?xIw)28Mi?2EFCDabSW^I^3A;=lQC$AKx{`3LXamVn=Apu#N;?S5O0OkJ_Uc2qbK zG`KxknvMNJIiqsAg@a2%XJ+CTXpd52IQ-Y^58Y3G`U~%(r~o()jLZDzy*qWar43#X z&NErCp8RKZ5G)6cP8oXo$iDW|8l2Q$Ce z6pJASS*UzwJNL5wz%JO<+kA({K=v*y)?oMHTGp#+LbLXM73te~T$c+I`h|#u;RH+z z%zqdZa0pC`i2;Lu_0?A*Ecly;=0On%SPSM0aZ`>#lyk@{b5HJXfru}^{LLzbw7XP>EbcVQPGTo5#YOGD69D>0q83#shg zf*{alEx}I}fYynnQA!%MlmMzET0&h%AweTrg15cp(Q}X2Hr(3!_SlNq87X6*=lrLF zZ_VPiK$i~m;bP#KFe|_FkO%;NA`E`~;~xv7V<}iZ7b%3chY*ZyKL6djcZCQHkM)3{Fgmyr z7GFRpOnYs!=NTT8e9mhPH2P()f%*H$YaBEWnq;P-mcXoM>=uH`KNlGU;sQ(l9_;OR z%`foJ_71&;&=MiRHCfVh8{Q3=^A-V?Kt{=~f6)jP+zJx_Fu;Omw4T4;^=2+w43m~Z zA{IO5Y(Hpw{$3*G7_5U!{ur*sO@F^%c+Gg#U*7`njqBp?yzc2OIyHwEr}Q)49eHpv zFl18Lqp_JeC8^MnX)Ftng zdvXoT!#jy#zjnQyf6??!gEQU+eTG>MTo3=EpqVeco?8IIOmdj=_U+qpjNk><0fe{+ zfxtt;#KZwC00=?;@c@_}NEapz^IQ5uOmy$}@_-EJU`e#r)jfdu(b}CX6*CsIK20>0 zcGdfvQ-XgTxeag1Pm@L|E3p6w=y6@QP)W+~kB+;-+z0-%p%)UneQi^=;Dx}pzn;J% z3vf`B;~?EfUg+S6pqbEG+umwuQnu@Vzu}(^bC-u%#eKm4lEq(24}Y}ePNC^dOy>KZ z-er7d;uZej#!!8H+z9_B{O1Gro8ilL*4|sPFsY=A+9`$GFPhIlT1^eDgi7n zu6flmxEq-Ig~SX}2^}<>+Uo7N>$U%rP1p4Tk!{nmV21Oo^I3YX^Is5PgBveio2U7x zX4djMYs-$ahFI8^3S(`>5qy5PSe0S>jR7sLWq~z2c9Hbq5$M=wM#p&OP11)ZO4^jR zq$s%%27nnc_YDAoKtPilG7M?K4G;oMfX^XfQpU_0?t^B72*oAhXZYh>k2`?yFjyr2 zx}F(f$Q=S2L^3foF(WZ?DuuY>zwh{CFf>aR|D6CH49xk%stQ>I6&M;QJn@+YaO6KY z_KSL-jZ9q#6oz6YUgT%pzWCH=K7<2iYEfxBvkiY846$3N$qW2|#osR#M=%WM1+=pS z%3u(Qwma>*g7(Ar?UlN(!MG461o?_k%1kOX$%2W`u1$GE(yto%*S!}NWm&lZ-IT~b z0mC97G2Ix0>4GL11z!+^Wna$CWPa)z5!73C?p)_zL{V?npk-oW-`Aqr7O0IOx|UXR zVUBU|HvWteZ_RBnuIgJxpPzQ@ZP7Pt!|p4YFSHO$&p%8LqW1XtFfSLt%-oRUTx{kF zg(C*&U~n!%ffvcOSPuN-zN7{%Q^0wKyOtt|LiY$%@Q`YD%-JpyE)*!tH$u-r=OoB< zBr$=+&9|H0V({iru4d(qU&tC4NW9h1(nUJb7(>fbcz0$>{Ne4vPy|Ig1TjTO`OimT zF?0Q~fRyyP=3oW_bA=1RGJijR&Lyu|5mZF9F_kw%5RlLAcngF%9F@u(g`ebYG8vu$ zR{Q!zi1n2ZB@@v1Laykvp8|X&L0h9!Ss9k2hwtF zdU{F%tH)g;$wLtWBD@oYQlIDi^AOH7^qwt9b1&mES=+ih&d!(_Y>{mXl39cug<}k7 zDPE3oo+0ooC;wNSKKHiR!Sm?D-xi!Vt0T8+B=aE{Fe|62)xPmqLRl1rYsSG_+;ux=_~sW16s zdm$2!6+obT&1>$RgCqC7rFn1u=k6OnmmAGa2pno<-a_KdqG93I2v8ygiy{;~xp6S% zZ_G|fFwCOp*S)*&b#EYxcsevVA^N=?0%r60asKB6|2$ixp8KJ{{@twUzVK%DJMET4 z#2cw*3^EXUo>8|v?R~6}pbeyzTAoM^g;*&(*GOB9A*h;QcAajuhSnCa56>+bf>i!) zw7jq9(y&Kh*miwXgruNa2Q&0rcWUWWp*2t$5S)zfd%*~$^PK--2n@z``R0q8Tglon zkPd_O6#L7Z#~S1q)+Nkhta7E5* z`~UzE+L0*#B{tY>H6)((cHBI-%Ub|6K5&ZTROx({aA{>bwt}`9-cKFLbv2aZ+P2msDQ{v&}SmQK~zm z#aWr8{mQ%y%>&HxDV-oX^8C+{7Jm;gRSc{fdH#3^!cCEg@>RUk{#^oNS zjQD9#=(Ex{7vg#JE$$H0&&?OLTp$-?Q7QNb%}&Ih9R2mFwl^ztVOoR$DL=G!o+x?^fpOVu8Lr{lS;_gQ z0{Xe^&FC9b9e@7FEj8Ps@sCG(Pm3+2;pK_h8~i2AS5_(u{EinM1gY?ip-*58{6HfB zlvcbzA~3e)&mC51#!N5=U;}RWdA;i`6-?+Ae|*C~$05i?e+;u1MqA8J1P{egXk*z= z0cK&}#8Dxddo-ZWLO%Y|lF(Q|_67{8!$~-BS7}>!(uvZuF-U;Y_Iq$Ag&_?5_4A%T zuEsa+1lw+rYLpD?jr9UkSz601Cd+YK^q#*hg>9fS7@G4{d0JebaVPIy20E98yxe0Lq^EWsTf`ER=Wg{p! zjtRnXZhr0BHO(;wd^2Vd@L|hA^71SP`-7zgf#|X@@!w$xa>#cN4o}ik_^(onpeF8Y z@c97@wLwNn9y)b%$oq9$P}G1CHJ~$`dG#m8?(3&_rhI{>lG%^H&m%v+D0E2eG+~1{muNB zEC2yJQW0i;*u>0q=N~u5BqBX%z#)!VA%#gZD~-eo;5ZVLVHHtU1CEffR0vvInpqI3 z^oy1B9g%v>_YoEd;2rM<@Q*@@S7#12~=3b!NVDkOJNbZj%KnhTx zsb%$4B$|OwiobiUnM=f*-}{)ue!*@~x;j!m1oejI`zX=8J&Sv<(ENC})O#YpxF1n+ z7CNpAKR5g{toSdCd(W9ECLFA;0GhuDnqCC~nNRh(T>$>rD1yLHNiMoycUwx-9nh$- zY7DRj3J@x!h)Yw7iFIjlkGwF(%RJBd55v1>U0J@IHDj99EHkrAO+#DI*4$E#qfA&! zalk7E`Iza9Mny@*Yxdhf-=M&!(T{`gMjwogx#ef2kA-0OwSZ%8+88a(O{J(O02MGB zi0mWLi29qUfpo%3fSC~<1}uPpX?+WHSScnGg_flN&1WL|-4Z;%HWnpSHwp`zoc?sC zh`tkaU?l-S79R*df#x>UB4`pk^Pl4`un2n~(lQOA3F?j)R_J~5093I7%oU-rFJ`S2 zExVaC6aN?OwiAvM83!=K{r5vKkvzB{->_l?Q(p=iu3O@AgqCSyN^7@XBIYN>MIri2 zPIoXIYi|OA&7>i1)AZcb#h$yM|3fZVKtKZhRe^F+ETq9j_H~q)fVFcE%xB`{P$1OR zxz3;SQ4g__FzC1mHWMHTwrS5h_j`Tn8q-WxQS4HmH!#Q1B!(`iXyi>NE|99r5{AZQB z=LObPFK}4oq|b0uo=9La5nmc6K@-k<`}qb1`UKVjLw@W{?SbcpnUG@_Gd`d&Wn-3M z-wnq760NCfqUpLM`2XV&X4tVfvj|^er^wz5+UHUI2Z08Q^Ph$$E$VyQ7HUg*9x@~6 zH-Fy!&%(PmlpDiR·k$4oxP(b`-&6yUJEDF{}NUIs5)mm_|z)iC~{&1QL$3^(8J8;X5O!D1|c&pq` zy-z&vuVK+Y4#+kbW53XjMV21pj-0dSpN&*mFZAK#KC8olAm?Dr`6%zJruonfM=Gt8 zRC^r&0HWB3otx8u>z3R;3pWjIz;f+!Bx%Cx1O}9ZBEc+ED3t8joM>+V6)nlwqLA3g zInVz}Q|D^Fv3(Exj?yrH)`F$Dovb<5j%jT^H;ri~vgN^1K-O@>G(6Xt?~OIey8f#| zAFJCk6^87475eaE>@vuzGN0eNl@dHa3PBVXfZx&lT+EBGc!327ZBm?r8$qGZ&wlnZ z2^4Z~f)YH4p`J|oF`385U4iIQ%FDVYnrukEENCkh`vE?rXJGyY@bi0@F9}Vcz-SXv zhg1s^@N0S#bxx}B(%}D(6t^w{B>-le6Su+4;Z4bbSAuibcz|vV&N8LN8Gi1Qu<%J4 z{)dZmaz6etbXfH3{vNT)Fti?nb;Q~-$gZ{AX6^Es_1rU)*UV=_!cztn7sAffi3do{fFObJS5 z-dy$T{$cM}DhgYf0$YZWKWtv}C4cRWW+EvC@q-_D;erVaAjb0HM8G$_K%yr8 zioX`kx0vMt0;@K8JF5g}bGO@<`*3d00(wcNj2u#Rn)Xq=Kh=Tc_0?2O0~#>60(QN8 zo(eoz`ZR`T=B0{lU|4}t@c@D}j9dwQ@5#B(-<&VYP22Z?Qo(eMJK~tni}}mCv=kVF z^~st<(b#bV$=G_fsyH;O1-T7v_idqX!|2DpyU~ZwjK2B*F?~z>8GREVMjyV7{}v0t z;E5JgFp0wNq$prigQxOPR=bzQa`}Ga2+6M5|4-H81xK2EWiu@;@6J{ zqWMYpVNN~#89rM z(-6VGe!$?D(HD<}fr&e;wBW@`Y_OckYjgcU$TE=PLaQOT2#&3W^ci3o>fA|x{q@;t z!M+pm1_aY3ok`Oae{HWiK|G2Xk2KD5soU3pPAZz)qW=~I6dj*vP}78kGdNVGARm(v zXnRAoso(f_?avSOZ*tdwNRT1Y7|G2bG3da#C=V zgzd{)!Kdcm@6FFh%d0KXX3A8w=NLHERv5K3z(A~_!EWZ6EM(8Bk}9M5#ZIW2`>@k; zSG}g2^IrskGW=rY&q`Wj^&`zJb^almEABl^#omWi==fTO7(kL(@t?DDV1k4a33j&e z`iWVO>r;*oh5Z@RYAh(?alW6*@jR?GEJMSqThzMV8q2IT7=UYxX{;c| z^5v{wOF5ecw0p!IcrE{ak@Wd#=slR*FaWPY-!KAo`f+KqVRsBEn42j=Fb^g*fDZmW z14cEJk-;P(Kt_Onh8F+{XqaWr10iT>LQ8uf9577~0M`2X<91UR8qJJ}j^EDjTmEGL z+#w~%dKF9naR6^#VkrztwE|k1M)UcH6|myngH`WBe7<)mnSneA57LP+!F~v2&4wO# z7C<5J#r-&+-)Zl)J{U<#4EkthYT_Sy%?#cECYhCVRyK~5Qv;8AjIaC`hg|{8(98!0 zS`9gVn1n3F^~gs<2;}^fe^2igW8fdLvJy@Gsm~s?xtaKtRYpYIU1N0HwsEZe!{@kO z=RN}pv@c8FAju@J0RtjUnJsh;2k;xS;XF&2?F@B8!~t>9zHfl zEN{s%9)xqTAb$DFUkdY^U&MpTzGVKzA8)M*$cMBYk^7_2A$JtgcL;}$xD%!Lqmt(* z#YcxA(w6=+u7S2)g8(7j$F=Wz0Wui(2QaOpuw#@^78vj6M_n;t4csEToIA~7Pls#) zc8TP<3vGgd76Ud*;|q0H)TgK96-@|}eCRC|m;)v$1hTBmJ7#Kvb(#9)c?f(ur}2Ch z5NY7$6ONVRdXh8j|5#u+>lJ_%a3TS;(8yvQPXmD-OFxFuxz7I}R0FaeSXVHErIBT7 zLAx0i#jxvj?P)OHcwOq`Rf z&mS)ds{jFjL}6n5jvK&3LhJ`W_<^_vz=(+ta2%hXhbN$Dc~moL@Ix+9_9!3}C#Ar& z|LRJ*sij3bXehr3rH_B1*#N5p7>3j!Rseki_Y3v;0h>r)`=M?Gf|(6mxFxZJYK(ZJ z)6iH7Q`P3@`^3`+4tovrtW>vx8h5Ud4mX8rK@e^Iv=V>$q1R|PJrm>K=1J*I;8K_a zT)kNqTY}kF(|Qmz3-7w?trPP1a4l)TX244brh@wahSpYyRX`RX*#6@8@tw)0tD3Z< zI#8R8sF5Wtv}Mq;{S z&ZF^Ra7)(_!iReu)#T9`|KW!6XFH z%=~W$tti!AWFT;JV7KmKm3DW>qiRUxIA zil&3H9`=<%fX?)T^^5gzMI@a8nZhFl%@{4xhj}6`5nV*r}Y*@If z&=TL*NkqKW)D~Gwxpp5ysAQG)(_{A{c$89F7=nNqLa7yU=FpX{E#-k{%7&Zu?|sV8 zJ@=oDNX2>DY#d4tKW*X@eEHzRTkulieVIao%@ttRWO_rf=@YB>HA4)ae^4(K-`}3xLVa4PbDzwE+#d z5VM_=(C${8zK9voC4*O6Z2Ts8zT*8)0N1e<=cOTiLAGdwx& ziKfG}$H%AVAHMvPuKJQNIDcc%JjAyJckZue0n+o2&=~E(+M(D7;jlAMO_deuJMD!a z6k$d)r8?61QrlyJ88{9bSYX})Y=I#pQ059Tfl2CozAS09O8VpRlGi-FP5?6j zqus?+SG11Ifnug*3+y;IUKoPY_l(@`w4JpIO9DOssi&#CLR|x4`b;nbRVWY&Z2?ZQ z04^Ulj=*~#4MBso*^5P~;EvEVQ_S zt$7h7FfhSB7@8ZR#Z9vtMAG1r1|(qN;^SMB4{P}G`RAXHEdV4d%x+tcnJZzbMT7zm z0iXwyem}GXlM1~;bN86QRej^5JsV}gw?+*-GEiYNFP9d9bJL31Ri^V z!op>=)p@;AZsTd2kHrn3(@z1!iwd1z7_oQ2|Fg2Nv02!50MXAFvFAT9DE5RwTQyFM;DGV9#HcoxGm zJOlr%NDp4-GkDUhh?tLa+Ikr_*rb!?8EuCWlQ;$=v?@TmA)niR5fN;Ry7o?e=SeJZeV0K0OwX|-Y!k8qquC7YS0w5r!^UN6Kw_y@ zMdq&#_e4mbci2e?7%?b9UM_;_H~-OpK8~20uLX8}Im(?*8dh#OX4LQw__yTTINEDb zT$A)xJT*KlWuuOVPu9fWmwgdZ-606*uU7L_5gPF6LY~suZXhZn^lg>nT7Q!TYy?79 z0n7o9)B#CZ=whFh41iip1n|~L_j<X%K>~#ckSrkN;KecoLbT4k@+PPEeKHjq2oK97V96jQ zt#XksHQs5pUad807*w+^00eFvSv@Q)EG7e;@E%v!Hr!FSCz^k%(-t3n6toNTgmIZr zdP3~~(gk3$arkVu7P$|fmFLCF*;fg-UHzzk+E`_?_ZcXf5`>)W?XpYPl} z`R>bluez$Iy8-cbR9C%xnfGMgyl3A?Q-3A7hdKnI6Y8V{!4wPu5kcJmf;i9I9(@Qr zar2oC`K?s0`+)|`uQ&TV?^2IrH|-C3qU|2%X#J%X>zJMVqABCLvDFBsNT|B7G9?Xl zRbRM@wRY7VMF1^rr?wN@O8)(X4|v)>T*Y(qnmc96m5AuvdVebBYpAc?UqB@jHTr2p zuDS_T!boW6UY$T6PIV$JREVm-%FXau4SkC2*xc|QD7BM47??l>r9SqLQZRRB)@%;P z)c8wT@OAN3U+M>cl;P`qBHmc=(I;@6s0%g>hK=z2-}Uw@9dipYzs>WlnHCQL&tM?naOf?e*J}|IyenLbAN&o3 z!!^84^Uqxq&w&%PB(NzH*ojPrph;qGu>bi@^D_o1XC|l?z|#O(20bBarb!8(kw(fj z8SW?h2&zkfSp)hTzxt9GT(8_(7eWassUFsxELZB3VQCSGOyz2$Yu&ZN@&MoLTrN%L z7AENnKXjg+IzLPAesq`q>A(6emH7Ix;Q<;S$vAm`v}Do=`r@ZA(B@8=e*VXw zvF7Zws?+^w|F&ZQbU&0E70M^&8MHNRhrgW4HMcC$iT zCsK((k;JAzDyArbkj_B7VZLARHJC}MqP~d8a26t}-j9vE3KK)K$$QMZihZ%QBl z->&no8&t2csk*LDJc>`xjSpKYv6D$u42?Dc=2avPVMGXy>ya0a^Pu8^2w?5nZF8JO zNI_^tbR{A>uM!aqNPtLtMHHIOr}r9k+Mb(x4x*n(u2OBQKa+?z;r zL5KLO6rywKe$hQFUIBY^VgR8uWYNArzgOMCHcF^_!QPCvWm9!43|lAC)Z*_Kd=<@c zJzy%lUhsj`I)}P{w8)9w-(BxP-9$EgweG3~w-?v8kyQz!9iQG0*_B{)5Mw`~=yjcN zcWe?xSP%$24?g2JkQ<1rQVl-iZ#@s!g9zYr90tzCdjr8>VjsR3Lqr67w3N9YB6gUm z6zGPR|4BonRqe499rJ7w1A!>eT0}BrK6Q&YN={7OM^h2m005^4r^fF_)3m9FaTK;A z-){(Ez;^?viGZqAt|0)phh#|j3i%A>`1^nK``@AQ;Q^5(5P1m#(rB7WQz=&E@ZD~) zMg!S&*L$|#kInVt@76~C21=`!w!E z>VjyYjSl4HSc zi?xUxf2V}mp^QIK=n?e2GFm&((jQl~8udXfNF zrA;OPXQoa@`_=Xk=v?;ux9t>>1fWJPe4(67djzTuj&n3AB5v$ZBv4TmoK1skRR7R^ zfLiNJgMbu~bf}^5VZvA6uEMunKaUE&5+1SsQg3lxiaXTm;+EC(4-)n5Og0jB^f-q0y6bTdDp438f@ef>IpE2U z@qq9ei;>tNQ;EPQC&v8^BR36Y17D>Hk2;D%xcMoFKKXtq7PG3bQ>-$=XGK&8BA+-m zN&~qJUAnbNxok?RHiLW5O^?vcrA?~1jjoJM*aqrNQ+NhH8{1_e0D}W*ku1Rd(Ve*& zHow8Kp+S?#1LC8|ZWjhohGR>&HqkEEE3~q=X7-+Iyf3I9DSNG1qg<+EaMyjMfk?VH z6mCK{q*nIDZ##?-L-YLAN~)V(^>HmzjALp=*BEDPI=5bbsG%Y{7y*zRe`C{NjZn1* zoLY&5>h$;xczN8gQWVbBItWB= zgvwDRLs+ni7QYFh+-RBAS1AeU(1ExlK2g%Yxufo}CbPGkRAe zB6D@KOt)5wA^||OKQ&vRVx?{#B~%miUF!Q>T1O)eyiZ?b*s6zee--1^O}e&*62Bd5zX5RY4M37|yihMU#Z7hP zd^Ko=2(LWXC<>~_yAfv>g6fA-6orO9=8BF4Mmgbia0#=BOr3s<$0moUSguitIr;El znkGgDnb^DZ?$vd=yS6LHFO6D!H_WYYX=XepmVM~?MJ5Q4KHyfKXDQ+R>l^gF4=>Ag z)kc$s80iPI304R2&YE<7VM;s&P&;5#IEoG6*5(~LGqFG`yX&m`r)XjPxDdUhG+YTi z2)u5nFBxYNeQf{=On;PG*i=H1K{v08z3N-k4X)Wp;Fj7TQ8zx$0at|2Q>l=OHd>WO zZ}MTnR|L1=>$5*9_}bE0Yzo!}h#T{IiSH_%*e_S5kAslE*WA8xaLfgyhB~GyfO*jU z6XAR%4tO?jD@X(Q+j4*Qxbe>1Y7=N_9f+`niQ`a5!goUs?~`Nk|M2ER{tuU*kxu-; zWIsY3kj5{f5`b{q%~SckItlqrA?F6nuYd2BxsLBGlZFnTq)$IJM=R@j+AW%LUL!*T zw9IprDki%WHiOxzVfyIin&|qCmP2n}*`S+uclf&mjTSPL@G>-z>G*kIM}R5;LYK`X zDV^#Vt`D*SVBFbRCNz&}@=vi*q8~Z)DcY&-F(H7PvLU78@SPXpurichHUJF$ozSEd z&4*T(7xZ8agit@X1_EK|`8Wrg8Now11`%Jw4iyMQWbL7*TuSw6wfake(9r7rFyU+P zhr-ufYeY6aD)_d`QjyW%Q>*K9{BPy{N}TKsVjqhS+inq`Cj@K|ISf#7*;hVx0W=AW8FSD>VL1GaXG(K|>c9GV^)Dwmt=1; z0*?*n>CaxhNl%=eq^YqyZSEMC-`aMC>S3Xq>Cr5W4P|Iyc8vc0E7$3N{U?7ewgV7~ zU-*epnxeZ{CWD>M?S1>hYltAbP&U8ie9;AhnWB7?5jRd)oTk`1M1z!ZB__ zv%cZ;1x6(pVDUVca%iVm7h;l<>ddYf1>xo^Rh!J=+w{gq8=}v*Ld*U2)}4B-xZ9ChdqhHD2MIhY4Q^-S-o|R-Lio z+ag;Wd2vk2Hzg9(Yut+vC;?HGM~PaXoHz(N2&zUt^$Mtwk0m5hkBy#hO9bHX_^iZ4 zJqqUG%y+mRzn7QIZ>+C8uGeMT2Vw_~N&$|PH#0E;KxA{j!iC9;bbE37g&ma*5eu82 zNyU_;W>c7mKqY|u^Y!oDrhFFSxJL^!1@SyAZtl|NZdJ}}Hho6ks{Do_FUu-If%W>C z2}U>;Coy8 zcogt8U)vV>ZK=dQrj#3VAi=7a5wVx#ptcBg|MU_ABjj|#7yxi^aA!qeB?MRp82FZ! zy0|w%^u#lS;m%#gLJbgtYM{YJYIIb>5#3B!VYKSP$XS+g%gL;8*}1g<2lK>NV3@R?v+*H%=1$P`My}L(CCoK!JnQO zqMJ+GBHBYGIL0agTzb#)weNhiOqXtK(8yp~kb7~pN|PgLTG^`6ZmB5}Nu_E+j14h% z#89aAB^z1QKqqeEMwQ0$qjY`!7Tw)lG5d~@Kj9@<=WmLQ11aMzAN3`GCXYfw%#5eaU!5Uaq%#2;nf$d^QwKrHfZ90?XEKsBCBBz$Z8wu zl#AnjRWi^4z2EgZ?m_*n$c^8KuGGjuCyY7_RTzyS5G#FT{4daGPIWYp_KJvLV%}x%} z)!Q44#C0mw+LTEew`Gfwyi#k>$WTrUf~V)lB&%~{rz&DUf_%1%b(wQ|3^g7TR2az6 zrJI}5_!a7ex(>TvM_9Zg4EWh9?=q1}Q?XW}p-h3AepAGJg#E$}kYhy0HJha!CKMyo zV2L1=Om&4;+cpM;q2DDm_FgH1>ckjY5?nius8PXgM1W!1S#_!iDc4bhngUcE+cv-f zK{NnKzkihQHJL|s^hW_-U7u2{A%^TKg~ToWwuiwsH@MRu)CSILk~H=Ait?~Xd1rw;T*0ZP`<`mkfeooR0Q)I{(K-NqWU1} zzxMuJLExE*L8{f;!s+X^rilMrjNDKaaBPs(00i|w4M21s0*VkqJT#D!UI=^T1}&~t zn24A?acda{#iFIltLISHJJ-EgDxvZZtII>o&6{<)x^{ym2ghl%vP}smZ0L%1 z^1fkGtg5X)Yoqt0r6Sm_;n5t5askLP3U(ozic7|@fpMc*8=&~ z?!e4O{rai`;4^}6Jhb7J?)Hjse(3!> z)m?gO_M%w*O~8-wfIu1;$`+{Bs*5oYbK?9$CLiqu%vIICvf zDOH01Ud8lY{mJbVU`=4rN+~+8Hb77ghs*28I6Aat0Y`3V2=1Md2)*0bh*Q9FNAhK-_P~BY#gc|dZAcW%x14h>y z+W-gxqN)I3;2aQuTer+{eBRkHfk68F;s2&AS`Z2wVFX`bkTp>Mebl;?#Hbepor(yZ zH8F-jaW6WlaPD@b#YiAg5SV%k;Y>wF);k-UVyQ>Q9s+fy#sV!mcXD-oSu+-xXltlKAY;=cdCXq zrCx(WJG{mjFH0-CYgG3e)My(O6S_Y<00{VjgwSlE9tP|MD9JZ2OO9h5LT6${vY`>z zRtpk#M-=76wx3!zixw}y!0!_$PDDDqX65MySDjwx!Z8hsu{Sur*H&bTFj_?6VZpb# zzKZdh@bf6)+d&zM#I`tSk9aJA{ade%2Yu){c4VQQ*c6>7KKd{K2pa?;y+D5bUJZo$ zI|ze@;1y6r_kF|ZF@WfHjR5>$P7sOX$4&APqy{7)NmtxZ91RnAoUn_r$^{fg^>tE! z0>jmVj_c6W5(vMOU%EynVo**GI+2zt!QgCU`8lDBF`GeXaF)4qjxOEU6uloIz>plE z?|W|On|ITU&<)xtHEHerW%}lucj)562p?;S%MdO_6sQ>*M309I9iJ%(VO!ZK(_5F< z1l|Zs7oRvYzVDsNyJ8smfk9eaU!nmwMZ0c-)=C?+Fg8c)r7apA7?Ln4arrfhXpSe3xLg|?RPZ2B0%OQ?vb8v+u6R`5{~SM}g~^(VLcqbrhOsCuDg zitLcrc%J*ihOc_LjTDa>z7}eXhegLd0da)3kLx|hgNWirL_6R|OSX3f0RidpgL$wQ z@IGvJqn>9Q_wYTC*te2_RQ%TzVvP~dKp`AMM;s6hHAH$|@^MO6vyrUVHnTq8qd!*vszgBL~Qm23E}&U(Kup}{}=^n94}&xpHO zVotx!-?hYe2XTLHdRPpBTf1eN7)sM#sYW-LtHYLn0HL9Qgm7_0|7p#DnejZ$P7X$o z01txw&LUe=NSsi*9o+uyTkp|@`Lklz7)WPnV{e-VS-kg{vaJ<2=;Y)HDzgL!0wC|X z)u7d#bviz7itWHFkYJS|l}JW=Rwc{?76qFTz*QXAnrOO#QR+)$;W5-ljvH4$x4Ocx z!4VCIGT>TN5$9+o91u&zW#xJbU)7O^311D4HhkQV7BhVm@KqF#8~OBaHPqNDf*l^( zD+p+xXdfQa;4bxQ?DIT;b1UMiQBNcN+8v*{{06a9uYlSlR5j279~#@BdOx^7u7L!g zwgkYyc}f6){EiXJ-7x%D5IpD<@-X%R4FSeaQnL9pC`fuNN;I%>7y98vo|lN;wb2T? zA`S#W@q?;Bmzxc$GN%W3f9%wR_|p;Ve|)+?xwJ=Xn?OGcL(4mBbY=AhT|9P{Dy+igGdY0)gW#R*Wtty7#t5GhcXlF^3MItO-d71XVQ(zm zbaudr8q1W{XY7WNWl9|e5<`F_Gzj{=5<=&B*FSJISyfA)yG z0(-d$^%?p`4uYsb0stXUM0LZ2A9ejHLD0`Q7X$#;f|zU7AVqFlWx)4pR8*UQ5)v^e zLRxBz5zlpe(#kwR(9xwGRRP3Bm=GLu!>T{9yyH3`8jNjDSo%Yy0gWjLB2uDjXIzKR zJT}K%yb`Kk<~xYsnggKsGZq&5n4cM;rL_|M?!S9iq=SWHLsHKXp}p|y!=wO9Ke#`# z{$NN{kv?_$S@P0mzkKcTw`h8BiZaOz zOD`p{31DoLwkgRfLyido2!l~+Tf9!4f;YCRDLWmT7Z&Qc;VGUJV{a(!NrvNUJJ9~< z8eF6WG&J%|5l8>FW94y;?IV9!@KyJ=;cJ=lM+INqg?0j#Rui(N0UH7I<@F*6@nbrt zW;fbyLfvrDHCO{6B#>0={%KaAa%=;<6Az%rH2P01|N3Kx{GtSy5)U;Js<!v?^Rl2po6a; zslJ#QetMMnD&(_b*xTAEi>mO{g<00)_r&FQ@zkj3`?+jV^7;`(gy=tTc-dn-KZ=C_ zhDilZi)sPG;+?f3jSe+wb+a0Y<7&L!-P@z=BhPd|90ojy{V)I`{TBwo)s2$rO!T~lKC;m`gWC(zbUc* z2nxDfaU4ZPdUSF+Hhqjfc=;Pt;JtZ?5gNfhb3+sKiMb2( z)ob6Olw$&Y;0j!>uG5+E6HJUuDEqOIX*s@K+NH_-7_F5yX(2n`S=X4Fm{6otJ=MIc zPFSw$uDW@26D!hc)SJ4)%$_o0u67dAs5sm2t@^hOUnA)rC47z2Q`g_4fN%SJl`x=(&6^%6;O#n zC5Z;Q5HXR8EczjW2CnhhtAP*d0UtmBj_5k!#RAmsSOGBSHXuTmEv5 zr@?eaGWYrV`*-M#5AX2%YSKW~6V5$S$cb?eh<{~qM@0K#HDH}R&BvOwu~VVioo#BP z_rU=2O4@?p$SD}g!`Oz%+LW|T!&qU$@xiqX5$W43T`aAlEt^Lp!`-zf_lmo6&m|@V zbH|R+tM9!*+2J%@oIguD^&)-y+V^N-e2#WXECukIzP0!P%?ysyNPbwJ=l}lme@?&h zgb)ApW;Z&};1)#=O^8v>redukjV3o}Yj6br zfYahQqR`YWW>?yMRPZqmV)YfiO$Z(pd_2FNSC4UJeNlEGkaF;T#6Ss_{T%kSb~z}W zBiazEtLu-7_iDvgWL8eD_5oXzSMlB^6!veZ7MAf<20p_Ugl44?C%1XJuxR1Z4jGZU z(ey*9K$3pNLja;64a@va&j>|+eNG&A0{jSo*yv0kF_v2m6v zHfV}JR~l7%>-u|i?CCGicRqTJPEVeopM3VmLqhLUl^M{fnUhjev7aYN@J{REX=00p zld39kP=9i4`gnhGU33*c?f&GN7Hb}^9U+u97STk7M+sjWmw;10D)>rt?H6O~_iB5v zZ8El~7#H>RTHK?ick8|MnqP@eOWk@(2=snbyjQNS$83FHiGUIWe2-d+@a^lIwk^Z9 zC*WMHwagJjasN^eP-u#A*9pTW;~i+KHQh0xz=3ZR2NQVXhiY0_Xp<<48&IJ{!|stm zh<5BS974T7?Z3TBU4n9&KH8)C{x;_OZ!n+TPs} z;h}tAHMiAP_uH0Yehs$tbK@x*Hws}XY z`gpD{O-~IyGC@BA&Nam^B2`AvUuzZWh9=UiAz1lEQgD;Zy-j&BIW;i(0G<#F61^TC zgwyjQ%>DE9-FI(GEdaOxfiMX6nH(LEn*N`Da#rSmuOBu2;61=-i4<$z2||9?cItHe zSdu>X{Bin`=T6ag-@Yl4g5UdKg_hQ?GQnv`5fZ2hN|5#43>StZiVsMPeYP+?N29p{ zU0b_NqXWZ|6y#Q2s$mAsPd>*IX_l_tyiU2n0ZMr(%B3AbaQDNBdunuK{1LjD*kGtL*;~O$<&8ZgBuV;JE*EJS~s)}-_Ec9iw-~D zP00SO(UJN->_j8`N3T(7LI2jFs)20?RHB8@=J3HgI=vz`QhyPtrv^d|7HYM+OMk&e zfOXV-05$Zfx}cGON?eo}VE&IjilXuGpC2$Ya78(nMx~x>N_Phm%IisFotXOMK3icV zVCdenH+B&HM<^}sIo4-O4L0lK(5bm$8e_5l8oO~M|^*pS#~yUTF=1~2+@6e zuO_1W+UA}#{(!3xHUNF!rl){Q7fy`Oxf7%G_?a2;F1&tenXWDFuzJvxx&p`pfXafh zVLEK7FP%z@-d8U9?Ha<-+fABpT&yI)$b?5F9ojrM) z7y=Du^E8mji91n8oMGrlKqprVH#RY{#c(4KCBgZS1Y;ZP>`tiK_YMf5A+;JXqZ?FP zi)j>^Eq*E-g%8*obn~d-Bj`tc`Hu=duG229y^J%OV5Bcg=d+W3Y>IAs2<-1|x1;R^ z=#mJOU?|sCM1^#qhC77sw%grqKYT_+U!23Mk6#G|U}Z9$<4REUe2DQu(jc^N>iIb# zf{G_V{P4V{fw9g~2F0YqVTGB0x$D>V__@dM z^4cz)SvbY&NtK?u@Hn}MTm->)oG#?ocOBm&vhTI5wd(#L2)3VE!&hxzwr)g4UPS=q zJ~&r>>iDhYvXn#G)_Bd%dsOfNTO$!MzzywE;Ew{n9R?iN<@X_|MHKXWHJoDDL?JSI z#iRqB8~`Gn2L;Q1cSv~xtdz)6+zTZ zrCmrBiEvy~UiC*U{>shN&86@~gz`Fl3e|O`ZIw0YM(0xay3rLeRTmx^86i!?(L^6R z5>e*@@v*BDKMMG2FjT1^j=Xk|lD!#vA-cX)4BO498%lrdTmf5+5HTJ)xmxjY9_~-8 z{FoQs;pUnStR4a&Gz@wg-m6?*1B&zqa2%&28HDFT?`J_fRNWn;qXWsc&b|Z&8R^9& z{e+x9?CmFdeYE+_6`C#RIZXj6j0fw7>w|V^SVRZGlyvF&vzIRm7Y6}Aunu&46!}0y z42V6-L_9dTSeXA(Xd2)7) zUV7n-oUd^yc51M`EA=`-(#W$!aBks@bWpgneV1m2rX&I|E6 zB`RBDu!)3iVYcV7i3GKq9i3h|hhqf*X>Ne;cd7>KcZkqy@`f&F4Z%=vfJy*BdR(gk zLAXWy0QUW}i5)S{twdKg*C*FndJ`iUBf^p`= zgseG&hT!_Ud}B?*fx)$rV*uiT>yTu$Fgr@8=EfsJ9k2HoC-(L`O!Cjd*t{TmtYA86 zwFLXPs)?!++WRM=h+>B$>X%)`BdHU>q8rub__k=F)@vmOY8|%?QW!W6H`(_SzFoa1 z4!<0VtNq|BGAew1X=>$HH9#r6!qNHcqM$aMUOJ%;Tf9@rz_$9^-`j-4e(o+e;^2_s z>frkLUJZE|IIi{lw7#DkCJw==0v)eG2W9yk1qmTI5cM(E8R>WS zDq?`_taV@teDKfvSC^$zaFnYM>L6HSAV2*2157A>;)frjw=XYAyKxW`9K#QgUz&!7 zVZqN{oTsa|*TdQa-94ghLTyWXHghmN06dEb+N%}Fx~O*~zISZqkoeRSOY|UzW4V(b z#Sk@?_wMAV`CO_zTKMtt)Dtsw{l+pCONYJU*NzUvw(rFIiyvkDhDi zb6?sIAD|FZfKpz0U6C!?{gl+z(Oyan4)*zUwwE$9@0=rNfFf!KEkkV6t*$cRt0Tt1PRBU3qv=0 zTqVJec&SlDK3f6mCgh9Y%8#7>1$EQrKm zkYCl=`x$qcRm{)L(5ZzyUBB5a21Icx4-tNROidVw;EKIQJPyI;Iv6(Rw~=2tNDa+$)fzyhI?}-^3gNsRHp7U|$X7y458?41U+6BXUIc6yEz_ z-WUdL`0;xDHvG097AI{dg&1Vha7n@l9ME1VknF#H@L6AB_)bswFMeW%eu$++Q|{b7 z()xF#qwYo`Hhc_KYY(&jT?C-aV$(pj;{kdQ1J40*ES0?X=C9UGN0_5X3sR=Z@BcmK z%IAoNSp~hh6orKJ9Sg^Ebn|xQ9%psW|0KWE4=*o^`eBUR#tXs$rQeWmC3y*8DNlxn!NV-9TBqn;CSl*G~c)7u<4GNRTEv)ExSzYZ1MLG>IH5)6mM z;Gk>F5>fa;$3xO+x`L!YOgu&|3&!Sz92#Z_H0%KUq!5V?0(Qd(&~1SqNQJTCd%)Lt zgnGlkPY!h8r;_G7Bo#I4ASlL=X^7O})?e*CirDJf#WAfxp|&R2&NNe1-M{wwV?;*# z?#;mZO)*RmyT``T3eYmKmT2oS%9Tz7*?SF%BT02Wf8>h$!qNp;Tul0Izi z6Bmhf#4Xh}SHgE{2-S1rqHEm!q#H19v}`m`8vq3{jSfCP2V=0|_&7E==t$XZ2}59n z9LO1qe7)WleY?ij=h9BZMD~h~oSV-FQNSaligZfPQRLVUzf^W;EVM0Q58YWR)48() z5qxwVCbuDR0o%I`I(clAstsS@Q<&edyXVlYTNN4}bEW$*25bnaB(K}p{u1xK)?e3G z*EQbY14G^Euq$XuI@D_3!-ATHvFRw82c~~IE45K zloHQmcB1b=g(3M24&2}T_cNL;ieCP?Gt>0$`-bbAXhieQ;zK!1T%Xr~B%537h#LWw z^V{d@`Ms{Uzq1QsV9G|5Iqz=3`^upc^9Fg5@ptF0Df3k-n}WibOdzoUsQQa{3FMbx zMm#TbSQrrTe!-0q2(+|JG&|$a#-^$KW4JFJ2!CYQFbt@erE(zUz&E#Yw7%J<;etmi zTYHRjH99pnM8gAl;{^o~L7dbMMer|Q-J(DK$8XVZ{O><0@Hh9$^w{a_k=7p~yXKqN z`aIrkE-c3;uI`v^rU6O8s$*fVKgK>I;9qPHd3WR#Y9utRulmRK}jklNSiHqYAMUI}$G5-D6 z-rA*H58{XWyl{ra(7MkX?eH)U<6Xe}-P%SiLqm`jj+<`AD=XU~)y>VMB?$<`X=|$} z@O$aPaTip4#NAHzaox;uLsbyl3>sQpt;0>`MD;{g)=VW|Apho;(esyA0wHerduto| zG|5IygnoJr07@hRkqRqSb8LK!_Y$AK%Q`zad9f69;+dL0BQ>~Jf>p%*@N3luUAE!$Xotzsn_ASSB?6u)9ue9j>#Wnhg&pj5wAIzoc z-D?|k22Dxw&Y{*{#eKw>~~?rDGE*{9e83G*Z(Zw#aXvuitgnro&}=aeaF(X`}8NWWv|Ywh1*jbJ7W8 zhyv!AMj8Z>&oQFjSvJI=z*0eB(D=NK(b|*)1GfkF2Df)bZ7`Ap!Vb3gj1-h(wPAbL z;XRQSH(wn7aaKE4R#U8Q__T+pzyY5!jP&(dRU!sAHk(ul%JlAyyEHa3FZb!CV0>i6 zZ*{|e<()Npdf^zIIyFkmtL7bLa~=(5hUog;5`F54v3={W2D*3+2aI^iy6tgM`%60v z%l1OaOxL_;Z@`|r=^n!k5e!Bza5VT^6`9oVQSRf92l4}XdvQk~2c(0zrC#e(LOlTT zD|`?M_=h2$MfY|H_@MNeK6VL>fVHjgbH`FFf;OVhKyFM8@o;O$zRa%%g4~6p4 z{ZuIsi_Puc-yJj#@-El|_X0x4E5Kr|xu1o(&i7i3q|xYkd0&zLpwDrxV_UBKjmQ>Y zsh!!3ar0fj;a0L&X6okrr&V@ycGwDEnqD02P80mIPj@UtP-SwMBO2mEw=Z;{B zVc0QMg`EBH7ngidA+lMMtaIg~oAl&Ur)X+=gb7oNzV_9RXgiH)&n^1QQ$HkKfWW)} zt7yUhr@!+NwOPX0E;g6|jFBnM>CslPLJP;RiS}<2<>cBMFdo;hNN(F#^g9gg@1ZMi zbNgPQ#`i4&B*M^fN_TToEZ|=SrzJi=bkT8@Joh8mO4A8$}IIwgg}!v|6d-VyZod-@~vH->4X}k;CjY zfyo^7)CsiCcIoyV^UTo0aU9HHb;Cs8O^gQOV@C+CB0r-3U~o%?$%(KTfY7V0rykmU zAT@vinddhy!#I3+5^mfw@JC0Iw4Bb!9^cxjP>J7hHkY86uPsw9*{0Jh{$qV%i`XrK z>-tje3-G`8$`yL?*)w$R!mOwoApU@#t_@ID43&wV>u>i4)bH(pA3fKut_>Bzu5n?n zxe{kPm`IODZ&`Jqql~x+?T4bZ1myJi{-B}o!J*^gKc0PfaQ`*;f`9tBL${Xt8wh)i zJImM{D`H89bo2OmBN7=7#{)!^%cw88pQvx{vD?t6!wx{v`RwcUT-o?!f_ZeHZFVutKAspihCJA7 zpz1+tfiy5OVhVcTeWOy4q#e}EXY8ZfciLjX2a;E6W=^cv%#0kWe724Zdrq8`_UMdQMlZ(-jjD0}if8xpGbZ2dc zKK;ovB3T}r?!a&IO{0pib_llDA5>g>1L``dhegxNH67i)PHY5W%DVkt^d0x?4TwK9 z{99co_dD<`spP%Siw_%qzw6;Uef6#GB(+}flVQ*iC_df@5V2b8e+|aT69ZBQV0Cqs zf>kV{Sp;yOTCEvbiqD@NVX<<%XOIr4dq0oIQO5mx7t|GtH30I%ma@6+OAJIk6!$?~ zdr3cbaEOZY7&a2ef8uUFd#LCNLZHP{;-SNxb324wU2#}_AUj-g`Mkqw9-4hX;tFVb zA`qQhTZ#vgcxb!@;+|`O%NqCK`ljLPKz?*G1L*b5K!^xHLR$0vZumk#Js4??!4B|0 z`}rq%o)l%WZCYI0rsHR)>4R@wqm)~ho&cVkG2PPn>QdEb_^!AI8>}vzKRYAIKY(e& zN18CI4)+O2ecY9RApb2yRbWI(Hg%3)87#D@(baGI3OX&6H&{FN-o8QX1=-hkbr~d>rqB#$g!OQ^!N2#BXEEEMG6RB<`=ruqA_du#OHT zjB(LYlc2C(ydR!5;OvLr4r(-5$VBi9g|ztX(HcC(0azu()T;HX=of19wr6*0$Pzc}i4E)Kllptv`7OkhQJoEFKG+O&ec-cv2+tDG};Qo)?P(w2XTJ z6F%(0Wckgps0WGohu!!`fuD8{2VXbJ)tdo5{rJ791`x0K(Iy?A&4l6=C^RUhD>>n& z7>=m~1OpykrbbPE+~>E)$MG)2hxoJ)c@H+?_R`_|xdr1HhW1>xUTeDpG3>}e<@g<= zm9eWpqCvvV$WS00zEpLD@Il1asZuFN5Xu$HdLZ!vdV9&F%OX69PrqC?aR4HrGLaZ6 zbmj;G+6VuY_f}|bY?5ZiGc0j<0>4;s#7Owx{2#B-pS|)CtzLbVu72c6LzBPucC@w^RM5kiDAzZ1EUu<1vN$& zh~$e)I`Ks93*yAKrzJGkO{H^tcuhiQ5C>;(xE$^vk$#JR@_C;YW^(+#J3f4UChZLfk$!l1NVs`9L}oS6YH=MqC0#iG+KM1Q zIRE(A7!!sP-C0_qVb*2GMn}8WPl<>SfT(10SRh6XTaMHDTlT@nun!zG`oRY`oSSwc z!`e~cV?RQzXtwrW6jx_}czC%Gm*GO2nMC9usuxx1%*hPB^FdA4VyK59@?ngJ>0O8q z-8{Xb#I%-ymW#)Ti}N-j>*(kHQ&?JE$q<$O=Q&QN-Jsk<+Y?dMuESyz6Tqw25yNk& zy%18z=>A9|DuJL6U|r!&2*N}klWN=x_Lp8o8lt=c`{A*% z+P)FLGUziB03rmg z4#Y#Ax750181H=e@Q@$YXTML<)x+{G#NlfIN!$*gh-DXAu`4sh(@gt~IM;{Hip{Ee zEe1HZx|MA^1V*!!p!E$S+Rx3B`0mYcBBLR^M&*jh3E1015D&lKNt6E}mU(6b2$lu% zlc_B%E<_!1_z3m^0YLQJZY`jlZHC3-{me9qf;NopBAFq-S*BKHliH24Sr6X0twxd1 z*tD^`NWbuFzd^tE5C01~H$O#}ZY{AIQ=_NOj)w3}!;)5P{g;FCXH2wH^ra+wcKza-77k_7NvO}Qh^K0>x*uK3 z{_t_VB|7}}e)#dB-i3z+Kc2W_8~yB{9{t=aI?YDdxXy1EsK9S*MyOV5Zzz_XuyK$h z+&Y~$k$x~_wp)%EFZG$7E|})HsW@gV_M2M{4Ht~$cX!zs4bw?5k05flnd`OduOrOG zhM!EqbCMBHOQq`5t)+%={fXfLy0NrPcUDc~6~{xy99(KFh7U~eF^}BjfN<-8B>=xs zq2+7eqc8vM|A$`r?f+beEuQz*${tOR=R?)h*|+{V*88TXrs?%J-(m(hKn<3b#>U5~ z9QK}oB#=sHq+~jX7>-Fhdlc7YLQo3}&}v)ytauR;UGGo{LSzFt%+P>~OuDlXwfx(I z7}!@g7meXZW8}p{(C%!0g^!m$K4OwK?gzf%q-KNmLcCrGrtpD`T1x*xj0Xw-Fj0&c zw$ZDXkKRV?cr;p^C}5~xTEDj?2-_HFf3LwQKt6m`ECwQZBXv!?fT9i>na-wUV_KW5Im!nvwSVIg&PsjR%o@e8enTwf5jt0BmgJUz@0` zPPCs0ZJIG?!51Q7c_ofAf_q%o@0kD}!9m${cRKWg9`_6XC{c{v^S1YKcR4R+=!@&! z`gc5UpqD+tHWu1x#`-S6akCN(W@7hYJoCdA1<1))A|Q4FX%4FMg4;Xv_<5re++7L8 zLv#9sLu;FXxD+MeN4J&Jy|~8CupfSLZ-;*OcfLn2eCh;!^_8pi_1|5h%{>!FZNhX1 zB)U&37PsqCa}fIa_)wPq)_?SQ`jgk*q1Qf0Q0?xk)T~$O#gh}XwN|2!zPm_w-(8_! z{na1hInq7XAI85FBlIVqf0n-SjXxJ82I3>L4@i#l!1bY`WOD=JDnx=CTHLF*=+dQ2 zbn)WFeG#4UzmG%!w6H6XaAfExSB8sC2fOl-<+ZWh>G6ILd{YUwd-O_7ist6`wO2Oq zA&xDrG{j%q>R}}8Hxxd&#$c*-Y|(dcdEW7@@!OUw=`2_b=brp^Z09__ZhyZb}S( zfnO)TS!2;Z!#_E?c>WCi@?ZHZ{gs!dXyI6nzWwcgKwf5qe)=!JM8EhqpA)@)kyU^b z#|lylaJSgInJa#v$1g0LppDHf`qsB!rq8|fS$s>gv$N4UZf|dkh@Z<%h`5h#Pap!2 z0MOPRL;}V@HTcCnHC>^=hZ*uBEDQZ%hU)0;u*_GloOB(Dd)$o;A9KQJw;LAt5N;6~ za_q>zAp8!;H76c=VX@d|@e7k3UWd4!=hq0m!#w}}z<&@?jJAkwi-~IVvt59y)5qh@ zP(uS~QklYmWH1Yh93_-tuz$gqdHlks(My$uV=t_3=x`_w4 zc-}EVIgI$I!iB$)ofpp=!JdF&*CdVYhmR2B_byjxY&b;&8J`gz@yT@>pPZ&*)e}P> zwORD9?MkPDf&2*l`p>;YpMP;envYyOcZ{BR@;SP2?j${T;TSEh?$O-jAk9twodF^Ws%lymN<+%}mRCRvjLM6@LG8IwLy&?%pmfE!~v>qlt;}C}=4DUb>{W zBkjN=4%sy^Z7C1LO+3ntx5@pU2*!O)5HCzG8 zgMNM;4=Rc=p3!Gp{OuwX=DB)v{`A>JYR8 zk$!z0IsIzEcA}V&R;P!E)DVCt3z7gN1-MSd3t-i)fLj<9dGN(h!N4~RqZ|J1r8b>i z$kK2A<3FOWefxWqCYOHwmw$?W?XN#cODxL&!hidRjFc;KU2OPSICiyGh5Vhs5{@gEzv4hYFp*rGD!VJ^yhQ<)Qz7@AcOs zy?$bRT&_jGK)4MNr;=f5;KcFcg8cE0Q99HOcwZ{OQHLG46ZL02pnTX-N8J*E71^LG zqNoV;KfqV1qA*}M!}7W*8sQ3YcnBD2jUec3yhsIofX|QdaibXf`S5MSpNjCk0()%Z zUfkjzmm=)yLAD(r6_$YmHwM9mT84KB&JW~Vx@$!A;h_#(*D;oSME_wd95;xIY*Hy0 z5||9VycEj`WUnA>7$M~-#)*hL*X_g{#Nngj&$Vms(p&Fdrl0zSpQqzx`eW@1g+cnlkA9wBfBj8fmjSDpjVF6+VPS#A{?Vv9&_U~O+hZOI0T7F8U^cp1 zaaH;|%;wjS&0a-84jMo%Z1RQq(9&!5rGRvBY|7LRz&L8HzM~BFTHrU*KXMY_VQH>*mtW_B`5^$;9Rxt2D${yY-s65RE(yQNDG@i2ckwB?t=D zfClqH7~|i$XR&`cCUynqquNg0s#1kp0(Xa~4vRjxuJQdkQjU9Q&=bHx&<+3QB8-|X z`ox7KojUa-wewdA4M~3VQ!`S%M`I7rQ0e){=V)$rh8Az{(A6tDG&eU9!3U_h6GJpC zZODE4@@LLdv22QmB4+^bjTA&jTL0&sd#3BNz5el_qF(EdeWBm?EC`~D7k4xDD^=|W z9qy|GYl@9uZ*;mz9d-ceQ3D>X9#Hcm!FMPuis*E~HyMGZPTYeYy`CTIJ34GiNG-3L z1-ReZgZCN_B8stJZ#r8XjT;0p;yDBHU?N3$dyZa(L&{|n?~h7Aw-^CIpkU3{wuNJ( zmJKIVD;HTRm_HT>au*7wst~dPwNjN7sgcL=WKSVZA5Q>VU@l`U_9~YFKGcQN3l819 z?em%@1paUT_A8X(Q-1MhAD5^>NvlrU@QHr^fB7l;yZ_@?s8ZRZ*_jD}k3IOM7iZ}= ze)F63pZ@(X@-rvJ?dP~oPxy~={o|3G`v?GxC2`K{Y4ML9qgPBm&RtnER8Xq?^uDy` zKp+RWIFey-FIdpS!Us105*Iv3?ez`R(}t}{f=JG59vbG!=Pq5Ydu71L+|0^ zL@~xI{$kjhNjP38viH*a?GYDUml`0*W9VH~-4Bt8umd_gk!@};YIfUGsc z;Za%}#X?4gohbSb5u+L&EoH)jZfQZICtJ*k@iz>Licnw>fR9)KG#l|emKdl(gUrf* z`Ila#x34aRV!EkE02ibUKby+Y^Uu%I_3P_2Hj<`ipPe%XU;g*^|Erhi!ud&IT&M~( zm`j9TvfUS*YC;AKz?S!J%VGD-ffg!>lcH`UdJza+| z@JRKvqriu_-wS>}Yg@VqpCW><4T{0RO+;W2sifFrHvGH0EA;f_jPWq!80jxH!_s6- zAohB}cj?6Z5V_SBOA>YSY~dQsPZj9fZ{MLO&~kh@K}k<;E39JdhjtAAk*>d9AN2;r zf3TQOjv0Lt6w&l&?}+cB&t@s=a&oU!U?n7n9dY1hL&8@{;eO%YWohBWEQt?U zUUuyHk2?NCgCBPd*~j~xV}Hib5K|3kQAF1t?UD>5m3^&wr>2+f)Px~iQr_D&$v5D# zUc#ZhqLJQEK{!9)u#{+!`0lPF_5wS;ATCNPaR>qsh-4@s82Cu>tx52Y!)lvPpL>3W z>QXYyrR`Eh3Ufe`>;=ExY|~pG?n+I-!NCkI%#TvRe{Cnjt*jTpvPfJ(X z*QOo06ys00i(a&Csg z7Gj<6v z=+f2uhu`rB_a1-`FTzog?*|>e?FO`6M*`E?pFI}mMD~U=*7-)4n@_p1UBfQEspH(Q zJ8!@c{=vK>?;3yKy5kG-gJ9a!)h z$hC}MBd8`e{B}^5ngv@sP0|0;sU)ifb^7+3n{H zR2WEoU)Mh(0Oq0lZ$44q*IKTrL zKY;LUj-eYQUb#muwUDP*DzG-Nh(KAJ)z(XUZRYp@$CUEi8An`#KswZhgoPj1!8@=Q z_IF5z{r60b!5}L1iCaO)E#@C8>~C}44}K7s5aJGeXCM3vXQpU-&vXg)T zHW%(K#(8jEa-JV-`vFGa!EnZ6^PA9ZE|N68=(U+1)<_@|tc9`=EXH|Gf7sZVaM|rW z7`RwVyeDtiJpaIi(e3&@fBHp6pd0+YVtqx1f9?KU6!!!F%b64{G537=>c>x|1pHyv z(|5t@Apx)+?j=2H`VF$OL;$G5jJfd4e1g94c!rkC4*kxbm!y3d(fy}q1Yz9_3TN?r zkKreJ!EZ9MW#Rq<5eG`3IcX;X;=#a|`o^O7`!;+(grA1ul0gyF82s+$peUhyman)( z`$FWgaUFigsdGHZr6Bs=V;!yWfv@V5JhIEWyu(zpy=H9$#}JPek~I=Lsdmo=bIay;EJ ze!aJtlubGGGtZ^y_g}3`mz)O;zd6gxP~{8ysA?F=uu#T99iQ_Vm*!^^^kYwDX>{15 zKVXD@Zy5v_Yup7Ntg8Olgj@LeQ!S~rKVR^u_TqpLfnLLu8xIbY#)PZ7-Fw?l_>!;` zgAc!zG!XY#D#4oWFMxJ9_({h0W?;GgV(@{|;xP;B47AMz&==7jSYjlQ-!PfX(p~CU z%)1CcYsDl{M4a2=;%Eel@4XlJ14|0cD|%1aZ)HPPmrsV*hiF^8AkFIl9;LC&pNpt)V_vlj?W- zWA^7B%g_i*DGL(`y0UUGkp%pod^$x#S%-f2yY)lC|0e6awGVpFcYZcS<%UNor$r@3 zBDCJVz21AiU;pwD&5yV=0wdw5M?dt7BlP9}ZH@Kp?(-Q>h9~k4{oNnS(dtH%R$2OZ z`l$r{#FJ_I=ie@Y$VAwP7=DwmW!8Ow_i(vSHR&jVFX%*l;#fWAr{;5{$|F>gh8BL| z>oN(Csx5gBec{^$U@-DbT(?`D%t3`e!h3rwjF6L1aQLdx#j^uc z06c$^C9L)>e*VV2pWmD2*K*3C+IJ2hJpBBrY=Y9c3>C|}BIWfPSC$);p7g}WeV9>9 z1vEA9(%G9HU0Uup{+{55}iX0=9EBwiT-QvUt8E5usugzkB0LeE!SF9ZTkDTc8CF#yrpU4&H-SQqIzsO{sSyK{R%e$J9*|8tB_>awIw+^3h zOl&^l4z5+pUFM$zxJRV(r;Sbz!9kD~1t`z+$4bcA5oN_26MCd@^|}%3MFM~kWP-0> z3gmbz4CBRo-Xw{vfz4;bb4U5oE$ePz^|jl`^MA8WlV8YEqK=Tb{#8t_*V|O82DG-t@IQTT_-}5s$QyI%`cD5v zI4t~UMw(RMwc5V!(lf~veew%Qy1~-YZ~jsD`x?x;R4TV9#rMk6EDf`|d3muWjAI{c zo^OpMk5ty9iK(_+TiI>WghyS-kFgFFg*S4>btEPMgx&AKgp4?Ekou+U9M!xgZ6{W! z)}ub9bg||OvizdHJUvF~RGHkyFfFoX@Nbsxl1*C_e%dvjk1R_GyRD|r2?WjaI|-dtgR5;3a4~hWul^mL-Y6bfgv`sv+A9HX(=}`dvlL-wfp8-8}!j z1TN!5X?@`0y3DfiU4taabG)Du`fBdb*70Ev2x<-`PVk1hY4m^5A5ogw<_6&cnvzI6 z_a*|6U6?SPJ$shkfB*fyQh*bd+D3}((@#H5Z@lqFSEIzP>u@N1E+-^dJ?G4Hh?vJz zy4@$MFwokAye944fMzew(&F{Iz2QURn&%f^kw)G%+p zEipp&f)B|JM0fAiDxLh?ak`egbs+qDaez{XgkNE1a5*tZE!5=t81PfP-^MaVyMhRX!u5C45PWfSX674OauPbKLOUT^P%j|jl4wM|->NK$flkkW$< z`r69IzBPr>F2{O)RSXj>4ImtxKW7nzXxUcaZ`xvlo60#N?)$t(L#?E-8Fa-@n7^0m zRVk_NTlPG_fl+a#QDZ*Zqe2a+J2tnAt}OPw;yyqy0wV#4Aqu6Xfyl>#$nYnA%cM~k z#6^)YhyY0g>H(yS#s@KzKqyV1_nme~i4RR@td^@HS^~Znyifw5@Nuph)a2T6l6q>I zbWuS#p>4g_1#->v_1gTlPgT4kcpe}=sCL#;A42#+qDAeBsXySM#QD}96Fiq#a-+)4 zZulT5%^SQf&(_GB;oooXhi^ZB^CP34)o-_`P#mTDb1T$%eLwslYb?r8Eq+kpV_Dy3 zf;+KVqvwBQM5L+;@7OkZQQ{=Yu20c>>q4Y=|<@;T@?6B5&TZz4kvtzOv1l@o^dz1I z<~r8>yXM{DJw23jsm#RH$vV7`+r-#FyVUI3qmoV>ia9OcY-kVaHOi9Js@-M5L7`jA ze9;6pb;o_zWgn0Ox?9Ld2G!6X+%nuhvB0P7@uVywwf6k|!7hnVJ#a%VT8Mq0H4hNC zCWC0;n!({g(+&ynac{hbN_Q$kH0+lTqhCLNbgS8t>zr6~%}8>w3@Q=)CUw*TjaIW8 zmB1<+nDxSYQ23M%C~?wAHX;h+z6X42U88-jLlCGYA9t!uxUTW0TsrvqTerJ`2MQpO@idR&hV#u!`FQa_|1GktE}4OSe2+W+m!c`RPsaQ58*3v@05LG zh%B3n#Vg<%{Pj1C*Q*t__43Pxp=&O5%1uR0TZP$T!Zzyb!?MrF+*&=r`fR(NU@Zs2 zHbZ*=LL--qwY#N693%|}7xOay0zw*uz|_1-w>?>I)ih#?`U zW`qCXUK(CZ{P(o7ZTzO} zsP6BFpZ4V?wQiZ?ketNuYTdsVc$9vQIm?=H2Oj$Qty^lm3yH0HPxx4Wf7fs17bYdbM2VCNNOtQ;!0_o;-8Ln|W-9R^9=>)A zewsSOA(Yl}OqiVK8|f{CpYSUtJU6JrQXf$B^8V*ED-??Cp=byrk;m6%o@46$dJrv$ zj|x1`7(&r2>=lNg=6j)yKrge3L{O$Tl0l^U@a;>H$D-$OLrNE_Cz>n~576j$t+1g! zg+eh`8?IQ_5I3ksZd?L!`oQ<=5k=PXqRr!k&tC>&@P~nWKYw5N{hmJtU)~GgV_6b^ zap7aX9!3KBe#4g$u8(+_7$--QDGBPq-PDnjfFL&1efNx{8U@{w1HyouW$_v)K< zh7m9^Zd4+BB!ZH;VT5(o-m<=!Gh$67$}wg1KEZ!{7@mMwp{qV6FS`7oA+k&Al(7;g zCebI2kH?~^VD(2RtgC$m{LmGYOZBw;tKK6>e3yl`8g zKOg>m{8Z2ZsALF)V`*qmXM=h-eEID6fnRLP{o_10gIpF+9KNCYJE6tiWA2;_BIBRN zF1Hzxanp%;GGelC+(7Omi#bpQ_Bycwjhi3IYxq=Y#Os$@~qXiRX7G_(RF^*TkN`U1j(?G5A3!MmvTOVQmkD&yuVI zdre6AHQ4DdB79o{na#E76SFS8bS6vx=y$7;1R~imj{ttgX-n2%Zrr15tL;$YB-6E2 zB|+rSG`iu#)z=EbQfi3y*bR)XulH35Rg6QzPmg+ZYquSF^Ad>u^HJ?27a9_n0Jvj( z>@M08`_aw8)zyILKCCR$AX@W1CIF|b6veO%Gj1Sx(xK*!&}NVa0+|O(o_DCd!smpX z*2#v5jlh|-sq6xS8{8|vJ`{YFK=RFXdY>m&k#+*HO+@d zSON(6qMu##=?`A69K8PPa6JZ8LE%!q=i8MQUHD{@ZeDQ?4Il6j>6m4XD+PpHht?}B zlCz4uw&WYbTq6|go&E5=Y>RnsK*gr9kzF{KqLrI%T0uy8@@VikwGk`h<`Cm!v5Ja7 zjsc8G5Wk;dZXSqUW&&7RyRE1W09+Xw@Zuu37(p8G>^_tqLXry4SO3sZDx6=t?o;+j zk6J56I_NcWLs_a8jd&&$nc*RJzfo%LGyZASlY3d_Q4_z{fJ+6v)I@Z`DT5@5D6K?O zbhGwWH+)eQQ0Q~aUoN2f8sms@O`HBKBiG4>A>J{j9W6v`Zf?_#5} z=$$xiRIl#`{5pcc7)G^u`1L<~F-fN;ptH7VkO|4@6In()hZd$2w2GR5G59FZSu9%W z8}HQ%LpAdG**95q|IW8-^xP+#^wbw7=|^73(%Xwox^}JE7yjTBkfce43<7U_8p$RGM9iy1&b8bNIqo<_ z+NU%V&RPTc{3P&CjwKHbzgDeD&x2~yp%DhM5uyuXFA!GUoekI~Q%nF^8S5c+3B4a0 zAd{Fe;;du{sve|n58sIJt(*MEiI5wH0W$5)fEuh0Bt{a{D7L!9Wem(WFh0;7p6eUq zi-?n!*pAIu7%fP1#Cv4}HhVM^5Q?i2Q_$JVhO=c?3RIiil%yyTmo4~i$`KoY7)<$^ zv8f^2^|pV+b!*Ph}l? za?+#MuC?w9J|xQzsx7*7wS8##3#St_#d~q>MxEYWZ_&z9m45c8r|304ezld0;GZ4w z=x=>#kcRR}`al1_B7N_wc@cOgX{J8VO3`{A!J^G3OPGxoO;02!KgLq&c7OPVVH5`C zJ&TfH4mvY4*XR9~PNr$>Y%+pB&4{vDMmsWxDvaROFq0Gd`AEj2XU0-eHf*joMfu?k zx?JnfoEI6@h}?U?N3NenPl^|U|2#dEq!q@w zEha)ap8I5Gkak$D*lgFj34jE=v>}It2|?qX3Np&Y{B_n(=J~^(sg)M_JVQii-SthP z4=jwV3beOC@eJo^ZnExFGv_&8w@8dTS--tOL(_$bYX_l)I6`tC^$L>IcH5FPg!2yL zJ6<4mh+go;tyD7`EQ!J#SMCq~lgC|pV#1?0F4KL(e~po`zHxZ? z2zhk_q{i~b5a7^fj_{f{?*AgRo2RMEMK5s{^B4_Wj*?%w@t)i zsZyi64M>+n=UBR(J(Zv|e?Q_S=-OKIK=|{_>354wRzW-(ZssV-a`cClGUWyX8s>jx z+97u>U?O2`WoR;r6ylK_2!}EBO)&wO$>hlA4PWPZPSJ5%@|I|`)fD&?N{^7P6n-O2 zfd(P*!dyHU;}d*xjoJBDyUy>$GXX%iiB{_-6&bgU^kuu20P+L2YDg|Jy$#YVvIF^% zVOVd)n}cFbAncZTVMq`WH$xZ^1g>_g&%bAWrQEdTH3uRbqStQGiXCoqUKLkKsj|Pb0LK2*VXH5hqvoQuA zF9^n#)>4b|sVwazHp$;~sJUT+T)Ltf+u?JoUBnHEo!jB8M}?pMB-4;L5rWI`j~yO9 z)>Vj5_#Q+myASxI7g>GVg^lLX!Czy2xLoyVY}}>2x=YVq%<%hf)0vqL{J%Wm(?@Tx zL^9r_=~RhMoE@f5KbE0CeY+xg0IzN^$3H{#6u-OUb07q1(S6@q3`DH%2Ym@dmvM+li!y9hEPf|UoiTaRcB-3P{#VwQ_ zGsJ6viB8=|uD?ffEHT`vv?RBoUN?*vBhR?HyXs4FQPpozwPjL*rFSM=8qo{S7*Rtv zEz*HA&#@lzVR%e(|5+4aeLl%AMM7#VHl94q=YC+^6hSdem>IG*2cp2hFs{3_! zxIuQecl*2dAZGGIRrabqBlF<$(0*$1fqLi_G#+pawMCod$hhd46j!{2=2E+-jUozK zLh16Is)TvAA@V9AYj@xW8Q*Zh(7vMciE*c75aFos0T&sUwXGw-C*!IGXU-Nf{0z0o zZFN-m-(rsCR*g#dsNlbHrA|qfe}C?ROD}vfMFYbr7QvY-ms(U~{T=ZA30@aoxnc3Oh+0{yM$-;ZS zDwlOx%{O!Abf4}%-F*(o=`@AEvS8Coc7>|W5`Dqe5v+yJ4jOc0*{1tVpBm+moKi@C zV!{0B^@!FNyVQK&CqK$0weWSh%*yfZCWl=s1ldxQOBY0z0syhXPzEl zJ2WVe&k7L=uf^KshwKv1v8HjI678}KggCJ`Qt|<-7yNJXr1?X)NnhWJMA58lutx74 zr~IpSnI3vQ6=ZLr1}UIdDsyzed_8_vSQC)drf$}lb`F;R5!`d=?om_9h08^D8Ix`! zum&QvOJG-G`s!E}DXpw63BL^ujp%>4M1zrbogjgig{UMt%BGV+j~!fskSDUu18*(! zi~n%2;iv2r=w=T1T5cL;r#q^-C~E0xOpB<~AemX&m7WKucjQoQb&gh6R?+}r;DqAB zlsC!)>64OCm95>@lM^-ZqA0T4Gx~2tfc;VaBpc8`yL;M)auKic&0 z^Dc^~T)~hzHZ9?(I?6!1iW3fX2y4RF@Px${E;*iLYlC)9eR%H3_wL8kh z_mq1)$I7(HN(L)8dIVr67Kh%tY&nMx{gl~;TGEfTCC=&&A6T6#NOr=82O zb2tLcQL(gy-$n!!S}!-pzNn+dV^ZlY-R;G_&l+%3M=P?>ol8qwBnd?ReasxT8EP@P zpT<=Ae*Sf*#Xy8#yA8?lN2vXm3_e89JFzqk!2@IKNfc3$a(fQo<)af$(h~tM5dkfH zUbJ&&EPP$7javHfxX(X-WuA`9Cp_^jv96M>!g8%d&Qo2tJ%%5vQyMq%)C-k_lVJ26 z`JuWN%-7)2C_lOo6|br1FQFw_ug1gQoA~^fmH$Inqk8O?0IJ@~MC-3cNUtox!{C1(k9fGh^8 z5wEs@ZB{6SSmhjN`E6+Dys}z(vB(H)#Qn6@9H{Lt_BGlz->4cH2wmZ7tdeB*Re^=~ zPaW-?i~G9(y=;X{AIUjqc@tzJ_?pfWkL)dbFoo9+MdPinulMuPM+>VIcR}TOTMlby zFYMPW=wMN{O&0v}w6G`Y^Ov_uQu2)2fmjDU;R?+=i{y~5#u%5$h=l^@(DQiuUO)fA z|Bp&K8hG4w6}U>eGmY>=&|Fl3gycO6Y0bGx$MN>W=fAA{&IJBXn-T3k4(W}@9{uB9 zL>q6esFHJMdJ80%zvHI`KLJjg9?Jhu_-{E4+GGv+)_#G$c+#Qe>l@PkeSrUUi~J{6 zEvolZ-)P($FtqF4(nfL*5H5_^fYv10UlI;h8UZNK3Ne+%2`JoYh0g{mXjcvZgEwhRQNF{*oT%q>4i*HQ=~om?90!k zGEjeC+nA?A9^`a~<%lI_W9az{%A8FP`xxd&+UANc$7cDK$0lWiO0TKTKf?(-|IhqL zpY>S4-@6~liu`gm3snVjVn~F6{&sFQQ5W#Qle;KH6(wpKXG8GNX?Fpe;p!Ep_So3QT$lM zhxuPySfEcj4YmM=#?)Tmm9IBAJF$8dVM6U75r{SEUS`$S)l6X{N(H*eqvG9F)l1=H zi^^}sBj%%+Vf*obUkOyMl&SNyD>=;QT9O=joOcXT%AZ^lgT<@#-rgQH_uDFwX--C1 zg#RH9cF(yCe6(bxM^|E({mm<@MVecwQ18U0%BxD@;rgO5yJ}YeshVP{_D1>JM_s9O zlVjj9iP-JwvQdY`4@nvBj`AbFd~UN$-Nz0st}QBlo>Zlpe*U2bD&UvgjOGM>-GvFJ zW}|o{3K|`1g54K~pI}L%ykVe7DLI&S_0)HH^8*0y! zllYJs{wA_*7u3Q~UM^Ahi9<^-FHniy8SgNl(yA8Rxj}wOr!S}gcf@W$`5N<=rsKcU zZS`+suFitibESAE-Y3b`w`;bvT1HZY>=Yoe=Q@AC9gj2#Eo6EnK;Qy4o#Lvc_BWAb zbp#C&OE6Jx8msI}Y!ZE`6bYIwpm0HYl<|ylb?fNFgin4xtxM2qwB&wA%*-)5q`tMl z+P1Cs`dcNwZwX%v|Fn(-&@jci}ym+;ul)uPU=rk6{)ciEcA0KJ6-qMTb82*5Trf3Jb{MFSe zxgBLjqK72-7Zw-DX?U_ACH!8j`5%$Wr9iF|+VS&BCy7F^(x>G2b*EDikk^EFTlREmu5!Udf78@$nNC|%?@k4D3YWZ4H{nzkf(N|CcM@iqmBBhmgQuMIX zpE0e&Hr3Y_Bn=oVeyMJ!=6fo;tO%rQ5R^cYf}9MXwiNmwrm@044BS(z-6ngnAX?n( z@gT1&{-ESZxv0;pRXv<1uk5Ix4fGHXaCL)~MYkYl(v?f)eq++28d+pih%EO5y?qn~F z;eS$;ELeTuSBAfMZhlb!SS{PzU0P_>C=Qi0BLI^CD`W3=NafcpiDaWVB$YL+Y?fN- z@q&GRsn9Y|3Q5Dzf^1jBs&5|qYSBr*-+)}7`J0d0?ECjkerZ0LLF=F`6AK9?PIoZr zesgn^_V@S4`uLODc_pq%=tVM-x}GEPkAz>gHTh552dd9pscefj#l2LQ=c(g%WOo5t z7`|h24-!Q#DFulZOfn9OI!$&?#T95-y8@AZeqIT>Psi{dj`C-p|IyAqc~SrQ2l#h% zT;>Jimv|Ev8NbLTzNaeUbji86X64f@kmO*SbMwPW2w5?dd~xkk{-Vk)fO{mZ?<2Jv zG{TQ@Led=szrrRwwyh`dBbpx~s)rrTqx192{ks_>8D@OX%|8i4l2DN6-7d;DR<@fj z%%RHL7P-1Dm}FDkRRt?(GMeqAOS=A9U6cmQM(`)XgJ7TgI1tmh{{d(^Tl}LeKVB-f z{I<5XXnQ-8tdPN^iow)<;g7h!tG55pX?8ah{JVUt9RYx(RS%4BqiZZ@(^_%mj&&G5IJBPHsyiU7E4+kGE;V->-{9=jfU}~@3quNTD zoTiuN5C{+En6^Ot2WpW@Ef7on{>=QxfjX2W+4<1?m8K$5YAe*?X(27%z0-wQ@sd5cevKI~hum?Ltn zK$(WakZnNh%Rh`q&(yO91$999NhzVg8RZ9>`z=~sT|Koja3Zq&h12=bUcC8LM*_nE zn`Py-G29l>gke25zqDj~tbPtFWaGv<6;5*Q!J?6v&5OVd#NuKO$oe7`NgKv6?GoeXY7X^>vQs2C}cwF@JUpD?WO*r*bqnW$+ z_^dD?S>{6cPZyzPny}G_ee<0k(1Z7HPn6lSBQL{Z=o91hpMinF1pz5P*3Ea`rW=2| zd!fXCB~?8B%YjIMfq}tAVq8L|fq}scz$gF%gBgZV00ss#45I)H3}zTc0T>v}FpL5) zFqmN&1z=z>!!QcKz+i@9=l4arP?$s@1B2%TV*wZ#%rJ5l0Mc}SjAHGa?@#+WN%778 zJVExF1_lPtA0rANE692Ok3;r%!=E>TZ(wjaG1dh*tI$qT5T_{*0|SHSgA0iP7~wZC zxSTLSKm&sr##jYl%yt8V7at=EATNM7?XE!H@00J7YZ(|ATpr|F0K)FhM{AgBgZV00ss#45I)H3}zTc0T>v}FpL5)FqmN&1z=z> z!!QcKz+i@96o7%j3?o$lU;pSk7srbHpIf)n6M_v444w~cmPI))7>9(O`sruYKaekj m14-}S`skTl&cMLneE1(^FfMcz0-ikp0000 + + + + + + +

+ + +
+ + +
+ + + + \ No newline at end of file diff --git a/maps/tests/Metadata/getGameState.json b/maps/tests/Metadata/getGameState.json new file mode 100644 index 00000000..a005ee8a --- /dev/null +++ b/maps/tests/Metadata/getGameState.json @@ -0,0 +1,279 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":10, + "infinite":false, + "layers":[ + { + "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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":1, + "name":"start", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "data":[33, 34, 34, 34, 34, 34, 34, 34, 34, 35, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 49, 50, 50, 50, 50, 50, 50, 50, 50, 51], + "height":10, + "id":2, + "name":"bottom", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "data":[0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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":4, + "name":"metadata", + "opacity":1, + "properties":[ + { + "name":"openWebsite", + "type":"string", + "value":"getGameState.html" + }, + { + "name":"openWebsiteAllowApi", + "type":"bool", + "value":true + }], + "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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 101, 0, 0, 0, 0, 0, 0, 0, 0, 0, 101, 0, 0, 0, 0, 0, 0, 0, 0, 0, 101, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":10, + "id":8, + "name":"exit", + "opacity":1, + "properties":[ + { + "name":"exitUrl", + "type":"string", + "value":"getGameState2.json#HereYouAppear" + }], + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":5, + "name":"floorLayer", + "objects":[ + { + "height":218.263975699515, + "id":1, + "name":"", + "rotation":0, + "text": + { + "fontfamily":"Sans Serif", + "pixelsize":9, + "text":"Start the test : \nWalk on the grass, an iframe open.\n\nTest : \nClick on the 'nickname' button.\nResult : \nYour nickname appears.\n\nTest : \nClick on the 'roomID' button.\nResult : \nAn ID appears.\n\nTest : \nClick on the 'UUID' button.\nResult : \nAn ID appears.\n\nFinally : \nWalk on the red tiles to continue the testing.\n\n", + "wrap":true + }, + "type":"", + "visible":true, + "width":305.097705765524, + "x":14.750638909983, + "y":101.908376657515 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":9, + "nextobjectid":2, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":32, + "tilesets":[ + { + "columns":8, + "firstgid":1, + "image":"tileset_dungeon.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"TDungeon", + "spacing":0, + "tilecount":64, + "tileheight":32, + "tiles":[ + { + "id":0, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":2, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":3, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":4, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":8, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":9, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":10, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":11, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":12, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":16, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":17, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":18, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":19, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":20, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }], + "tilewidth":32 + }, + { + "columns":8, + "firstgid":65, + "image":"floortileset.png", + "imageheight":288, + "imagewidth":256, + "margin":0, + "name":"Floor", + "spacing":0, + "tilecount":72, + "tileheight":32, + "tilewidth":32 + }], + "tilewidth":32, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/maps/tests/Metadata/getGameState2.html b/maps/tests/Metadata/getGameState2.html new file mode 100644 index 00000000..e8529617 --- /dev/null +++ b/maps/tests/Metadata/getGameState2.html @@ -0,0 +1,40 @@ + + + + + + + +
+ + +
+ + +
+ + + + \ No newline at end of file diff --git a/maps/tests/Metadata/getGameState2.json b/maps/tests/Metadata/getGameState2.json new file mode 100644 index 00000000..04127918 --- /dev/null +++ b/maps/tests/Metadata/getGameState2.json @@ -0,0 +1,273 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":10, + "infinite":false, + "layers":[ + { + "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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":1, + "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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 109, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":10, + "id":9, + "name":"HereYouAppear", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "data":[33, 34, 34, 34, 34, 34, 34, 34, 34, 35, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 49, 50, 50, 50, 50, 50, 50, 50, 50, 51], + "height":10, + "id":2, + "name":"bottom", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "data":[0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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":4, + "name":"metadata", + "opacity":1, + "properties":[ + { + "name":"openWebsite", + "type":"string", + "value":"getGameState2.html" + }, + { + "name":"openWebsiteAllowApi", + "type":"bool", + "value":true + }], + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":5, + "name":"floorLayer", + "objects":[ + { + "height":200.31900227817, + "id":1, + "name":"", + "rotation":0, + "text": + { + "fontfamily":"Sans Serif", + "pixelsize":9, + "text":"Start the test : \nWalk on the grass, an iframe open.\n\nTest : \nClick on the 'startLayer' button.\nResult : \nThe name of the layer where you start appears. (only work when the start layer is not 'start')\n\nTest : \nClick on the 'mapUrl' button.\nResult : \nThe url of the JSON file of the map is displayed in the console.log().\n\nTest : \nClick on the 'Map' button.\nResult : \nThe JSON file map appears.\n\n\n", + "wrap":true + }, + "type":"", + "visible":true, + "width":305.097705765524, + "x":14.750638909983, + "y":119.85335007886 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":10, + "nextobjectid":2, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":32, + "tilesets":[ + { + "columns":8, + "firstgid":1, + "image":"tileset_dungeon.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"TDungeon", + "spacing":0, + "tilecount":64, + "tileheight":32, + "tiles":[ + { + "id":0, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":2, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":3, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":4, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":8, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":9, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":10, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":11, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":12, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":16, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":17, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":18, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":19, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":20, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }], + "tilewidth":32 + }, + { + "columns":8, + "firstgid":65, + "image":"floortileset.png", + "imageheight":288, + "imagewidth":256, + "margin":0, + "name":"Floor", + "spacing":0, + "tilecount":72, + "tileheight":32, + "tilewidth":32 + }], + "tilewidth":32, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/maps/tests/Metadata/playerMove.html b/maps/tests/Metadata/playerMove.html new file mode 100644 index 00000000..3fecf576 --- /dev/null +++ b/maps/tests/Metadata/playerMove.html @@ -0,0 +1,12 @@ + + + + + + +
+ + + \ No newline at end of file diff --git a/maps/tests/Metadata/playerMove.json b/maps/tests/Metadata/playerMove.json new file mode 100644 index 00000000..db590b05 --- /dev/null +++ b/maps/tests/Metadata/playerMove.json @@ -0,0 +1,254 @@ +{ "compressionlevel":-1, + "height":10, + "infinite":false, + "layers":[ + { + "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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":1, + "name":"start", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "data":[33, 34, 34, 34, 34, 34, 34, 34, 34, 35, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 49, 50, 50, 50, 50, 50, 50, 50, 50, 51], + "height":10, + "id":2, + "name":"bottom", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "data":[0, 0, 0, 128, 128, 128, 128, 128, 128, 128, 0, 0, 0, 128, 128, 128, 128, 128, 128, 128, 0, 0, 0, 128, 128, 128, 128, 128, 128, 128, 0, 0, 0, 128, 128, 128, 128, 128, 128, 128, 0, 0, 0, 128, 128, 128, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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":4, + "name":"metadata", + "opacity":1, + "properties":[ + { + "name":"openWebsite", + "type":"string", + "value":"playerMove.html" + }, + { + "name":"openWebsiteAllowApi", + "type":"bool", + "value":true + }], + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":5, + "name":"floorLayer", + "objects":[ + { + "height":159.195104854255, + "id":1, + "name":"", + "rotation":0, + "text": + { + "fontfamily":"Sans Serif", + "pixelsize":9, + "text":"Test : \nWalk on the grass, an iframe open.\nResult : \nIf you move on the grass, your movement will be displayed in the console.log(). \nYour movement appears according to the following rules : \n - When you stop (the moving attribute will be false)\n - When you change direction (the direction attribute will change value)\n - Every 200ms if you keep moving in the same direction.\n\nMovement are represented by the following attributes : \n - moving : if you are moving or not.\n - direction : the direction where you are moving into\n - X and Y coordinates : Place of your character in the room.\n\n\n", + "wrap":true + }, + "type":"", + "visible":true, + "width":305.097705765524, + "x":14.750638909983, + "y":160.977247502775 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":10, + "nextobjectid":2, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":32, + "tilesets":[ + { + "columns":8, + "firstgid":1, + "image":"tileset_dungeon.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"TDungeon", + "spacing":0, + "tilecount":64, + "tileheight":32, + "tiles":[ + { + "id":0, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":2, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":3, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":4, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":8, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":9, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":10, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":11, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":12, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":16, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":17, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":18, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":19, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":20, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }], + "tilewidth":32 + }, + { + "columns":8, + "firstgid":65, + "image":"floortileset.png", + "imageheight":288, + "imagewidth":256, + "margin":0, + "name":"Floor", + "spacing":0, + "tilecount":72, + "tileheight":32, + "tilewidth":32 + }], + "tilewidth":32, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/maps/tests/Metadata/script.js b/maps/tests/Metadata/script.js deleted file mode 100644 index c857d783..00000000 --- a/maps/tests/Metadata/script.js +++ /dev/null @@ -1,9 +0,0 @@ - - -/*WA.getMapUrl().then((map) => {console.log('mapUrl : ', map)}); -WA.getUuid().then((uuid) => {console.log('Uuid : ',uuid)}); -WA.getRoomId().then((roomId) => console.log('roomID : ',roomId));*/ - -//WA.onPlayerMove(console.log); -WA.setProperty('metadata', 'openWebsite', 'https://fr.wikipedia.org/'); -WA.getDataLayer().then((data) => {console.log('data 1 : ', data)}); \ No newline at end of file diff --git a/maps/tests/Metadata/setProperty.html b/maps/tests/Metadata/setProperty.html new file mode 100644 index 00000000..06b029da --- /dev/null +++ b/maps/tests/Metadata/setProperty.html @@ -0,0 +1,12 @@ + + + + + + + + + \ No newline at end of file diff --git a/maps/tests/Metadata/setProperty.json b/maps/tests/Metadata/setProperty.json new file mode 100644 index 00000000..06addc2f --- /dev/null +++ b/maps/tests/Metadata/setProperty.json @@ -0,0 +1,266 @@ +{ "compressionlevel":-1, + "height":10, + "infinite":false, + "layers":[ + { + "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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":1, + "name":"start", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "data":[33, 34, 34, 34, 34, 34, 34, 34, 34, 35, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 49, 50, 50, 50, 50, 50, 50, 50, 50, 51], + "height":10, + "id":2, + "name":"bottom", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "data":[0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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":4, + "name":"metadata", + "opacity":1, + "properties":[ + { + "name":"openWebsite", + "type":"string", + "value":"setProperty.html" + }, + { + "name":"openWebsiteAllowApi", + "type":"bool", + "value":true + }], + "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, 101, 101, 101, 101, 101, 0, 0, 0, 0, 0, 101, 101, 101, 101, 101, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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":"iframeTest", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":5, + "name":"floorLayer", + "objects":[ + { + "height":157.325836789532, + "id":1, + "name":"", + "rotation":0, + "text": + { + "fontfamily":"Sans Serif", + "pixelsize":9, + "text":"Test : \nWalk on the red tiles.\nResult :\nNothing happens.\n\nTest : \nWalk on the grass, an iframe open. Then walk on the red tiles.\nResult : \nAn iframe of Wikipedia open.\n\nTest : \nWalk on the grass again.\nResult : \nAn iframe of Wikipedia open.\n", + "wrap":true + }, + "type":"", + "visible":true, + "width":305.097705765524, + "x":15.1244925229277, + "y":162.846515567498 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":8, + "nextobjectid":2, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":32, + "tilesets":[ + { + "columns":8, + "firstgid":1, + "image":"tileset_dungeon.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"TDungeon", + "spacing":0, + "tilecount":64, + "tileheight":32, + "tiles":[ + { + "id":0, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":2, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":3, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":4, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":8, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":9, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":10, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":11, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":12, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":16, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":17, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":18, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":19, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":20, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }], + "tilewidth":32 + }, + { + "columns":8, + "firstgid":65, + "image":"floortileset.png", + "imageheight":288, + "imagewidth":256, + "margin":0, + "name":"Floor", + "spacing":0, + "tilecount":72, + "tileheight":32, + "tilewidth":32 + }], + "tilewidth":32, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/maps/tests/Metadata/showHideLayer.html b/maps/tests/Metadata/showHideLayer.html new file mode 100644 index 00000000..391ec449 --- /dev/null +++ b/maps/tests/Metadata/showHideLayer.html @@ -0,0 +1,21 @@ + + + + + + +
+ +
+ + + \ No newline at end of file diff --git a/maps/tests/Metadata/map.json b/maps/tests/Metadata/showHideLayer.json similarity index 70% rename from maps/tests/Metadata/map.json rename to maps/tests/Metadata/showHideLayer.json index 8967ed02..df61a655 100644 --- a/maps/tests/Metadata/map.json +++ b/maps/tests/Metadata/showHideLayer.json @@ -3,7 +3,7 @@ "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, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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":1, "name":"start", @@ -15,7 +15,7 @@ "y":0 }, { - "data":[46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 33, 34, 34, 34, 34, 34, 34, 35, 46, 46, 41, 42, 42, 42, 42, 42, 42, 43, 46, 46, 41, 42, 42, 42, 42, 42, 42, 43, 46, 46, 41, 42, 42, 42, 42, 42, 42, 43, 46, 46, 41, 42, 42, 42, 42, 42, 42, 43, 46, 46, 41, 42, 42, 42, 42, 42, 42, 43, 46, 46, 41, 42, 42, 42, 42, 42, 42, 43, 46, 46, 49, 50, 50, 50, 50, 50, 50, 51, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46], + "data":[33, 34, 34, 34, 34, 34, 34, 34, 34, 35, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 49, 50, 50, 50, 50, 50, 50, 50, 50, 51], "height":10, "id":2, "name":"bottom", @@ -27,11 +27,34 @@ "y":0 }, { - "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, 52, 52, 0, 0, 0, 0, 0, 0, 0, 52, 52, 52, 0, 0, 0, 0, 0, 0, 0, 52, 52, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "data":[22, 0, 0, 0, 22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22, 0, 0, 0, 0, 0, 22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22, 0, 0, 22, 0, 0, 22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":10, + "id":6, + "name":"crystal", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "data":[0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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":4, "name":"metadata", "opacity":1, + "properties":[ + { + "name":"openWebsite", + "type":"string", + "value":"showHideLayer.html" + }, + { + "name":"openWebsiteAllowApi", + "type":"bool", + "value":true + }], "type":"tilelayer", "visible":true, "width":10, @@ -42,34 +65,34 @@ "draworder":"topdown", "id":5, "name":"floorLayer", - "objects":[], + "objects":[ + { + "height":191.346515567498, + "id":1, + "name":"", + "rotation":0, + "text": + { + "fontfamily":"Sans Serif", + "pixelsize":9, + "text":"Test : \nWalk on the grass, an iframe open, uncheck the checkbox.\nResult : \nCrystals disappeared.\n\nTest : \nCheck the checkbox\nResult : \nCrystals appear.", + "wrap":true + }, + "type":"", + "visible":true, + "width":306.219266604358, + "x":14.0029316840937, + "y":128.078129563643 + }], "opacity":1, "type":"objectgroup", "visible":true, "x":0, "y":0 - }, - { - "data":[1, 2, 2, 2, 2, 2, 2, 2, 2, 3, 9, 0, 0, 0, 0, 0, 0, 0, 0, 11, 9, 0, 0, 0, 0, 0, 0, 0, 0, 11, 9, 0, 0, 0, 0, 0, 0, 0, 0, 11, 9, 0, 0, 0, 0, 0, 0, 0, 0, 11, 9, 0, 0, 0, 0, 0, 0, 0, 0, 11, 9, 0, 0, 0, 0, 0, 0, 0, 0, 11, 9, 0, 0, 0, 0, 0, 0, 0, 0, 11, 9, 0, 0, 0, 0, 0, 0, 0, 0, 11, 17, 18, 18, 18, 18, 18, 18, 18, 18, 19], - "height":10, - "id":3, - "name":"wall", - "opacity":1, - "type":"tilelayer", - "visible":true, - "width":10, - "x":0, - "y":0 }], - "nextlayerid":6, - "nextobjectid":1, + "nextlayerid":7, + "nextobjectid":2, "orientation":"orthogonal", - "properties":[ - { - "name":"script", - "type":"string", - "value":"script.js" - }], "renderorder":"right-down", "tiledversion":"1.4.3", "tileheight":32, @@ -222,6 +245,19 @@ }] }], "tilewidth":32 + }, + { + "columns":8, + "firstgid":65, + "image":"floortileset.png", + "imageheight":288, + "imagewidth":256, + "margin":0, + "name":"Floor", + "spacing":0, + "tilecount":72, + "tileheight":32, + "tilewidth":32 }], "tilewidth":32, "type":"map", diff --git a/maps/tests/iframe.html b/maps/tests/iframe.html index b46b8c32..aa8e55ec 100644 --- a/maps/tests/iframe.html +++ b/maps/tests/iframe.html @@ -12,31 +12,11 @@
-
- -
- From 2f9cc393a79ff6cc23864372f7e961d6bd262544 Mon Sep 17 00:00:00 2001 From: GRL Date: Thu, 20 May 2021 10:57:36 +0200 Subject: [PATCH 25/46] Implementation of getTag of the current user documentation of getTag Adding map for test of getTag --- docs/maps/api-reference.md | 15 ++ front/src/Api/Events/IframeEvent.ts | 4 +- front/src/Api/Events/TagEvent.ts | 10 + front/src/Api/IframeListener.ts | 14 ++ front/src/Connexion/RoomConnection.ts | 7 + front/src/Phaser/Game/GameScene.ts | 7 + front/src/iframe_api.ts | 27 ++- maps/tests/Metadata/TagList.html | 19 ++ maps/tests/Metadata/TagList.json | 254 ++++++++++++++++++++++++++ 9 files changed, 354 insertions(+), 3 deletions(-) create mode 100644 front/src/Api/Events/TagEvent.ts create mode 100644 maps/tests/Metadata/TagList.html create mode 100644 maps/tests/Metadata/TagList.json diff --git a/docs/maps/api-reference.md b/docs/maps/api-reference.md index 01d3e636..889ed3ac 100644 --- a/docs/maps/api-reference.md +++ b/docs/maps/api-reference.md @@ -370,3 +370,18 @@ WA.registerMenuCommand('help', () => { WA.onChatMessage ... }); ``` + +### Getting the list of tags of the current user +``` +getTagUser(): Promise +``` + +Return the list of all the tags that has the current user. If the current user has no tag, return an empty list. If there is no connection with the room, return nothing. + +Example : +```javascript +WA.getTagUser().then((tagList) => { + ... +}); +``` + diff --git a/front/src/Api/Events/IframeEvent.ts b/front/src/Api/Events/IframeEvent.ts index 8383cfbd..114cbb90 100644 --- a/front/src/Api/Events/IframeEvent.ts +++ b/front/src/Api/Events/IframeEvent.ts @@ -15,6 +15,7 @@ import type { UserInputChatEvent } from './UserInputChatEvent'; import type { DataLayerEvent } from "./DataLayerEvent"; import type { LayerEvent } from './LayerEvent'; import type { SetPropertyEvent } from "./setPropertyEvent"; +import type { TagEvent } from "./TagEvent"; export interface TypedMessageEvent extends MessageEvent { data: T @@ -36,11 +37,11 @@ export type IframeEventMap = { displayBubble: null removeBubble: null onPlayerMove: undefined - onDataLayerChange: undefined showLayer: LayerEvent hideLayer: LayerEvent setProperty: SetPropertyEvent getDataLayer: undefined + getTag: undefined } export interface IframeEvent { type: T; @@ -60,6 +61,7 @@ export interface IframeResponseEventMap { hasPlayerMoved: HasPlayerMovedEvent dataLayer: DataLayerEvent menuItemClicked: MenuItemClickedEvent + tagList: TagEvent } export interface IframeResponseEvent { type: T; diff --git a/front/src/Api/Events/TagEvent.ts b/front/src/Api/Events/TagEvent.ts new file mode 100644 index 00000000..66665403 --- /dev/null +++ b/front/src/Api/Events/TagEvent.ts @@ -0,0 +1,10 @@ +import * as tg from "generic-type-guard"; + +export const isTagEvent = + new tg.IsInterface().withProperties({ + list: tg.isArray(tg.isString), + }).get(); +/** + * A message sent from the iFrame to the game to show/hide a layer. + */ +export type TagEvent = tg.GuardedType; \ No newline at end of file diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index 07246333..35ef6341 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -19,6 +19,7 @@ import { Math } from 'phaser'; import type { DataLayerEvent } from "./Events/DataLayerEvent"; import { isMenuItemRegisterEvent } from './Events/MenuItemRegisterEvent'; import type { MenuItemClickedEvent } from './Events/MenuItemClickedEvent'; +import type { TagEvent } from "./Events/TagEvent"; /** @@ -77,6 +78,10 @@ class IframeListener { private readonly _registerMenuCommandStream: Subject = new Subject(); public readonly registerMenuCommandStream = this._registerMenuCommandStream.asObservable(); + + private readonly _tagListStream: Subject = new Subject(); + public readonly tagListStream = this._tagListStream.asObservable(); + private readonly iframes = new Set(); private readonly scripts = new Map(); private sendPlayerMove: boolean = false; @@ -145,12 +150,21 @@ class IframeListener { this._dataLayerChangeStream.next(); } else if (payload.type == "registerMenuCommand" && isMenuItemRegisterEvent(payload.data)) { this._registerMenuCommandStream.next(payload.data.menutItem) + } else if (payload.type == "getTag") { + this._tagListStream.next(); } } }, false); } + sendUserTagList(tagList: TagEvent){ + this.postMessage({ + 'type' : 'tagList', + 'data' : tagList + }) + } + sendDataLayerEvent(dataLayerEvent: DataLayerEvent) { this.postMessage({ 'type' : 'dataLayer', diff --git a/front/src/Connexion/RoomConnection.ts b/front/src/Connexion/RoomConnection.ts index 6b2c63af..1cb4a97d 100644 --- a/front/src/Connexion/RoomConnection.ts +++ b/front/src/Connexion/RoomConnection.ts @@ -598,4 +598,11 @@ export class RoomConnection implements RoomConnection { public isAdmin(): boolean { return this.hasTag('admin'); } + + public getAllTag() : string[] { + this.tags.push('TEST'); + this.tags.push('TEST 2'); + this.tags.push('TEST 3'); + return this.tags; + } } diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 9150b4c1..dee5eb53 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -903,6 +903,13 @@ ${escapedMessage} iframeListener.sendDataLayerEvent({data: this.gameMap.getMap()}); })) + this.iframeSubscriptionList.push(iframeListener.tagListStream.subscribe(()=> { + if (this.connection === undefined) { + return; + } + iframeListener.sendUserTagList({list: this.connection.getAllTag()}); + })) + } private setPropertyLayer(layerName: string, propertyName: string, propertyValue: string | number | boolean | undefined): void { diff --git a/front/src/iframe_api.ts b/front/src/iframe_api.ts index 00977157..517248ed 100644 --- a/front/src/iframe_api.ts +++ b/front/src/iframe_api.ts @@ -17,6 +17,7 @@ import { DataLayerEvent, isDataLayerEvent } from "./Api/Events/DataLayerEvent"; import type { ITiledMap } from "./Phaser/Map/ITiledMap"; import type { MenuItemRegisterEvent } from "./Api/Events/MenuItemRegisterEvent"; import { isMenuItemClickedEvent } from "./Api/Events/MenuItemClickedEvent"; +import {TagEvent, isTagEvent} from "./Api/Events/TagEvent"; interface WorkAdventureApi { sendChatMessage(message: string, author: string): void; @@ -45,10 +46,10 @@ interface WorkAdventureApi { getRoomId(): Promise; getStartLayerName(): Promise; getNickName(): Promise; - + getTagUser(): Promise; + getMap(): Promise onPlayerMove(callback: (playerMovedEvent: HasPlayerMovedEvent) => void): void - getMap(): Promise } declare global { @@ -128,8 +129,19 @@ function getDataLayer(): Promise { }) } +function getTag(): Promise { + return new Promise((resolver, thrower) => { + tagResolver.push((resolver)); + postToParent({ + type: "getTag", + data: undefined + }) + }) +} + const gameStateResolver: Array<(event: GameStateEvent) => void> = [] const dataLayerResolver: Array<(event: DataLayerEvent) => void> = [] +const tagResolver: Array<(event : TagEvent) => void> = [] let immutableData: GameStateEvent; const callbackPlayerMoved: { [type: string]: HasPlayerMovedEventCallback | ((arg?: HasPlayerMovedEvent | never) => void) } = {} @@ -151,6 +163,11 @@ window.WA = { }) }, + getTagUser(): Promise { + return getTag().then((res) => { + return res.list; + }) + }, getMap(): Promise { return getDataLayer().then((res) => { @@ -389,6 +406,12 @@ window.addEventListener('message', message => { if (callback) { callback(payload.data.menuItem) } + } else { + if (payload.type == "tagList" && isTagEvent(payloadData)) { + tagResolver.forEach(resolver => { + resolver(payloadData); + }) + } } } diff --git a/maps/tests/Metadata/TagList.html b/maps/tests/Metadata/TagList.html new file mode 100644 index 00000000..73bdc368 --- /dev/null +++ b/maps/tests/Metadata/TagList.html @@ -0,0 +1,19 @@ + + + + + + + + +
+ + \ No newline at end of file diff --git a/maps/tests/Metadata/TagList.json b/maps/tests/Metadata/TagList.json new file mode 100644 index 00000000..cced49a3 --- /dev/null +++ b/maps/tests/Metadata/TagList.json @@ -0,0 +1,254 @@ +{ "compressionlevel":-1, + "height":10, + "infinite":false, + "layers":[ + { + "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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":1, + "name":"start", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "data":[33, 34, 34, 34, 34, 34, 34, 34, 34, 35, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 49, 50, 50, 50, 50, 50, 50, 50, 50, 51], + "height":10, + "id":2, + "name":"bottom", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "data":[0, 0, 0, 0, 0, 128, 128, 128, 128, 128, 0, 0, 0, 0, 0, 128, 128, 128, 128, 128, 0, 0, 0, 0, 0, 128, 128, 128, 128, 128, 0, 0, 0, 0, 0, 128, 128, 128, 128, 128, 0, 0, 0, 0, 0, 128, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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":4, + "name":"metadata", + "opacity":1, + "properties":[ + { + "name":"openWebsite", + "type":"string", + "value":"TagList.html" + }, + { + "name":"openWebsiteAllowApi", + "type":"bool", + "value":true + }], + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":5, + "name":"floorLayer", + "objects":[ + { + "height":131.903791109293, + "id":1, + "name":"", + "rotation":0, + "text": + { + "fontfamily":"Sans Serif", + "pixelsize":9, + "text":"Test : \nWalk on the grass, an iframe open, click on the 'Get Tag List' button.\nResult : \nThe list of the tag is displayed in the iframe.\n\n\n", + "wrap":true + }, + "type":"", + "visible":true, + "width":305.097705765524, + "x":14.750638909983, + "y":188.268561247737 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":10, + "nextobjectid":2, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":32, + "tilesets":[ + { + "columns":8, + "firstgid":1, + "image":"tileset_dungeon.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"TDungeon", + "spacing":0, + "tilecount":64, + "tileheight":32, + "tiles":[ + { + "id":0, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":2, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":3, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":4, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":8, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":9, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":10, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":11, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":12, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":16, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":17, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":18, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":19, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":20, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }], + "tilewidth":32 + }, + { + "columns":8, + "firstgid":65, + "image":"floortileset.png", + "imageheight":288, + "imagewidth":256, + "margin":0, + "name":"Floor", + "spacing":0, + "tilecount":72, + "tileheight":32, + "tilewidth":32 + }], + "tilewidth":32, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file From 3506063e65a2b8f62c3c4faec897bf1dffa2e62b Mon Sep 17 00:00:00 2001 From: GRL Date: Thu, 20 May 2021 17:09:10 +0200 Subject: [PATCH 26/46] first step on loading a tileset from a script --- front/src/Api/Events/IframeEvent.ts | 2 + front/src/Api/Events/TilesetEvent.ts | 15 ++ front/src/Api/IframeListener.ts | 8 +- front/src/Phaser/Game/GameMap.ts | 8 +- front/src/Phaser/Game/GameScene.ts | 9 +- front/src/iframe_api.ts | 16 ++ maps/tests/Metadata/ScriptMap.json | 219 +++++++++++++++++++++++++++ maps/tests/Metadata/script.js | 1 + maps/tests/iframe.html | 1 + 9 files changed, 276 insertions(+), 3 deletions(-) create mode 100644 front/src/Api/Events/TilesetEvent.ts create mode 100644 maps/tests/Metadata/ScriptMap.json create mode 100644 maps/tests/Metadata/script.js diff --git a/front/src/Api/Events/IframeEvent.ts b/front/src/Api/Events/IframeEvent.ts index 114cbb90..1ee7d1fb 100644 --- a/front/src/Api/Events/IframeEvent.ts +++ b/front/src/Api/Events/IframeEvent.ts @@ -16,6 +16,7 @@ import type { DataLayerEvent } from "./DataLayerEvent"; import type { LayerEvent } from './LayerEvent'; import type { SetPropertyEvent } from "./setPropertyEvent"; import type { TagEvent } from "./TagEvent"; +import type { TilesetEvent } from "./TilesetEvent"; export interface TypedMessageEvent extends MessageEvent { data: T @@ -42,6 +43,7 @@ export type IframeEventMap = { setProperty: SetPropertyEvent getDataLayer: undefined getTag: undefined + tilsetEvent: TilesetEvent } export interface IframeEvent { type: T; diff --git a/front/src/Api/Events/TilesetEvent.ts b/front/src/Api/Events/TilesetEvent.ts new file mode 100644 index 00000000..eab33bf7 --- /dev/null +++ b/front/src/Api/Events/TilesetEvent.ts @@ -0,0 +1,15 @@ +import * as tg from "generic-type-guard"; + +export const isTilesetEvent = + new tg.IsInterface().withProperties({ + name : tg.isString, + imgUrl : tg.isString, + tilewidth : tg.isNumber, + tileheight : tg.isNumber, + margin : tg.isNumber, + spacing : tg.isNumber, + }).get(); +/** + * A message sent from the iFrame to the game to show/hide a layer. + */ +export type TilesetEvent = tg.GuardedType; \ No newline at end of file diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index 35ef6341..8af0949f 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -20,6 +20,7 @@ import type { DataLayerEvent } from "./Events/DataLayerEvent"; import { isMenuItemRegisterEvent } from './Events/MenuItemRegisterEvent'; import type { MenuItemClickedEvent } from './Events/MenuItemClickedEvent'; import type { TagEvent } from "./Events/TagEvent"; +import { isTilesetEvent, TilesetEvent } from "./Events/TilesetEvent"; /** @@ -79,9 +80,12 @@ class IframeListener { private readonly _registerMenuCommandStream: Subject = new Subject(); public readonly registerMenuCommandStream = this._registerMenuCommandStream.asObservable(); - private readonly _tagListStream: Subject = new Subject(); + private readonly _tagListStream: Subject = new Subject(); public readonly tagListStream = this._tagListStream.asObservable(); + private readonly _tilesetLoaderStream: Subject = new Subject(); + public readonly tilesetLoaderStream = this._tilesetLoaderStream.asObservable(); + private readonly iframes = new Set(); private readonly scripts = new Map(); private sendPlayerMove: boolean = false; @@ -152,6 +156,8 @@ class IframeListener { this._registerMenuCommandStream.next(payload.data.menutItem) } else if (payload.type == "getTag") { this._tagListStream.next(); + } else if (payload.type == "tilsetEvent" && isTilesetEvent(payload.data)) { + this._tilesetLoaderStream.next(payload.data); } } }, false); diff --git a/front/src/Phaser/Game/GameMap.ts b/front/src/Phaser/Game/GameMap.ts index 24ca60c7..d63a67e0 100644 --- a/front/src/Phaser/Game/GameMap.ts +++ b/front/src/Phaser/Game/GameMap.ts @@ -1,6 +1,5 @@ import type {ITiledMap, ITiledMapLayer, ITiledMapTileLayer} from "../Map/ITiledMap"; import { flattenGroupLayersMap } from "../Map/LayersFlattener"; -import {iframeListener} from "../../Api/IframeListener"; import TilemapLayer = Phaser.Tilemaps.TilemapLayer; export type PropertyChangeCallback = (newValue: string | number | boolean | undefined, oldValue: string | number | boolean | undefined, allProps: Map) => void; @@ -151,4 +150,11 @@ export class GameMap { return undefined; } + public addTerrain(terrain : Phaser.Tilemaps.Tileset): void { + console.log('Add'); + for (const phaserLayer of this.phaserLayers) { + phaserLayer.tileset.push(terrain); + } + } + } diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index dee5eb53..120bb303 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -500,7 +500,7 @@ export class GameScene extends DirtyScene implements CenterListener { if (!this.room.isDisconnected()) { this.connect(); } - + console.log('display'); document.addEventListener('visibilitychange', this.onVisibilityChangeCallback); } @@ -910,6 +910,13 @@ ${escapedMessage} iframeListener.sendUserTagList({list: this.connection.getAllTag()}); })) + this.iframeSubscriptionList.push(iframeListener.tilesetLoaderStream.subscribe((tileset) => { + //this.load.tilemapTiledJSON('logo', tileset.imgUrl); + this.load.image('logo', tileset.imgUrl); + this.Terrains.push(this.Map.addTilesetImage(tileset.name, tileset.imgUrl, tileset.tilewidth, tileset.tileheight, tileset.margin, tileset.spacing)); + this.gameMap.addTerrain(this.Terrains[this.Terrains.length - 1]); + })) + } private setPropertyLayer(layerName: string, propertyName: string, propertyValue: string | number | boolean | undefined): void { diff --git a/front/src/iframe_api.ts b/front/src/iframe_api.ts index 517248ed..5a3336a4 100644 --- a/front/src/iframe_api.ts +++ b/front/src/iframe_api.ts @@ -18,6 +18,7 @@ import type { ITiledMap } from "./Phaser/Map/ITiledMap"; import type { MenuItemRegisterEvent } from "./Api/Events/MenuItemRegisterEvent"; import { isMenuItemClickedEvent } from "./Api/Events/MenuItemClickedEvent"; import {TagEvent, isTagEvent} from "./Api/Events/TagEvent"; +import type { TilesetEvent } from "./Api/Events/TilesetEvent"; interface WorkAdventureApi { sendChatMessage(message: string, author: string): void; @@ -48,6 +49,7 @@ interface WorkAdventureApi { getNickName(): Promise; getTagUser(): Promise; getMap(): Promise + loadTileset(name: string, imgUrl : string, tilewidth : number, tileheight : number, margin : number, spacing : number): void; onPlayerMove(callback: (playerMovedEvent: HasPlayerMovedEvent) => void): void } @@ -163,6 +165,20 @@ window.WA = { }) }, + loadTileset(name: string, imgUrl : string, tilewidth : number, tileheight : number, margin : number, spacing : number): void { + postToParent({ + type: "tilsetEvent", + data: { + name: name, + imgUrl: imgUrl, + tilewidth: tilewidth, + tileheight: tileheight, + margin: margin, + spacing: spacing + } as TilesetEvent + }) + }, + getTagUser(): Promise { return getTag().then((res) => { return res.list; diff --git a/maps/tests/Metadata/ScriptMap.json b/maps/tests/Metadata/ScriptMap.json new file mode 100644 index 00000000..93972a73 --- /dev/null +++ b/maps/tests/Metadata/ScriptMap.json @@ -0,0 +1,219 @@ +{ "compressionlevel":-1, + "height":10, + "infinite":false, + "layers":[ + { + "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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":1, + "name":"start", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "data":[33, 34, 34, 34, 34, 34, 34, 34, 34, 35, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 49, 50, 50, 50, 50, 50, 50, 50, 50, 51], + "height":10, + "id":2, + "name":"bottom", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":5, + "name":"floorLayer", + "objects":[], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":10, + "nextobjectid":2, + "orientation":"orthogonal", + "properties":[ + { + "name":"script", + "type":"string", + "value":"script.js" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":32, + "tilesets":[ + { + "columns":8, + "firstgid":1, + "image":"tileset_dungeon.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"TDungeon", + "spacing":0, + "tilecount":64, + "tileheight":32, + "tiles":[ + { + "id":0, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":2, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":3, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":4, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":8, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":9, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":10, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":11, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":12, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":16, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":17, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":18, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":19, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":20, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }], + "tilewidth":32 + }, + { + "columns":8, + "firstgid":65, + "image":"floortileset.png", + "imageheight":288, + "imagewidth":256, + "margin":0, + "name":"Floor", + "spacing":0, + "tilecount":72, + "tileheight":32, + "tilewidth":32 + }], + "tilewidth":32, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/maps/tests/Metadata/script.js b/maps/tests/Metadata/script.js new file mode 100644 index 00000000..d04d7952 --- /dev/null +++ b/maps/tests/Metadata/script.js @@ -0,0 +1 @@ +console.log('script chargé !!!!!'); \ No newline at end of file diff --git a/maps/tests/iframe.html b/maps/tests/iframe.html index aa8e55ec..e0ba05d6 100644 --- a/maps/tests/iframe.html +++ b/maps/tests/iframe.html @@ -17,6 +17,7 @@ chatDiv.innerText = message; document.getElementById('chatSent').append(chatDiv); })); + WA.loadTileset('TEST', 'https://gparant.github.io/tcm-client/TCM/paris-map/tileset1.png', 32, 32, 0, 0); From 1110f4fb7f6132a07c808ccc2522b1ce524420af Mon Sep 17 00:00:00 2001 From: GRL Date: Fri, 21 May 2021 16:24:48 +0200 Subject: [PATCH 27/46] Revert "Merge branch 'update-game-tiles' into metadataScriptingApi" This reverts commit 796a9418d3b6c356c5c25bfbc4503207b08572c4, reversing changes made to 3506063e65a2b8f62c3c4faec897bf1dffa2e62b. --- front/src/Api/Events/IframeEvent.ts | 4 +-- front/src/Api/Events/UpdateTileEvent.ts | 15 --------- front/src/Api/IframeListener.ts | 14 ++++---- front/src/Phaser/Game/GameScene.ts | 45 +++---------------------- front/src/Phaser/Map/ITiledMap.ts | 21 +++++------- front/src/iframe_api.ts | 22 +++++++++++- 6 files changed, 42 insertions(+), 79 deletions(-) delete mode 100644 front/src/Api/Events/UpdateTileEvent.ts diff --git a/front/src/Api/Events/IframeEvent.ts b/front/src/Api/Events/IframeEvent.ts index 6a76f870..1ee7d1fb 100644 --- a/front/src/Api/Events/IframeEvent.ts +++ b/front/src/Api/Events/IframeEvent.ts @@ -17,7 +17,6 @@ import type { LayerEvent } from './LayerEvent'; import type { SetPropertyEvent } from "./setPropertyEvent"; import type { TagEvent } from "./TagEvent"; import type { TilesetEvent } from "./TilesetEvent"; -import type { UpdateTileEvent } from "./UpdateTileEvent"; export interface TypedMessageEvent extends MessageEvent { data: T @@ -44,8 +43,7 @@ export type IframeEventMap = { setProperty: SetPropertyEvent getDataLayer: undefined getTag: undefined - tilesetEvent: TilesetEvent - updateTileEvent: UpdateTileEvent + tilsetEvent: TilesetEvent } export interface IframeEvent { type: T; diff --git a/front/src/Api/Events/UpdateTileEvent.ts b/front/src/Api/Events/UpdateTileEvent.ts deleted file mode 100644 index 5817622c..00000000 --- a/front/src/Api/Events/UpdateTileEvent.ts +++ /dev/null @@ -1,15 +0,0 @@ -import * as tg from "generic-type-guard"; - - -export const isUpdateTileEvent = tg.isArray( - new tg.IsInterface().withProperties({ - x: tg.isNumber, - y: tg.isNumber, - tile: tg.isUnion(tg.isNumber, tg.isString), - layer: 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 UpdateTileEvent = tg.GuardedType; \ No newline at end of file diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index 2406e92d..8af0949f 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -21,7 +21,6 @@ import { isMenuItemRegisterEvent } from './Events/MenuItemRegisterEvent'; import type { MenuItemClickedEvent } from './Events/MenuItemClickedEvent'; import type { TagEvent } from "./Events/TagEvent"; import { isTilesetEvent, TilesetEvent } from "./Events/TilesetEvent"; -import { isUpdateTileEvent, UpdateTileEvent } from './Events/UpdateTileEvent'; /** @@ -36,6 +35,12 @@ class IframeListener { private readonly _openPopupStream: Subject = new Subject(); public readonly openPopupStream = this._openPopupStream.asObservable(); + private readonly _openTabStream: Subject = new Subject(); + public readonly openTabStream = this._openTabStream.asObservable(); + + private readonly _goToPageStream: Subject = new Subject(); + public readonly goToPageStream = this._goToPageStream.asObservable(); + private readonly _openCoWebSiteStream: Subject = new Subject(); public readonly openCoWebSiteStream = this._openCoWebSiteStream.asObservable(); @@ -81,9 +86,6 @@ class IframeListener { private readonly _tilesetLoaderStream: Subject = new Subject(); public readonly tilesetLoaderStream = this._tilesetLoaderStream.asObservable(); - private readonly _updateTileStream: Subject = new Subject(); - public readonly updateTileStream = this._updateTileStream.asObservable(); - private readonly iframes = new Set(); private readonly scripts = new Map(); private sendPlayerMove: boolean = false; @@ -154,10 +156,8 @@ class IframeListener { this._registerMenuCommandStream.next(payload.data.menutItem) } else if (payload.type == "getTag") { this._tagListStream.next(); - } else if (payload.type == "tilesetEvent" && isTilesetEvent(payload.data)) { + } else if (payload.type == "tilsetEvent" && isTilesetEvent(payload.data)) { this._tilesetLoaderStream.next(payload.data); - } else if (payload.type == "updateTileEvent" && isUpdateTileEvent(payload.data)) { - this._updateTileStream.next(payload.data) } } }, false); diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 33013454..120bb303 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -1,4 +1,4 @@ -import { gameManager } from "./GameManager"; +import {gameManager} from "./GameManager"; import type { GroupCreatedUpdatedMessageInterface, MessageUserJoined, @@ -80,7 +80,6 @@ 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 EVENT_TYPE = Phaser.Scenes.Events import type { Subscription } from "rxjs"; import { worldFullMessageStream } from "../../Connexion/WorldFullMessageStream"; import { lazyLoadCompanionResource } from "../Companion/CompanionTexturesLoadingManager"; @@ -186,7 +185,7 @@ export class GameScene extends DirtyScene implements CenterListener { private characterLayers!: string[]; private companion!: string | null; private messageSubscription: Subscription | null = null; - private popUpElements: Map = new Map(); + private popUpElements : Map = new Map(); private originalMapUrl: string | undefined; private pinchManager: PinchManager | undefined; private physicsEnabled: boolean = true; @@ -911,33 +910,12 @@ ${escapedMessage} iframeListener.sendUserTagList({list: this.connection.getAllTag()}); })) -/* this.iframeSubscriptionList.push(iframeListener.tilesetLoaderStream.subscribe((tileset) => { //this.load.tilemapTiledJSON('logo', tileset.imgUrl); this.load.image('logo', tileset.imgUrl); this.Terrains.push(this.Map.addTilesetImage(tileset.name, tileset.imgUrl, tileset.tilewidth, tileset.tileheight, tileset.margin, tileset.spacing)); this.gameMap.addTerrain(this.Terrains[this.Terrains.length - 1]); })) -*/ - - this.iframeSubscriptionList.push(iframeListener.updateTileStream.subscribe(event => { - for (const eventTile of event) { - const layer = this.gameMap.findPhaserLayer(eventTile.layer); - if (layer) { - const tile = layer.getTileAt(eventTile.x, eventTile.y) - if (typeof eventTile.tile == "string") { - const tileIndex = this.getIndexForTileType(eventTile.tile); - if (tileIndex) { - tile.index = tileIndex - } else { - return - } - } else { - tile.index = eventTile.tile - } - } - } - })) } @@ -967,19 +945,6 @@ ${escapedMessage} } - private getIndexForTileType(tileType: string): number | null { - for (const tileset of this.mapFile.tilesets) { - if (tileset.tiles) { - for (const tilesetTile of tileset.tiles) { - if (tilesetTile.type == tileType) { - return tileset.firstgid + tilesetTile.id - } - } - } - } - return null - } - private getMapDirUrl(): string { return this.MapUrlFile.substr(0, this.MapUrlFile.lastIndexOf('/')); } @@ -987,8 +952,8 @@ ${escapedMessage} private onMapExit(exitKey: string) { if (this.mapTransitioning) return; this.mapTransitioning = true; - const { roomId, hash } = Room.getIdFromIdentifier(exitKey, this.MapUrlFile, this.instance); - if (!roomId) throw new Error('Could not find the room from its exit key: ' + exitKey); + const {roomId, hash} = Room.getIdFromIdentifier(exitKey, this.MapUrlFile, this.instance); + if (!roomId) throw new Error('Could not find the room from its exit key: '+exitKey); urlManager.pushStartLayerNameToUrl(hash); const menuScene: MenuScene = this.scene.get(MenuSceneName) as MenuScene menuScene.reset() @@ -1190,7 +1155,7 @@ ${escapedMessage} this.physics.add.collider(this.CurrentPlayer, phaserLayer, (object1: GameObject, object2: GameObject) => { //this.CurrentPlayer.say("Collision with layer : "+ (object2 as Tile).layer.name) }); - phaserLayer.setCollisionByProperty({ collides: true }); + phaserLayer.setCollisionByProperty({collides: true}); if (DEBUG_MODE) { //debug code to see the collision hitbox of the object in the top layer phaserLayer.renderDebug(this.add.graphics(), { diff --git a/front/src/Phaser/Map/ITiledMap.ts b/front/src/Phaser/Map/ITiledMap.ts index 2f5d45bc..d381e9d4 100644 --- a/front/src/Phaser/Map/ITiledMap.ts +++ b/front/src/Phaser/Map/ITiledMap.ts @@ -36,7 +36,7 @@ export interface ITiledMap { export interface ITiledMapLayerProperty { name: string; type: string; - value: string | boolean | number | undefined; + value: string|boolean|number|undefined; } /*export interface ITiledMapLayerBooleanProperty { @@ -65,7 +65,7 @@ export interface ITiledMapGroupLayer { export interface ITiledMapTileLayer { id?: number, - data: number[] | string; + data: number[]|string; height: number; name: string; opacity: number; @@ -117,7 +117,7 @@ export interface ITiledMapObject { gid: number; height: number; name: string; - properties: { [key: string]: string }; + properties: {[key: string]: string}; rotation: number; type: string; visible: boolean; @@ -133,12 +133,12 @@ export interface ITiledMapObject { /** * Polygon points */ - polygon: { x: number, y: number }[]; + polygon: {x: number, y: number}[]; /** * Polyline points */ - polyline: { x: number, y: number }[]; + polyline: {x: number, y: number}[]; text?: ITiledText } @@ -152,7 +152,7 @@ export interface ITiledText { underline?: boolean, italic?: boolean, strikeout?: boolean, - halign?: "center" | "right" | "justify" | "left" + halign?: "center"|"right"|"justify"|"left" } export interface ITiledTileSet { @@ -163,14 +163,14 @@ export interface ITiledTileSet { imagewidth: number; margin: number; name: string; - properties: { [key: string]: string }; + properties: {[key: string]: string}; spacing: number; tilecount: number; tileheight: number; tilewidth: number; transparentcolor: string; terrains: ITiledMapTerrain[]; - tiles: Array; + tiles: {[key: string]: { terrain: number[] }}; /** * Refers to external tileset file (should be JSON) @@ -178,11 +178,6 @@ export interface ITiledTileSet { source: string; } -export interface ITile { - id: number, - type?: string -} - export interface ITiledMapTerrain { name: string; tile: number; diff --git a/front/src/iframe_api.ts b/front/src/iframe_api.ts index f253c48d..5a3336a4 100644 --- a/front/src/iframe_api.ts +++ b/front/src/iframe_api.ts @@ -26,6 +26,8 @@ interface WorkAdventureApi { onEnterZone(name: string, callback: () => void): void; onLeaveZone(name: string, callback: () => void): void; openPopup(targetObject: string, message: string, buttons: ButtonDescriptor[]): Popup; + openTab(url: string): void; + goToPage(url: string): void; openCoWebSite(url: string): void; closeCoWebSite(): void; disablePlayerControls() : void; @@ -165,7 +167,7 @@ window.WA = { loadTileset(name: string, imgUrl : string, tilewidth : number, tileheight : number, margin : number, spacing : number): void { postToParent({ - type: "updateTileEvent", + type: "tilsetEvent", data: { name: name, imgUrl: imgUrl, @@ -274,6 +276,24 @@ window.WA = { window.parent.postMessage({ 'type': 'removeBubble' }, '*'); }, + openTab(url: string): void { + window.parent.postMessage({ + "type": 'openTab', + "data": { + url + } as OpenTabEvent + }, '*'); + }, + + goToPage(url: string): void { + window.parent.postMessage({ + "type": 'goToPage', + "data": { + url + } as GoToPageEvent + }, '*'); + }, + openCoWebSite(url: string): void { window.parent.postMessage({ "type": 'openCoWebSite', From a3165a0540f8aef8477c62e6b4d4dad6adac1150 Mon Sep 17 00:00:00 2001 From: GRL Date: Tue, 25 May 2021 09:39:04 +0200 Subject: [PATCH 28/46] pause for loading tileset on the fly --- front/src/Phaser/Game/GameScene.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 5b049ebc..3df7e093 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -910,12 +910,12 @@ ${escapedMessage} iframeListener.sendUserTagList({list: this.connection.getAllTag()}); })) - this.iframeSubscriptionList.push(iframeListener.tilesetLoaderStream.subscribe((tileset) => { +/* this.iframeSubscriptionList.push(iframeListener.tilesetLoaderStream.subscribe((tileset) => { //this.load.tilemapTiledJSON('logo', tileset.imgUrl); this.load.image('logo', tileset.imgUrl); this.Terrains.push(this.Map.addTilesetImage(tileset.name, tileset.imgUrl, tileset.tilewidth, tileset.tileheight, tileset.margin, tileset.spacing)); this.gameMap.addTerrain(this.Terrains[this.Terrains.length - 1]); - })) + }))*/ } From b18b2fe0e31c5c6481f184b754c581ac2e4cd6a7 Mon Sep 17 00:00:00 2001 From: GRL Date: Tue, 25 May 2021 09:50:59 +0200 Subject: [PATCH 29/46] preparation for merge with metadataScriptApi --- .../{ApiUpdateTileEvent.ts => ChangeTileEvent.ts} | 6 ++---- front/src/Api/IframeListener.ts | 6 +++--- front/src/Phaser/Game/GameScene.ts | 10 +++++----- 3 files changed, 10 insertions(+), 12 deletions(-) rename front/src/Api/Events/{ApiUpdateTileEvent.ts => ChangeTileEvent.ts} (70%) diff --git a/front/src/Api/Events/ApiUpdateTileEvent.ts b/front/src/Api/Events/ChangeTileEvent.ts similarity index 70% rename from front/src/Api/Events/ApiUpdateTileEvent.ts rename to front/src/Api/Events/ChangeTileEvent.ts index 094596a4..5a9183ca 100644 --- a/front/src/Api/Events/ApiUpdateTileEvent.ts +++ b/front/src/Api/Events/ChangeTileEvent.ts @@ -1,9 +1,7 @@ - import * as tg from "generic-type-guard"; -export const updateTile = "updateTile" -export const isUpdateTileEvent = tg.isArray( +export const isChangeTileEvent = tg.isArray( new tg.IsInterface().withProperties({ x: tg.isNumber, y: tg.isNumber, @@ -14,4 +12,4 @@ export const isUpdateTileEvent = tg.isArray( /** * A message sent from the game to the iFrame when a user enters or leaves a zone marked with the "zone" property. */ -export type UpdateTileEvent = tg.GuardedType; \ No newline at end of file +export type ChangeTileEvent = tg.GuardedType; \ No newline at end of file diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index f97e80ae..d59c9140 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -13,7 +13,7 @@ import { scriptUtils } from "./ScriptUtils"; import { GoToPageEvent, isGoToPageEvent } from "./Events/GoToPageEvent"; import { isOpenCoWebsite, OpenCoWebSiteEvent } from "./Events/OpenCoWebSiteEvent"; import { isLoadPageEvent } from './Events/LoadPageEvent'; -import { isUpdateTileEvent, UpdateTileEvent } from './Events/ApiUpdateTileEvent'; +import { isChangeTileEvent, ChangeTileEvent } from './Events/ChangeTileEvent'; /** @@ -58,7 +58,7 @@ class IframeListener { private readonly _removeBubbleStream: Subject = new Subject(); public readonly removeBubbleStream = this._removeBubbleStream.asObservable(); - private readonly _updateTileEvent: Subject = new Subject(); + private readonly _updateTileEvent: Subject = new Subject(); public readonly updateTileEvent = this._updateTileEvent.asObservable(); private readonly iframes = new Set(); @@ -114,7 +114,7 @@ class IframeListener { this._removeBubbleStream.next(); } else if (payload.type === 'loadPage' && isLoadPageEvent(payload.data)) { this._loadPageStream.next(payload.data.url); - } else if (payload.type == "updateTile" && isUpdateTileEvent(payload.data)) { + } else if (payload.type == "updateTile" && isChangeTileEvent(payload.data)) { this._updateTileEvent.next(payload.data) } } diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 674087e0..fc5bf80f 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -134,7 +134,7 @@ export class GameScene extends ResizableScene implements CenterListener { MapPlayers!: Phaser.Physics.Arcade.Group; MapPlayersByKey: Map = new Map(); Map!: Phaser.Tilemaps.Tilemap; - Layers!: Array; + Layers!: Array; Objects!: Array; mapFile!: ITiledMap; groups: Map; @@ -395,12 +395,12 @@ export class GameScene extends ResizableScene implements CenterListener { this.physics.world.setBounds(0, 0, this.Map.widthInPixels, this.Map.heightInPixels); //add layer on map - this.Layers = new Array(); + this.Layers = new Array(); let depth = -2; for (const layer of this.gameMap.layersIterator) { if (layer.type === 'tilelayer') { - this.addLayer(this.Map.createStaticLayer(layer.name, this.Terrains, 0, 0).setDepth(depth)); + this.addLayer(this.Map.createLayer(layer.name, this.Terrains, 0, 0).setDepth(depth)); const exitSceneUrl = this.getExitSceneUrl(layer); if (exitSceneUrl !== undefined) { @@ -1105,13 +1105,13 @@ ${escapedMessage} this.cameras.main.setZoom(ZOOM_LEVEL); } - addLayer(Layer: Phaser.Tilemaps.StaticTilemapLayer) { + addLayer(Layer: Phaser.Tilemaps.TilemapLayer) { this.Layers.push(Layer); } createCollisionWithPlayer() { //add collision layer - this.Layers.forEach((Layer: Phaser.Tilemaps.StaticTilemapLayer) => { + this.Layers.forEach((Layer: Phaser.Tilemaps.TilemapLayer) => { this.physics.add.collider(this.CurrentPlayer, Layer, (object1: GameObject, object2: GameObject) => { //this.CurrentPlayer.say("Collision with layer : "+ (object2 as Tile).layer.name) }); From a7b09e91ba95dcd17207179c8b9dd1e6a313028d Mon Sep 17 00:00:00 2001 From: GRL Date: Tue, 25 May 2021 10:09:58 +0200 Subject: [PATCH 30/46] Revert "Merge branch 'update-game-tiles' into metadataScriptingApi" This reverts commit 428625e61b558004ae37385b21270fdf11864b2a, reversing changes made to a3165a0540f8aef8477c62e6b4d4dad6adac1150. --- front/src/Api/Events/ChangeTileEvent.ts | 15 --------------- front/src/Api/IframeListener.ts | 9 --------- 2 files changed, 24 deletions(-) delete mode 100644 front/src/Api/Events/ChangeTileEvent.ts diff --git a/front/src/Api/Events/ChangeTileEvent.ts b/front/src/Api/Events/ChangeTileEvent.ts deleted file mode 100644 index 5a9183ca..00000000 --- a/front/src/Api/Events/ChangeTileEvent.ts +++ /dev/null @@ -1,15 +0,0 @@ -import * as tg from "generic-type-guard"; - - -export const isChangeTileEvent = tg.isArray( - new tg.IsInterface().withProperties({ - x: tg.isNumber, - y: tg.isNumber, - tile: tg.isUnion(tg.isNumber, tg.isString), - layer: tg.isUnion(tg.isNumber, 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 ChangeTileEvent = tg.GuardedType; \ No newline at end of file diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index d14a3486..8af0949f 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -21,8 +21,6 @@ import { isMenuItemRegisterEvent } from './Events/MenuItemRegisterEvent'; import type { MenuItemClickedEvent } from './Events/MenuItemClickedEvent'; import type { TagEvent } from "./Events/TagEvent"; import { isTilesetEvent, TilesetEvent } from "./Events/TilesetEvent"; -import { isLoadPageEvent } from './Events/LoadPageEvent'; -import { isChangeTileEvent, ChangeTileEvent } from './Events/ChangeTileEvent'; /** @@ -88,9 +86,6 @@ class IframeListener { private readonly _tilesetLoaderStream: Subject = new Subject(); public readonly tilesetLoaderStream = this._tilesetLoaderStream.asObservable(); - private readonly _updateTileEvent: Subject = new Subject(); - public readonly updateTileEvent = this._updateTileEvent.asObservable(); - private readonly iframes = new Set(); private readonly scripts = new Map(); private sendPlayerMove: boolean = false; @@ -163,10 +158,6 @@ class IframeListener { this._tagListStream.next(); } else if (payload.type == "tilsetEvent" && isTilesetEvent(payload.data)) { this._tilesetLoaderStream.next(payload.data); - } else if (payload.type === 'loadPage' && isLoadPageEvent(payload.data)) { - this._loadPageStream.next(payload.data.url); - } else if (payload.type == "updateTile" && isChangeTileEvent(payload.data)) { - this._updateTileEvent.next(payload.data) } } }, false); From 343ad6ea9636838d12e6127f6a8aba59a9d3c324 Mon Sep 17 00:00:00 2001 From: GRL Date: Tue, 25 May 2021 10:11:25 +0200 Subject: [PATCH 31/46] Revert "preparation for merge with metadataScriptApi" This reverts commit b18b2fe0e31c5c6481f184b754c581ac2e4cd6a7. --- front/src/Api/Events/ApiUpdateTileEvent.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 front/src/Api/Events/ApiUpdateTileEvent.ts diff --git a/front/src/Api/Events/ApiUpdateTileEvent.ts b/front/src/Api/Events/ApiUpdateTileEvent.ts new file mode 100644 index 00000000..e69de29b From 36f0cd1a23206c14b718165875ba4d970def49ed Mon Sep 17 00:00:00 2001 From: GRL Date: Tue, 25 May 2021 10:11:27 +0200 Subject: [PATCH 32/46] Revert "pause for loading tileset on the fly" This reverts commit a3165a0540f8aef8477c62e6b4d4dad6adac1150. --- front/src/Phaser/Game/GameScene.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 3df7e093..5b049ebc 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -910,12 +910,12 @@ ${escapedMessage} iframeListener.sendUserTagList({list: this.connection.getAllTag()}); })) -/* this.iframeSubscriptionList.push(iframeListener.tilesetLoaderStream.subscribe((tileset) => { + this.iframeSubscriptionList.push(iframeListener.tilesetLoaderStream.subscribe((tileset) => { //this.load.tilemapTiledJSON('logo', tileset.imgUrl); this.load.image('logo', tileset.imgUrl); this.Terrains.push(this.Map.addTilesetImage(tileset.name, tileset.imgUrl, tileset.tilewidth, tileset.tileheight, tileset.margin, tileset.spacing)); this.gameMap.addTerrain(this.Terrains[this.Terrains.length - 1]); - }))*/ + })) } From d4bc999c54a2e7d9cb961c26d847777e9f0e8ad6 Mon Sep 17 00:00:00 2001 From: GRL Date: Tue, 25 May 2021 10:15:56 +0200 Subject: [PATCH 33/46] pause loading tileset on fly --- front/src/Api/Events/IframeEvent.ts | 2 +- front/src/Api/IframeListener.ts | 10 +++++----- front/src/Phaser/Game/GameScene.ts | 4 ++-- front/src/iframe_api.ts | 6 +++--- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/front/src/Api/Events/IframeEvent.ts b/front/src/Api/Events/IframeEvent.ts index 1ee7d1fb..8e4a76f5 100644 --- a/front/src/Api/Events/IframeEvent.ts +++ b/front/src/Api/Events/IframeEvent.ts @@ -43,7 +43,7 @@ export type IframeEventMap = { setProperty: SetPropertyEvent getDataLayer: undefined getTag: undefined - tilsetEvent: TilesetEvent + //tilsetEvent: TilesetEvent } export interface IframeEvent { type: T; diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index 8af0949f..647a95dc 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -20,7 +20,7 @@ import type { DataLayerEvent } from "./Events/DataLayerEvent"; import { isMenuItemRegisterEvent } from './Events/MenuItemRegisterEvent'; import type { MenuItemClickedEvent } from './Events/MenuItemClickedEvent'; import type { TagEvent } from "./Events/TagEvent"; -import { isTilesetEvent, TilesetEvent } from "./Events/TilesetEvent"; +//import { isTilesetEvent, TilesetEvent } from "./Events/TilesetEvent"; /** @@ -83,8 +83,8 @@ class IframeListener { private readonly _tagListStream: Subject = new Subject(); public readonly tagListStream = this._tagListStream.asObservable(); - private readonly _tilesetLoaderStream: Subject = new Subject(); - public readonly tilesetLoaderStream = this._tilesetLoaderStream.asObservable(); +/* private readonly _tilesetLoaderStream: Subject = new Subject(); + public readonly tilesetLoaderStream = this._tilesetLoaderStream.asObservable();*/ private readonly iframes = new Set(); private readonly scripts = new Map(); @@ -156,8 +156,8 @@ class IframeListener { this._registerMenuCommandStream.next(payload.data.menutItem) } else if (payload.type == "getTag") { this._tagListStream.next(); - } else if (payload.type == "tilsetEvent" && isTilesetEvent(payload.data)) { - this._tilesetLoaderStream.next(payload.data); +/* } else if (payload.type == "tilsetEvent" && isTilesetEvent(payload.data)) { + this._tilesetLoaderStream.next(payload.data);*/ } } }, false); diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 5b049ebc..3df7e093 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -910,12 +910,12 @@ ${escapedMessage} iframeListener.sendUserTagList({list: this.connection.getAllTag()}); })) - this.iframeSubscriptionList.push(iframeListener.tilesetLoaderStream.subscribe((tileset) => { +/* this.iframeSubscriptionList.push(iframeListener.tilesetLoaderStream.subscribe((tileset) => { //this.load.tilemapTiledJSON('logo', tileset.imgUrl); this.load.image('logo', tileset.imgUrl); this.Terrains.push(this.Map.addTilesetImage(tileset.name, tileset.imgUrl, tileset.tilewidth, tileset.tileheight, tileset.margin, tileset.spacing)); this.gameMap.addTerrain(this.Terrains[this.Terrains.length - 1]); - })) + }))*/ } diff --git a/front/src/iframe_api.ts b/front/src/iframe_api.ts index 5a3336a4..b2eac975 100644 --- a/front/src/iframe_api.ts +++ b/front/src/iframe_api.ts @@ -49,7 +49,7 @@ interface WorkAdventureApi { getNickName(): Promise; getTagUser(): Promise; getMap(): Promise - loadTileset(name: string, imgUrl : string, tilewidth : number, tileheight : number, margin : number, spacing : number): void; + //loadTileset(name: string, imgUrl : string, tilewidth : number, tileheight : number, margin : number, spacing : number): void; onPlayerMove(callback: (playerMovedEvent: HasPlayerMovedEvent) => void): void } @@ -165,7 +165,7 @@ window.WA = { }) }, - loadTileset(name: string, imgUrl : string, tilewidth : number, tileheight : number, margin : number, spacing : number): void { +/* loadTileset(name: string, imgUrl : string, tilewidth : number, tileheight : number, margin : number, spacing : number): void { postToParent({ type: "tilsetEvent", data: { @@ -177,7 +177,7 @@ window.WA = { spacing: spacing } as TilesetEvent }) - }, + },*/ getTagUser(): Promise { return getTag().then((res) => { From 7c44d747de474ea8c476ff77d75158ee7d1339f5 Mon Sep 17 00:00:00 2001 From: GRL78 <80678534+GRL78@users.noreply.github.com> Date: Tue, 25 May 2021 11:02:25 +0200 Subject: [PATCH 34/46] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: David Négrier --- docs/maps/api-reference.md | 22 ++++++--------- front/src/Api/Events/GameStateEvent.ts | 4 +-- front/src/Api/Events/HasPlayerMovedEvent.ts | 2 +- front/src/Phaser/Game/GameMap.ts | 31 ++------------------- 4 files changed, 14 insertions(+), 45 deletions(-) diff --git a/docs/maps/api-reference.md b/docs/maps/api-reference.md index 889ed3ac..30d0f1ea 100644 --- a/docs/maps/api-reference.md +++ b/docs/maps/api-reference.md @@ -258,7 +258,7 @@ WA.hideLayer('bottom'); WA.setProperty(layerName : string, propertyName : string, propertyValue : string | number | boolean | undefined) : void; ``` -Set the value of the "propertyName" property of the layer "layerName" at "propertyValue". If the property doesn't exist, create the property "propertyName" and set the value of the property at "propertyValue". +Set the value of the `propertyName` property of the layer `layerName` at `propertyValue`. If the property doesn't exist, create the property `propertyName` and set the value of the property at `propertyValue`. Example : @@ -266,12 +266,12 @@ Example : WA.setProperty('wikiLayer', 'openWebsite', 'https://www.wikipedia.org/'); ``` -### Listen player movement +### Listen to player movement ``` onPlayerMove(callback: HasPlayerMovedEventCallback): void; ``` -Listens to the movement of the current user and calls the callback. Send a event when current user stop moving, change direction and every 200ms when moving in the same direction. +Listens to the movement of the current user and calls the callback. Sends an event when the user stops moving, changes direction and every 200ms when moving in the same direction. The event has the following attributes : * **moving (boolean):** **true** when the current player is moving, **false** otherwise. @@ -281,7 +281,7 @@ The event has the following attributes : **callback:** the function that will be called when the current player is moving. It contains the event. -Exemple : +Example : ```javascript WA.onPlayerMove(console.log); ``` @@ -292,7 +292,7 @@ WA.onPlayerMove(console.log); getMap(): Promise ``` -Return a promise of an ITiledMap that contains the JSON file of the map plus the property set by a script. +Returns a promise that resolves to the JSON file of the map. Please note that if you modified the map (for instance by calling `WA.setProperty`, the data returned by `getMap` will contain those changes. Example : ```javascript @@ -360,23 +360,20 @@ WA.getStartLayerName().then((starLayerName) => {console.log(starLayerName)}); ``` registerMenuCommand(commandDescriptor: string, callback: (commandDescriptor: string) => void) ``` -Add a custom menu named "commandDescriptor" in the menu that call the callback when clicked. +Add a custom menu item containing the text `commandDescriptor`. A click on the menu will trigger the `callback`. Example : ```javascript -let chatbotEnabled = false -WA.registerMenuCommand('help', () => { - chatbotEnabled = true; - WA.onChatMessage ... +WA.registerMenuCommand('About', () => { + console.log("The About menu was clicked"); }); -``` ### Getting the list of tags of the current user ``` getTagUser(): Promise ``` -Return the list of all the tags that has the current user. If the current user has no tag, return an empty list. If there is no connection with the room, return nothing. +Returns the tags of the current user. If the current user has no tag, returns an empty list. Example : ```javascript @@ -384,4 +381,3 @@ WA.getTagUser().then((tagList) => { ... }); ``` - diff --git a/front/src/Api/Events/GameStateEvent.ts b/front/src/Api/Events/GameStateEvent.ts index 72e40898..946febe8 100644 --- a/front/src/Api/Events/GameStateEvent.ts +++ b/front/src/Api/Events/GameStateEvent.ts @@ -23,6 +23,6 @@ export const isGameStateEvent = startLayerName: tg.isUnion(tg.isString, tg.isNull) }).get(); /** - * A message sent from the game to the iFrame when the gameState is got by the script + * A message sent from the game to the iFrame when the gameState is received by the script */ -export type GameStateEvent = tg.GuardedType; \ No newline at end of file +export type GameStateEvent = tg.GuardedType; diff --git a/front/src/Api/Events/HasPlayerMovedEvent.ts b/front/src/Api/Events/HasPlayerMovedEvent.ts index 28603284..e7750367 100644 --- a/front/src/Api/Events/HasPlayerMovedEvent.ts +++ b/front/src/Api/Events/HasPlayerMovedEvent.ts @@ -11,7 +11,7 @@ export const isHasPlayerMovedEvent = }).get(); /** - * A message sent from the game to the iFrame when the player move after the iFrame send a message to the game that it want to listen to the position of the player + * A message sent from the game to the iFrame to notify a movement from the current player. */ export type HasPlayerMovedEvent = tg.GuardedType; diff --git a/front/src/Phaser/Game/GameMap.ts b/front/src/Phaser/Game/GameMap.ts index d63a67e0..cc109751 100644 --- a/front/src/Phaser/Game/GameMap.ts +++ b/front/src/Phaser/Game/GameMap.ts @@ -117,41 +117,14 @@ export class GameMap { } public findLayer(layerName: string): ITiledMapLayer | undefined { - let i = 0; - let found = false; - while (!found && i layer.name = layerName); } public findPhaserLayer(layerName: string): TilemapLayer | undefined { - let i = 0; - let found = false; - while (!found && i layer.layer.name = layerName); } public addTerrain(terrain : Phaser.Tilemaps.Tileset): void { - console.log('Add'); for (const phaserLayer of this.phaserLayers) { phaserLayer.tileset.push(terrain); } From a5cb93541afad9a95fe4614bd96a8f7d8ee99a9b Mon Sep 17 00:00:00 2001 From: GRL Date: Tue, 25 May 2021 17:21:02 +0200 Subject: [PATCH 35/46] correction from code review --- front/src/Api/Events/ApiUpdateTileEvent.ts | 0 front/src/Api/Events/GameStateEvent.ts | 25 ++--- front/src/Api/Events/HasPlayerMovedEvent.ts | 2 +- front/src/Api/Events/IframeEvent.ts | 5 - front/src/Api/Events/TagEvent.ts | 10 -- front/src/Api/Events/TilesetEvent.ts | 15 --- front/src/Api/IframeListener.ts | 15 +-- front/src/Connexion/RoomConnection.ts | 9 +- front/src/Phaser/Game/GameScene.ts | 27 ++--- front/src/iframe_api.ts | 111 +++++++------------- 10 files changed, 58 insertions(+), 161 deletions(-) delete mode 100644 front/src/Api/Events/ApiUpdateTileEvent.ts delete mode 100644 front/src/Api/Events/TagEvent.ts delete mode 100644 front/src/Api/Events/TilesetEvent.ts diff --git a/front/src/Api/Events/ApiUpdateTileEvent.ts b/front/src/Api/Events/ApiUpdateTileEvent.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/front/src/Api/Events/GameStateEvent.ts b/front/src/Api/Events/GameStateEvent.ts index 72e40898..704cd962 100644 --- a/front/src/Api/Events/GameStateEvent.ts +++ b/front/src/Api/Events/GameStateEvent.ts @@ -1,26 +1,13 @@ import * as tg from "generic-type-guard"; -/*export const isPositionState = new tg.IsInterface().withProperties({ - x: tg.isNumber, - y: tg.isNumber -}).get() -export const isPlayerState = new tg.IsInterface() - .withStringIndexSignature( - new tg.IsInterface().withProperties({ - position: isPositionState, - pusherId: tg.isUnion(tg.isNumber, tg.isUndefined) - }).get() - ).get() - -export type PlayerStateObject = tg.GuardedType;*/ - export const isGameStateEvent = new tg.IsInterface().withProperties({ - roomId: tg.isString, - mapUrl: tg.isString, - nickname: tg.isUnion(tg.isString, tg.isNull), - uuid: tg.isUnion(tg.isString, tg.isUndefined), - startLayerName: tg.isUnion(tg.isString, tg.isNull) + roomId: tg.isString, + mapUrl: tg.isString, + nickname: tg.isUnion(tg.isString, tg.isNull), + uuid: tg.isUnion(tg.isString, tg.isUndefined), + startLayerName: tg.isUnion(tg.isString, tg.isNull), + tags : tg.isArray(tg.isString), }).get(); /** * A message sent from the game to the iFrame when the gameState is got by the script diff --git a/front/src/Api/Events/HasPlayerMovedEvent.ts b/front/src/Api/Events/HasPlayerMovedEvent.ts index 28603284..5fe2a1e2 100644 --- a/front/src/Api/Events/HasPlayerMovedEvent.ts +++ b/front/src/Api/Events/HasPlayerMovedEvent.ts @@ -4,7 +4,7 @@ import * as tg from "generic-type-guard"; export const isHasPlayerMovedEvent = new tg.IsInterface().withProperties({ - direction: tg.isString, + direction: tg.isElementOf('right', 'left', 'up', 'down'), moving: tg.isBoolean, x: tg.isNumber, y: tg.isNumber diff --git a/front/src/Api/Events/IframeEvent.ts b/front/src/Api/Events/IframeEvent.ts index 8e4a76f5..1bab019a 100644 --- a/front/src/Api/Events/IframeEvent.ts +++ b/front/src/Api/Events/IframeEvent.ts @@ -15,8 +15,6 @@ import type { UserInputChatEvent } from './UserInputChatEvent'; import type { DataLayerEvent } from "./DataLayerEvent"; import type { LayerEvent } from './LayerEvent'; import type { SetPropertyEvent } from "./setPropertyEvent"; -import type { TagEvent } from "./TagEvent"; -import type { TilesetEvent } from "./TilesetEvent"; export interface TypedMessageEvent extends MessageEvent { data: T @@ -24,7 +22,6 @@ export interface TypedMessageEvent extends MessageEvent { export type IframeEventMap = { getState: GameStateEvent, - // updateTile: UpdateTileEvent registerMenuCommand: MenuItemRegisterEvent chat: ChatEvent, openPopup: OpenPopupEvent @@ -42,7 +39,6 @@ export type IframeEventMap = { hideLayer: LayerEvent setProperty: SetPropertyEvent getDataLayer: undefined - getTag: undefined //tilsetEvent: TilesetEvent } export interface IframeEvent { @@ -63,7 +59,6 @@ export interface IframeResponseEventMap { hasPlayerMoved: HasPlayerMovedEvent dataLayer: DataLayerEvent menuItemClicked: MenuItemClickedEvent - tagList: TagEvent } export interface IframeResponseEvent { type: T; diff --git a/front/src/Api/Events/TagEvent.ts b/front/src/Api/Events/TagEvent.ts deleted file mode 100644 index 66665403..00000000 --- a/front/src/Api/Events/TagEvent.ts +++ /dev/null @@ -1,10 +0,0 @@ -import * as tg from "generic-type-guard"; - -export const isTagEvent = - new tg.IsInterface().withProperties({ - list: tg.isArray(tg.isString), - }).get(); -/** - * A message sent from the iFrame to the game to show/hide a layer. - */ -export type TagEvent = tg.GuardedType; \ No newline at end of file diff --git a/front/src/Api/Events/TilesetEvent.ts b/front/src/Api/Events/TilesetEvent.ts deleted file mode 100644 index eab33bf7..00000000 --- a/front/src/Api/Events/TilesetEvent.ts +++ /dev/null @@ -1,15 +0,0 @@ -import * as tg from "generic-type-guard"; - -export const isTilesetEvent = - new tg.IsInterface().withProperties({ - name : tg.isString, - imgUrl : tg.isString, - tilewidth : tg.isNumber, - tileheight : tg.isNumber, - margin : tg.isNumber, - spacing : tg.isNumber, - }).get(); -/** - * A message sent from the iFrame to the game to show/hide a layer. - */ -export type TilesetEvent = tg.GuardedType; \ No newline at end of file diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index 647a95dc..ec340b16 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -19,7 +19,6 @@ import { Math } from 'phaser'; import type { DataLayerEvent } from "./Events/DataLayerEvent"; import { isMenuItemRegisterEvent } from './Events/MenuItemRegisterEvent'; import type { MenuItemClickedEvent } from './Events/MenuItemClickedEvent'; -import type { TagEvent } from "./Events/TagEvent"; //import { isTilesetEvent, TilesetEvent } from "./Events/TilesetEvent"; @@ -80,9 +79,6 @@ class IframeListener { private readonly _registerMenuCommandStream: Subject = new Subject(); public readonly registerMenuCommandStream = this._registerMenuCommandStream.asObservable(); - private readonly _tagListStream: Subject = new Subject(); - public readonly tagListStream = this._tagListStream.asObservable(); - /* private readonly _tilesetLoaderStream: Subject = new Subject(); public readonly tilesetLoaderStream = this._tilesetLoaderStream.asObservable();*/ @@ -154,9 +150,7 @@ class IframeListener { this._dataLayerChangeStream.next(); } else if (payload.type == "registerMenuCommand" && isMenuItemRegisterEvent(payload.data)) { this._registerMenuCommandStream.next(payload.data.menutItem) - } else if (payload.type == "getTag") { - this._tagListStream.next(); -/* } else if (payload.type == "tilsetEvent" && isTilesetEvent(payload.data)) { +/* } else if (payload.type == "tilsetEvent" && isTilesetEvent(payload.data)) { this._tilesetLoaderStream.next(payload.data);*/ } } @@ -164,13 +158,6 @@ class IframeListener { } - sendUserTagList(tagList: TagEvent){ - this.postMessage({ - 'type' : 'tagList', - 'data' : tagList - }) - } - sendDataLayerEvent(dataLayerEvent: DataLayerEvent) { this.postMessage({ 'type' : 'dataLayer', diff --git a/front/src/Connexion/RoomConnection.ts b/front/src/Connexion/RoomConnection.ts index 1cb4a97d..8bfa3b6a 100644 --- a/front/src/Connexion/RoomConnection.ts +++ b/front/src/Connexion/RoomConnection.ts @@ -169,9 +169,9 @@ export class RoomConnection implements RoomConnection { } else if (message.hasWorldfullmessage()) { worldFullMessageStream.onMessage(); this.closed = true; - // // } else if (message.hasWorldconnexionmessage()) { - // worldFullMessageStream.onMessage(message.getWorldconnexionmessage()?.getMessage()); - // this.closed = true; + } else if (message.hasWorldconnexionmessage()) { + worldFullMessageStream.onMessage(message.getWorldconnexionmessage()?.getMessage()); + this.closed = true; } else if (message.hasWebrtcsignaltoclientmessage()) { this.dispatch(EventMessage.WEBRTC_SIGNAL, message.getWebrtcsignaltoclientmessage()); } else if (message.hasWebrtcscreensharingsignaltoclientmessage()) { @@ -600,9 +600,6 @@ export class RoomConnection implements RoomConnection { } public getAllTag() : string[] { - this.tags.push('TEST'); - this.tags.push('TEST 2'); - this.tags.push('TEST 3'); return this.tags; } } diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 3df7e093..5e540770 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -864,15 +864,6 @@ ${escapedMessage} this.iframeSubscriptionList.push(iframeListener.enablePlayerControlStream.subscribe(() => { this.userInputManager.restoreControls(); })); - this.iframeSubscriptionList.push(iframeListener.gameStateStream.subscribe(() => { - iframeListener.sendFrozenGameStateEvent({ - mapUrl: this.MapUrlFile, - startLayerName: this.startLayerName, - uuid: localUserStore.getLocalUser()?.uuid, - nickname: localUserStore.getName(), - roomId: this.RoomId, - }) - })); let scriptedBubbleSprite: Sprite; this.iframeSubscriptionList.push(iframeListener.displayBubbleStream.subscribe(() => { @@ -886,12 +877,10 @@ ${escapedMessage} })); this.iframeSubscriptionList.push(iframeListener.showLayerStream.subscribe((layerEvent)=>{ - console.log('show'); this.setLayerVisibility(layerEvent.name, true); })); this.iframeSubscriptionList.push(iframeListener.hideLayerStream.subscribe((layerEvent)=>{ - console.log('hide'); this.setLayerVisibility(layerEvent.name, false); })); @@ -903,12 +892,16 @@ ${escapedMessage} iframeListener.sendDataLayerEvent({data: this.gameMap.getMap()}); })) - this.iframeSubscriptionList.push(iframeListener.tagListStream.subscribe(()=> { - if (this.connection === undefined) { - return; - } - iframeListener.sendUserTagList({list: this.connection.getAllTag()}); - })) + this.iframeSubscriptionList.push(iframeListener.gameStateStream.subscribe(() => { + iframeListener.sendFrozenGameStateEvent({ + mapUrl: this.MapUrlFile, + startLayerName: this.startLayerName, + uuid: localUserStore.getLocalUser()?.uuid, + nickname: localUserStore.getName(), + roomId: this.RoomId, + tags: this.connection ? this.connection.getAllTag() : [] + }) + })); /* this.iframeSubscriptionList.push(iframeListener.tilesetLoaderStream.subscribe((tileset) => { //this.load.tilemapTiledJSON('logo', tileset.imgUrl); diff --git a/front/src/iframe_api.ts b/front/src/iframe_api.ts index b2eac975..f62b77a4 100644 --- a/front/src/iframe_api.ts +++ b/front/src/iframe_api.ts @@ -17,8 +17,6 @@ import { DataLayerEvent, isDataLayerEvent } from "./Api/Events/DataLayerEvent"; import type { ITiledMap } from "./Phaser/Map/ITiledMap"; import type { MenuItemRegisterEvent } from "./Api/Events/MenuItemRegisterEvent"; import { isMenuItemClickedEvent } from "./Api/Events/MenuItemClickedEvent"; -import {TagEvent, isTagEvent} from "./Api/Events/TagEvent"; -import type { TilesetEvent } from "./Api/Events/TilesetEvent"; interface WorkAdventureApi { sendChatMessage(message: string, author: string): void; @@ -42,18 +40,26 @@ interface WorkAdventureApi { displayBubble(): void; removeBubble(): void; registerMenuCommand(commandDescriptor: string, callback: (commandDescriptor: string) => void): void - getMapUrl(): Promise; - getUuid(): Promise; - getRoomId(): Promise; - getStartLayerName(): Promise; - getNickName(): Promise; - getTagUser(): Promise; - getMap(): Promise + getCurrentUser(): Promise + getCurrentRoom(): Promise //loadTileset(name: string, imgUrl : string, tilewidth : number, tileheight : number, margin : number, spacing : number): void; onPlayerMove(callback: (playerMovedEvent: HasPlayerMovedEvent) => void): void } +interface User { + id: string | undefined + nickName: string | null + tags: string[] +} + +interface Room { + id: string + mapUrl: string + map: ITiledMap + startLayer: string | null +} + declare global { // eslint-disable-next-line no-var var WA: WorkAdventureApi @@ -101,12 +107,14 @@ class Popup { }, '*'); } } -function uuidv4() { + +/*function uuidv4() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { const r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); -} +}*/ + function getGameState(): Promise { if (immutableData) { return Promise.resolve(immutableData); @@ -131,34 +139,21 @@ function getDataLayer(): Promise { }) } -function getTag(): Promise { - return new Promise((resolver, thrower) => { - tagResolver.push((resolver)); - postToParent({ - type: "getTag", - data: undefined - }) - }) -} - const gameStateResolver: Array<(event: GameStateEvent) => void> = [] const dataLayerResolver: Array<(event: DataLayerEvent) => void> = [] -const tagResolver: Array<(event : TagEvent) => void> = [] let immutableData: GameStateEvent; -const callbackPlayerMoved: { [type: string]: HasPlayerMovedEventCallback | ((arg?: HasPlayerMovedEvent | never) => void) } = {} - +//const callbackPlayerMoved: { [type: string]: HasPlayerMovedEventCallback | ((arg?: HasPlayerMovedEvent | never) => void) } = {} +const callbackPlayerMoved: Array<(event: HasPlayerMovedEvent) => void> = [] function postToParent(content: IframeEvent) { window.parent.postMessage(content, "*") } -let playerUuid: string | undefined; window.WA = { onPlayerMove(callback: HasPlayerMovedEventCallback): void { - playerUuid = uuidv4(); - callbackPlayerMoved[playerUuid] = callback; + callbackPlayerMoved.push(callback); postToParent({ type: "onPlayerMove", data: undefined @@ -179,45 +174,17 @@ window.WA = { }) },*/ - getTagUser(): Promise { - return getTag().then((res) => { - return res.list; + getCurrentUser(): Promise { + return getGameState().then((gameState) => { + return {id: gameState.uuid, nickName: gameState.nickname, tags: gameState.tags}; }) }, - getMap(): Promise { - return getDataLayer().then((res) => { - return res.data as ITiledMap; - }) - }, - - getNickName(): Promise { - return getGameState().then((res) => { - return res.nickname; - }) - }, - - getMapUrl(): Promise { - return getGameState().then((res) => { - return res.mapUrl; - }) - }, - - getUuid(): Promise { - return getGameState().then((res) => { - return res.uuid; - }) - }, - - getRoomId(): Promise { - return getGameState().then((res) => { - return res.roomId; - }) - }, - - getStartLayerName(): Promise { - return getGameState().then((res) => { - return res.startLayerName; + getCurrentRoom(): Promise { + return getGameState().then((gameState) => { + return getDataLayer().then((mapJson) => { + return {id: gameState.roomId, map: mapJson.data as ITiledMap, mapUrl: gameState.mapUrl, startLayer: gameState.startLayerName}; + }) }) }, @@ -411,22 +378,18 @@ window.addEventListener('message', message => { resolver(payloadData); }) immutableData = payloadData; - } else if (payload.type == "hasPlayerMoved" && isHasPlayerMovedEvent(payloadData) && playerUuid) { - callbackPlayerMoved[playerUuid](payloadData) + } else if (payload.type == "hasPlayerMoved" && isHasPlayerMovedEvent(payloadData)) { + callbackPlayerMoved.forEach(callback => { + callback(payloadData); + }) } else if (payload.type == "dataLayer" && isDataLayerEvent(payloadData)) { dataLayerResolver.forEach(resolver => { resolver(payloadData); }) - } else if (payload.type == "menuItemClicked" && isMenuItemClickedEvent(payload.data)) { - const callback = menuCallbacks.get(payload.data.menuItem); + } else if (payload.type == "menuItemClicked" && isMenuItemClickedEvent(payloadData)) { + const callback = menuCallbacks.get(payloadData.menuItem); if (callback) { - callback(payload.data.menuItem) - } - } else { - if (payload.type == "tagList" && isTagEvent(payloadData)) { - tagResolver.forEach(resolver => { - resolver(payloadData); - }) + callback(payloadData.menuItem) } } } From c8e2416e081a5450b24b3498b384038ebb82cd6d Mon Sep 17 00:00:00 2001 From: GRL Date: Wed, 26 May 2021 10:41:33 +0200 Subject: [PATCH 36/46] documentation of getCurrentUser, getCurrentRoom and on working with group layer --- docs/maps/api-reference.md | 111 +++++++++++++------------------------ front/src/iframe_api.ts | 4 ++ 2 files changed, 44 insertions(+), 71 deletions(-) diff --git a/docs/maps/api-reference.md b/docs/maps/api-reference.md index 30d0f1ea..6a4dd7ab 100644 --- a/docs/maps/api-reference.md +++ b/docs/maps/api-reference.md @@ -236,8 +236,7 @@ mySound.play(config); mySound.stop(); ``` -### Show / Hide a layer - +### Show / Hide a layer ``` WA.showLayer(layerName : string): void WA.hideLayer(layerName : string) : void @@ -245,7 +244,6 @@ WA.hideLayer(layerName : string) : void These 2 methods can be used to show and hide a layer. Example : - ```javascript WA.showLayer('bottom'); //... @@ -260,8 +258,7 @@ WA.setProperty(layerName : string, propertyName : string, propertyValue : string Set the value of the `propertyName` property of the layer `layerName` at `propertyValue`. If the property doesn't exist, create the property `propertyName` and set the value of the property at `propertyValue`. -Example : - +Example : ```javascript WA.setProperty('wikiLayer', 'openWebsite', 'https://www.wikipedia.org/'); ``` @@ -286,74 +283,42 @@ Example : WA.onPlayerMove(console.log); ``` -### Getting the map - +### Getting informations on the current user ``` -getMap(): Promise +getCurrentUser(): Promise ``` - -Returns a promise that resolves to the JSON file of the map. Please note that if you modified the map (for instance by calling `WA.setProperty`, the data returned by `getMap` will contain those changes. +Return a promise that resolves to a `User` object with the following attributes : +* **id (string) :** ID of the current user +* **nickName (string) :** name displayed above the current user +* **tags (string[]) :** list of all the tags of the current user Example : ```javascript -WA.getMap().then((data) => console.log(data.layers)); +WA.getCurrentUser().then((user) => { + if (user.nickName === 'ABC') { + console.log(user.tags); + } +}) ``` -### Getting the url of the JSON file map - +### Getting informations on the current room ``` -getMapUrl(): Promise +getCurrentRoom(): Promise ``` - -Return a promise of the url of the JSON file map. +Return a promise that resolves to a `Room` object with the following attributes : +* **id (string) :** ID of the current room +* **map (ITiledMap) :** contains the JSON map file with the properties that were setted by the script if `setProperty` was called. +* **mapUrl (string) :** Url of the JSON map file +* **startLayer (string | null) :** Name of the layer where the current user started, only if different from `start` layer Example : ```javascript -WA.getMapUrl().then((mapUrl) => {console.log(mapUrl)}); -``` - -### Getting the roomID -``` -getRoomId(): Promise -``` -Return a promise of the ID of the current room. - -Example : -```javascript -WA.getRoomId().then((roomId) => console.log(roomId)); -``` - -### Getting the UUID of the current user -``` -getUuid(): Promise -``` -Return a promise of the ID of the current user. - -Example : -```javascript -WA.getUuid().then((uuid) => {console.log(uuid)}); -``` - -### Getting the nickname of the current user -``` -getNickName(): Promise -``` -Return a promise of the nickname of the current user. - -Example : -```javascript -WA.getNickName().then((nickname) => {console.log(nickname)}); -``` - -### Getting the name of the layer where the current user started (if other than start) -``` -getStartLayerName(): Promise -``` -Return a promise of the name of the layer where the current user started if the name is different than "start". - -Example : -```javascript -WA.getStartLayerName().then((starLayerName) => {console.log(starLayerName)}); +WA.getCurrentRoom((room) => { + if (room.id === '42') { + console.log(room.map); + window.open(room.mapUrl, '_blank'); + } +}) ``` ### Add a custom menu @@ -367,17 +332,21 @@ Example : WA.registerMenuCommand('About', () => { console.log("The About menu was clicked"); }); - -### Getting the list of tags of the current user -``` -getTagUser(): Promise ``` -Returns the tags of the current user. If the current user has no tag, returns an empty list. + +### Working with group layers +If you use group layers in your map, to reference a layer in a group you will need to use a `/` to join layer names together. Example : -```javascript -WA.getTagUser().then((tagList) => { - ... -}); -``` +
+
+ +
+
+ +The name of the layers of this map are : +* `entries/start` +* `bottom/ground/under` +* `bottom/build/carpet` +* `wall` \ No newline at end of file diff --git a/front/src/iframe_api.ts b/front/src/iframe_api.ts index f62b77a4..8da1fa23 100644 --- a/front/src/iframe_api.ts +++ b/front/src/iframe_api.ts @@ -201,6 +201,7 @@ window.WA = { } as ChatEvent }, '*'); }, + showLayer(layer: string) : void { window.parent.postMessage({ 'type' : 'showLayer', @@ -209,6 +210,7 @@ window.WA = { } as LayerEvent }, '*'); }, + hideLayer(layer: string) : void { window.parent.postMessage({ 'type' : 'hideLayer', @@ -217,6 +219,7 @@ window.WA = { } as LayerEvent }, '*'); }, + setProperty(layerName: string, propertyName: string, propertyValue: string | number | boolean | undefined): void { window.parent.postMessage({ 'type' : 'setProperty', @@ -227,6 +230,7 @@ window.WA = { } as SetPropertyEvent }, '*'); }, + disablePlayerControls(): void { window.parent.postMessage({ 'type': 'disablePlayerControls' }, '*'); }, From e1f0192e617b8118474abfcf7382f1cefb6bd649 Mon Sep 17 00:00:00 2001 From: GRL Date: Wed, 26 May 2021 17:18:38 +0200 Subject: [PATCH 37/46] Adding and updating test map for metadata --- maps/tests/Metadata/ScriptMap.json | 219 --------------- maps/tests/Metadata/TagList.html | 19 -- maps/tests/Metadata/TagList.json | 254 ------------------ maps/tests/Metadata/getCurrentRoom.html | 16 ++ ...{getGameState.json => getCurrentRoom.json} | 46 ++-- maps/tests/Metadata/getCurrentUser.html | 15 ++ ...getGameState2.json => getCurrentUser.json} | 43 ++- maps/tests/Metadata/getGameState.html | 42 --- maps/tests/Metadata/getGameState2.html | 40 --- maps/tests/Metadata/script.js | 1 - 10 files changed, 87 insertions(+), 608 deletions(-) delete mode 100644 maps/tests/Metadata/ScriptMap.json delete mode 100644 maps/tests/Metadata/TagList.html delete mode 100644 maps/tests/Metadata/TagList.json create mode 100644 maps/tests/Metadata/getCurrentRoom.html rename maps/tests/Metadata/{getGameState.json => getCurrentRoom.json} (90%) create mode 100644 maps/tests/Metadata/getCurrentUser.html rename maps/tests/Metadata/{getGameState2.json => getCurrentUser.json} (86%) delete mode 100644 maps/tests/Metadata/getGameState.html delete mode 100644 maps/tests/Metadata/getGameState2.html delete mode 100644 maps/tests/Metadata/script.js diff --git a/maps/tests/Metadata/ScriptMap.json b/maps/tests/Metadata/ScriptMap.json deleted file mode 100644 index 93972a73..00000000 --- a/maps/tests/Metadata/ScriptMap.json +++ /dev/null @@ -1,219 +0,0 @@ -{ "compressionlevel":-1, - "height":10, - "infinite":false, - "layers":[ - { - "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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":1, - "name":"start", - "opacity":1, - "type":"tilelayer", - "visible":true, - "width":10, - "x":0, - "y":0 - }, - { - "data":[33, 34, 34, 34, 34, 34, 34, 34, 34, 35, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 49, 50, 50, 50, 50, 50, 50, 50, 50, 51], - "height":10, - "id":2, - "name":"bottom", - "opacity":1, - "type":"tilelayer", - "visible":true, - "width":10, - "x":0, - "y":0 - }, - { - "draworder":"topdown", - "id":5, - "name":"floorLayer", - "objects":[], - "opacity":1, - "type":"objectgroup", - "visible":true, - "x":0, - "y":0 - }], - "nextlayerid":10, - "nextobjectid":2, - "orientation":"orthogonal", - "properties":[ - { - "name":"script", - "type":"string", - "value":"script.js" - }], - "renderorder":"right-down", - "tiledversion":"1.4.3", - "tileheight":32, - "tilesets":[ - { - "columns":8, - "firstgid":1, - "image":"tileset_dungeon.png", - "imageheight":256, - "imagewidth":256, - "margin":0, - "name":"TDungeon", - "spacing":0, - "tilecount":64, - "tileheight":32, - "tiles":[ - { - "id":0, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":1, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":2, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":3, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":4, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":8, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":9, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":10, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":11, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":12, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":16, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":17, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":18, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":19, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":20, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }], - "tilewidth":32 - }, - { - "columns":8, - "firstgid":65, - "image":"floortileset.png", - "imageheight":288, - "imagewidth":256, - "margin":0, - "name":"Floor", - "spacing":0, - "tilecount":72, - "tileheight":32, - "tilewidth":32 - }], - "tilewidth":32, - "type":"map", - "version":1.4, - "width":10 -} \ No newline at end of file diff --git a/maps/tests/Metadata/TagList.html b/maps/tests/Metadata/TagList.html deleted file mode 100644 index 73bdc368..00000000 --- a/maps/tests/Metadata/TagList.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - -
- - \ No newline at end of file diff --git a/maps/tests/Metadata/TagList.json b/maps/tests/Metadata/TagList.json deleted file mode 100644 index cced49a3..00000000 --- a/maps/tests/Metadata/TagList.json +++ /dev/null @@ -1,254 +0,0 @@ -{ "compressionlevel":-1, - "height":10, - "infinite":false, - "layers":[ - { - "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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":1, - "name":"start", - "opacity":1, - "type":"tilelayer", - "visible":true, - "width":10, - "x":0, - "y":0 - }, - { - "data":[33, 34, 34, 34, 34, 34, 34, 34, 34, 35, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 49, 50, 50, 50, 50, 50, 50, 50, 50, 51], - "height":10, - "id":2, - "name":"bottom", - "opacity":1, - "type":"tilelayer", - "visible":true, - "width":10, - "x":0, - "y":0 - }, - { - "data":[0, 0, 0, 0, 0, 128, 128, 128, 128, 128, 0, 0, 0, 0, 0, 128, 128, 128, 128, 128, 0, 0, 0, 0, 0, 128, 128, 128, 128, 128, 0, 0, 0, 0, 0, 128, 128, 128, 128, 128, 0, 0, 0, 0, 0, 128, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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":4, - "name":"metadata", - "opacity":1, - "properties":[ - { - "name":"openWebsite", - "type":"string", - "value":"TagList.html" - }, - { - "name":"openWebsiteAllowApi", - "type":"bool", - "value":true - }], - "type":"tilelayer", - "visible":true, - "width":10, - "x":0, - "y":0 - }, - { - "draworder":"topdown", - "id":5, - "name":"floorLayer", - "objects":[ - { - "height":131.903791109293, - "id":1, - "name":"", - "rotation":0, - "text": - { - "fontfamily":"Sans Serif", - "pixelsize":9, - "text":"Test : \nWalk on the grass, an iframe open, click on the 'Get Tag List' button.\nResult : \nThe list of the tag is displayed in the iframe.\n\n\n", - "wrap":true - }, - "type":"", - "visible":true, - "width":305.097705765524, - "x":14.750638909983, - "y":188.268561247737 - }], - "opacity":1, - "type":"objectgroup", - "visible":true, - "x":0, - "y":0 - }], - "nextlayerid":10, - "nextobjectid":2, - "orientation":"orthogonal", - "renderorder":"right-down", - "tiledversion":"1.4.3", - "tileheight":32, - "tilesets":[ - { - "columns":8, - "firstgid":1, - "image":"tileset_dungeon.png", - "imageheight":256, - "imagewidth":256, - "margin":0, - "name":"TDungeon", - "spacing":0, - "tilecount":64, - "tileheight":32, - "tiles":[ - { - "id":0, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":1, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":2, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":3, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":4, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":8, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":9, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":10, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":11, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":12, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":16, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":17, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":18, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":19, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":20, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }], - "tilewidth":32 - }, - { - "columns":8, - "firstgid":65, - "image":"floortileset.png", - "imageheight":288, - "imagewidth":256, - "margin":0, - "name":"Floor", - "spacing":0, - "tilecount":72, - "tileheight":32, - "tilewidth":32 - }], - "tilewidth":32, - "type":"map", - "version":1.4, - "width":10 -} \ No newline at end of file diff --git a/maps/tests/Metadata/getCurrentRoom.html b/maps/tests/Metadata/getCurrentRoom.html new file mode 100644 index 00000000..b290c6a4 --- /dev/null +++ b/maps/tests/Metadata/getCurrentRoom.html @@ -0,0 +1,16 @@ + + + + + + + + + \ No newline at end of file diff --git a/maps/tests/Metadata/getGameState.json b/maps/tests/Metadata/getCurrentRoom.json similarity index 90% rename from maps/tests/Metadata/getGameState.json rename to maps/tests/Metadata/getCurrentRoom.json index a005ee8a..c14bb946 100644 --- a/maps/tests/Metadata/getGameState.json +++ b/maps/tests/Metadata/getCurrentRoom.json @@ -9,6 +9,24 @@ "height":10, "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, 92, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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":10, + "name":"HereYouAppered", + "opacity":1, + "properties":[ + { + "name":"startLayer", + "type":"bool", + "value":true + }], + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, { "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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, @@ -43,7 +61,7 @@ { "name":"openWebsite", "type":"string", - "value":"getGameState.html" + "value":"getCurrentRoom.html" }, { "name":"openWebsiteAllowApi", @@ -56,31 +74,13 @@ "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, 101, 0, 0, 0, 0, 0, 0, 0, 0, 0, 101, 0, 0, 0, 0, 0, 0, 0, 0, 0, 101, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - "height":10, - "id":8, - "name":"exit", - "opacity":1, - "properties":[ - { - "name":"exitUrl", - "type":"string", - "value":"getGameState2.json#HereYouAppear" - }], - "type":"tilelayer", - "visible":true, - "width":10, - "x":0, - "y":0 - }, { "draworder":"topdown", "id":5, "name":"floorLayer", "objects":[ { - "height":218.263975699515, + "height":191.607568521364, "id":1, "name":"", "rotation":0, @@ -88,14 +88,14 @@ { "fontfamily":"Sans Serif", "pixelsize":9, - "text":"Start the test : \nWalk on the grass, an iframe open.\n\nTest : \nClick on the 'nickname' button.\nResult : \nYour nickname appears.\n\nTest : \nClick on the 'roomID' button.\nResult : \nAn ID appears.\n\nTest : \nClick on the 'UUID' button.\nResult : \nAn ID appears.\n\nFinally : \nWalk on the red tiles to continue the testing.\n\n", + "text":"Test : \nWalk on the grass and open the console.\n\nResult : \nYou should see a console.log() of the following attributes : \n\t- id : ID of the current room\n\t- map : data of the JSON file of the map\n\t- mapUrl : url of the JSON file of the map\n\t- startLayer : Name of the layer where the current user started (HereYouAppered)\n\n\n", "wrap":true }, "type":"", "visible":true, "width":305.097705765524, "x":14.750638909983, - "y":101.908376657515 + "y":128.564783835666 }], "opacity":1, "type":"objectgroup", @@ -103,7 +103,7 @@ "x":0, "y":0 }], - "nextlayerid":9, + "nextlayerid":11, "nextobjectid":2, "orientation":"orthogonal", "renderorder":"right-down", diff --git a/maps/tests/Metadata/getCurrentUser.html b/maps/tests/Metadata/getCurrentUser.html new file mode 100644 index 00000000..318fdf1b --- /dev/null +++ b/maps/tests/Metadata/getCurrentUser.html @@ -0,0 +1,15 @@ + + + + + + + + + \ No newline at end of file diff --git a/maps/tests/Metadata/getGameState2.json b/maps/tests/Metadata/getCurrentUser.json similarity index 86% rename from maps/tests/Metadata/getGameState2.json rename to maps/tests/Metadata/getCurrentUser.json index 04127918..9efd0d09 100644 --- a/maps/tests/Metadata/getGameState2.json +++ b/maps/tests/Metadata/getCurrentUser.json @@ -22,10 +22,10 @@ "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, 109, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "data":[33, 34, 34, 34, 34, 34, 34, 34, 34, 35, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 49, 50, 50, 50, 50, 50, 50, 50, 50, 51], "height":10, - "id":9, - "name":"HereYouAppear", + "id":2, + "name":"bottom", "opacity":1, "type":"tilelayer", "visible":true, @@ -34,11 +34,17 @@ "y":0 }, { - "data":[33, 34, 34, 34, 34, 34, 34, 34, 34, 35, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 49, 50, 50, 50, 50, 50, 50, 50, 50, 51], + "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, 101, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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":"bottom", + "id":9, + "name":"exit", "opacity":1, + "properties":[ + { + "name":"exitUrl", + "type":"string", + "value":"getCurrentRoom.json#HereYouAppered" + }], "type":"tilelayer", "visible":true, "width":10, @@ -55,7 +61,7 @@ { "name":"openWebsite", "type":"string", - "value":"getGameState2.html" + "value":"getCurrentUser.html" }, { "name":"openWebsiteAllowApi", @@ -74,7 +80,7 @@ "name":"floorLayer", "objects":[ { - "height":200.31900227817, + "height":151.839293303871, "id":1, "name":"", "rotation":0, @@ -82,14 +88,14 @@ { "fontfamily":"Sans Serif", "pixelsize":9, - "text":"Start the test : \nWalk on the grass, an iframe open.\n\nTest : \nClick on the 'startLayer' button.\nResult : \nThe name of the layer where you start appears. (only work when the start layer is not 'start')\n\nTest : \nClick on the 'mapUrl' button.\nResult : \nThe url of the JSON file of the map is displayed in the console.log().\n\nTest : \nClick on the 'Map' button.\nResult : \nThe JSON file map appears.\n\n\n", + "text":"Test : \nWalk on the grass, open the console.\n\nResut : \nYou should see a console.log() of the following attributes :\n\t- id : ID of the current user\n\t- nickName : Name of the current user\n\t- tags : List of tags of the current user\n\nFinally : \nWalk on the red tile and continue the test in an another room.", "wrap":true }, "type":"", "visible":true, "width":305.097705765524, "x":14.750638909983, - "y":119.85335007886 + "y":159.621625296353 }], "opacity":1, "type":"objectgroup", @@ -264,6 +270,23 @@ "spacing":0, "tilecount":72, "tileheight":32, + "tiles":[ + { + "animation":[ + { + "duration":100, + "tileid":9 + }, + { + "duration":100, + "tileid":64 + }, + { + "duration":100, + "tileid":55 + }], + "id":0 + }], "tilewidth":32 }], "tilewidth":32, diff --git a/maps/tests/Metadata/getGameState.html b/maps/tests/Metadata/getGameState.html deleted file mode 100644 index f11dab17..00000000 --- a/maps/tests/Metadata/getGameState.html +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - -
- - -
- - -
- - - - \ No newline at end of file diff --git a/maps/tests/Metadata/getGameState2.html b/maps/tests/Metadata/getGameState2.html deleted file mode 100644 index e8529617..00000000 --- a/maps/tests/Metadata/getGameState2.html +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - -
- - -
- - -
- - - - \ No newline at end of file diff --git a/maps/tests/Metadata/script.js b/maps/tests/Metadata/script.js deleted file mode 100644 index d04d7952..00000000 --- a/maps/tests/Metadata/script.js +++ /dev/null @@ -1 +0,0 @@ -console.log('script chargé !!!!!'); \ No newline at end of file From 5d8d729bd73711977e3b6a562e34b654961f4893 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Thu, 27 May 2021 18:25:27 +0200 Subject: [PATCH 38/46] Uncommenting action --- front/src/Connexion/RoomConnection.ts | 8 ++++---- front/src/Phaser/Game/GameScene.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/front/src/Connexion/RoomConnection.ts b/front/src/Connexion/RoomConnection.ts index 58c62a78..159db5a2 100644 --- a/front/src/Connexion/RoomConnection.ts +++ b/front/src/Connexion/RoomConnection.ts @@ -177,9 +177,9 @@ export class RoomConnection implements RoomConnection { } else if (message.hasWorldfullmessage()) { worldFullMessageStream.onMessage(); this.closed = true; - } else if (message.hasWorldconnexionmessage()) { - worldFullMessageStream.onMessage(message.getWorldconnexionmessage()?.getMessage()); - this.closed = true; + } else if (message.hasWorldconnexionmessage()) { + worldFullMessageStream.onMessage(message.getWorldconnexionmessage()?.getMessage()); + this.closed = true; } else if (message.hasWebrtcsignaltoclientmessage()) { this.dispatch(EventMessage.WEBRTC_SIGNAL, message.getWebrtcsignaltoclientmessage()); } else if (message.hasWebrtcscreensharingsignaltoclientmessage()) { @@ -617,7 +617,7 @@ export class RoomConnection implements RoomConnection { this.socket.send(clientToServerMessage.serializeBinary().buffer); } - public getAllTag() : string[] { + public getAllTags() : string[] { return this.tags; } } diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 1e4c55f5..a785b7f6 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -923,7 +923,7 @@ ${escapedMessage} uuid: localUserStore.getLocalUser()?.uuid, nickname: localUserStore.getName(), roomId: this.RoomId, - tags: this.connection ? this.connection.getAllTag() : [] + tags: this.connection ? this.connection.getAllTags() : [] }) })); From 858a513569026d4566857919731dbc21d44d221d Mon Sep 17 00:00:00 2001 From: GRL Date: Fri, 28 May 2021 12:13:10 +0200 Subject: [PATCH 39/46] correction of adding custom menu correction of setProperty updating CHANGELOG updating api-reference --- CHANGELOG.md | 7 +++++ docs/maps/api-reference.md | 2 +- front/src/Api/Events/IframeEvent.ts | 5 ++- front/src/Api/IframeListener.ts | 20 ++++++++---- front/src/Phaser/Game/GameMap.ts | 8 ++--- front/src/Phaser/Game/GameScene.ts | 7 ----- front/src/Phaser/Menu/MenuScene.ts | 7 +++++ front/src/iframe_api.ts | 25 +++------------ maps/tests/index.html | 48 +++++++++++++++++++++++++++++ 9 files changed, 87 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dec14540..68a7016f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,13 @@ - Improved virtual joystick size (adapts to the zoom level) - New scripting API features: - Use `WA.loadSound(): Sound` to load / play / stop a sound + - Use `WA.showLayer(): void` to show a layer + - Use `WA.hideLayer(): void` to hide a layer + - Use `WA.setProperty() : void` to add or change existing property of a layer + - Use `WA.onPlayerMove(): void` to track the movement of the current player + - Use `WA.getCurrentUser(): Promise` to get the ID, name and tags of the current player + - Use `WA.getCurrentRoom(): Promise` to get the ID, JSON map file, url of the map of the current room and the layer where the current player started + - Use `WA.registerMenuCommand(): void` to add a custom menu ### Bug Fixes diff --git a/docs/maps/api-reference.md b/docs/maps/api-reference.md index 6a4dd7ab..d4316772 100644 --- a/docs/maps/api-reference.md +++ b/docs/maps/api-reference.md @@ -323,7 +323,7 @@ WA.getCurrentRoom((room) => { ### Add a custom menu ``` -registerMenuCommand(commandDescriptor: string, callback: (commandDescriptor: string) => void) +registerMenuCommand(commandDescriptor: string, callback: (commandDescriptor: string) => void): void ``` Add a custom menu item containing the text `commandDescriptor`. A click on the menu will trigger the `callback`. diff --git a/front/src/Api/Events/IframeEvent.ts b/front/src/Api/Events/IframeEvent.ts index bb15528d..e5b1c30b 100644 --- a/front/src/Api/Events/IframeEvent.ts +++ b/front/src/Api/Events/IframeEvent.ts @@ -15,8 +15,8 @@ import type { UserInputChatEvent } from './UserInputChatEvent'; import type { DataLayerEvent } from "./DataLayerEvent"; import type { LayerEvent } from './LayerEvent'; import type { SetPropertyEvent } from "./setPropertyEvent"; -import type {LoadSoundEvent} from "./LoadSoundEvent"; -import type {PlaySoundEvent} from "./PlaySoundEvent"; +import type { LoadSoundEvent } from "./LoadSoundEvent"; +import type { PlaySoundEvent } from "./PlaySoundEvent"; export interface TypedMessageEvent extends MessageEvent { @@ -42,7 +42,6 @@ export type IframeEventMap = { hideLayer: LayerEvent setProperty: SetPropertyEvent getDataLayer: undefined - //tilsetEvent: TilesetEvent loadSound: LoadSoundEvent playSound: PlaySoundEvent stopSound: null diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index ceeea1c4..d05b416f 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -20,7 +20,6 @@ import { Math } from 'phaser'; import type { DataLayerEvent } from "./Events/DataLayerEvent"; import { isMenuItemRegisterEvent } from './Events/MenuItemRegisterEvent'; import type { MenuItemClickedEvent } from './Events/MenuItemClickedEvent'; -//import { isTilesetEvent, TilesetEvent } from "./Events/TilesetEvent"; import { isPlaySoundEvent, PlaySoundEvent } from "./Events/PlaySoundEvent"; import { isStopSoundEvent, StopSoundEvent } from "./Events/StopSoundEvent"; import { isLoadSoundEvent, LoadSoundEvent } from "./Events/LoadSoundEvent"; @@ -81,8 +80,8 @@ class IframeListener { private readonly _registerMenuCommandStream: Subject = new Subject(); public readonly registerMenuCommandStream = this._registerMenuCommandStream.asObservable(); -/* private readonly _tilesetLoaderStream: Subject = new Subject(); - public readonly tilesetLoaderStream = this._tilesetLoaderStream.asObservable();*/ + private readonly _unregisterMenuCommandStream: Subject = new Subject(); + public readonly unregisterMenuCommandStream = this._unregisterMenuCommandStream.asObservable(); private readonly _playSoundStream: Subject = new Subject(); public readonly playSoundStream = this._playSoundStream.asObservable(); @@ -94,6 +93,7 @@ class IframeListener { public readonly loadSoundStream = this._loadSoundStream.asObservable(); private readonly iframes = new Set(); + private readonly iframeCloseCallbacks = new Map void)[]>(); private readonly scripts = new Map(); private sendPlayerMove: boolean = false; @@ -103,7 +103,8 @@ class IframeListener { // 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 foundSrc: string | null = null; - for (const iframe of this.iframes) { + let iframe: HTMLIFrameElement; + for (iframe of this.iframes) { if (iframe.contentWindow === message.source) { foundSrc = iframe.src; break; @@ -171,9 +172,12 @@ class IframeListener { } else if (payload.type == "getDataLayer") { this._dataLayerChangeStream.next(); } else if (payload.type == "registerMenuCommand" && isMenuItemRegisterEvent(payload.data)) { + const data = payload.data.menutItem; + // @ts-ignore + this.iframeCloseCallbacks.get(iframe).push(() => { + this._unregisterMenuCommandStream.next(data); + }) this._registerMenuCommandStream.next(payload.data.menutItem) -/* } else if (payload.type == "tilsetEvent" && isTilesetEvent(payload.data)) { - this._tilesetLoaderStream.next(payload.data);*/ } } }, false); @@ -200,9 +204,13 @@ class IframeListener { */ registerIframe(iframe: HTMLIFrameElement): void { this.iframes.add(iframe); + this.iframeCloseCallbacks.set(iframe, []); } unregisterIframe(iframe: HTMLIFrameElement): void { + this.iframeCloseCallbacks.get(iframe)?.forEach(callback => { + callback(); + }); this.iframes.delete(iframe); } diff --git a/front/src/Phaser/Game/GameMap.ts b/front/src/Phaser/Game/GameMap.ts index 34f55d0b..873b6062 100644 --- a/front/src/Phaser/Game/GameMap.ts +++ b/front/src/Phaser/Game/GameMap.ts @@ -1,7 +1,7 @@ -import type {ITiledMap, ITiledMapLayer, ITiledMapTileLayer} from "../Map/ITiledMap"; +import type { ITiledMap, ITiledMapLayer } from "../Map/ITiledMap"; import { flattenGroupLayersMap } from "../Map/LayersFlattener"; import TilemapLayer = Phaser.Tilemaps.TilemapLayer; -import {DEPTH_OVERLAY_INDEX} from "./DepthIndexes"; +import { DEPTH_OVERLAY_INDEX } from "./DepthIndexes"; export type PropertyChangeCallback = (newValue: string | number | boolean | undefined, oldValue: string | number | boolean | undefined, allProps: Map) => void; @@ -118,11 +118,11 @@ export class GameMap { } public findLayer(layerName: string): ITiledMapLayer | undefined { - return this.flatLayers.find((layer) => layer.name = layerName); + return this.flatLayers.find((layer) => layer.name === layerName); } public findPhaserLayer(layerName: string): TilemapLayer | undefined { - return this.phaserLayers.find((layer) => layer.layer.name = layerName); + return this.phaserLayers.find((layer) => layer.layer.name === layerName); } public addTerrain(terrain : Phaser.Tilemaps.Tileset): void { diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 1e4c55f5..cb2ec0a0 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -927,13 +927,6 @@ ${escapedMessage} }) })); -/* this.iframeSubscriptionList.push(iframeListener.tilesetLoaderStream.subscribe((tileset) => { - //this.load.tilemapTiledJSON('logo', tileset.imgUrl); - this.load.image('logo', tileset.imgUrl); - this.Terrains.push(this.Map.addTilesetImage(tileset.name, tileset.imgUrl, tileset.tilewidth, tileset.tileheight, tileset.margin, tileset.spacing)); - this.gameMap.addTerrain(this.Terrains[this.Terrains.length - 1]); - }))*/ - } private setPropertyLayer(layerName: string, propertyName: string, propertyValue: string | number | boolean | undefined): void { diff --git a/front/src/Phaser/Menu/MenuScene.ts b/front/src/Phaser/Menu/MenuScene.ts index 8957bbce..8a01c259 100644 --- a/front/src/Phaser/Menu/MenuScene.ts +++ b/front/src/Phaser/Menu/MenuScene.ts @@ -49,7 +49,10 @@ export class MenuScene extends Phaser.Scene { this.subscriptions.add(iframeListener.registerMenuCommandStream.subscribe(menuCommand => { this.addMenuOption(menuCommand); + })) + this.subscriptions.add(iframeListener.unregisterMenuCommandStream.subscribe(menuCommand => { + this.destroyMenu(menuCommand); })) } @@ -386,6 +389,10 @@ export class MenuScene extends Phaser.Scene { } } + public destroyMenu(menu: string) { + this.menuElement.getChildByID(menu).remove(); + } + public isDirty(): boolean { return false; } diff --git a/front/src/iframe_api.ts b/front/src/iframe_api.ts index f76c4218..61a3c890 100644 --- a/front/src/iframe_api.ts +++ b/front/src/iframe_api.ts @@ -17,9 +17,9 @@ import { DataLayerEvent, isDataLayerEvent } from "./Api/Events/DataLayerEvent"; import type { ITiledMap } from "./Phaser/Map/ITiledMap"; import type { MenuItemRegisterEvent } from "./Api/Events/MenuItemRegisterEvent"; import { isMenuItemClickedEvent } from "./Api/Events/MenuItemClickedEvent"; -import type {PlaySoundEvent} from "./Api/Events/PlaySoundEvent"; -import type {StopSoundEvent} from "./Api/Events/StopSoundEvent"; -import type {LoadSoundEvent} from "./Api/Events/LoadSoundEvent"; +import type { PlaySoundEvent } from "./Api/Events/PlaySoundEvent"; +import type { StopSoundEvent } from "./Api/Events/StopSoundEvent"; +import type { LoadSoundEvent } from "./Api/Events/LoadSoundEvent"; import SoundConfig = Phaser.Types.Sound.SoundConfig; interface WorkAdventureApi { @@ -47,8 +47,6 @@ interface WorkAdventureApi { registerMenuCommand(commandDescriptor: string, callback: (commandDescriptor: string) => void): void getCurrentUser(): Promise getCurrentRoom(): Promise - //loadTileset(name: string, imgUrl : string, tilewidth : number, tileheight : number, margin : number, spacing : number): void; - onPlayerMove(callback: (playerMovedEvent: HasPlayerMovedEvent) => void): void } @@ -176,7 +174,6 @@ const gameStateResolver: Array<(event: GameStateEvent) => void> = [] const dataLayerResolver: Array<(event: DataLayerEvent) => void> = [] let immutableData: GameStateEvent; -//const callbackPlayerMoved: { [type: string]: HasPlayerMovedEventCallback | ((arg?: HasPlayerMovedEvent | never) => void) } = {} const callbackPlayerMoved: Array<(event: HasPlayerMovedEvent) => void> = [] function postToParent(content: IframeEvent) { @@ -193,20 +190,6 @@ window.WA = { }) }, -/* loadTileset(name: string, imgUrl : string, tilewidth : number, tileheight : number, margin : number, spacing : number): void { - postToParent({ - type: "tilsetEvent", - data: { - name: name, - imgUrl: imgUrl, - tilewidth: tilewidth, - tileheight: tileheight, - margin: margin, - spacing: spacing - } as TilesetEvent - }) - },*/ - getCurrentUser(): Promise { return getGameState().then((gameState) => { return {id: gameState.uuid, nickName: gameState.nickname, tags: gameState.tags}; @@ -353,7 +336,7 @@ window.WA = { return popup; }, - registerMenuCommand(commandDescriptor: string, callback: (commandDescriptor: string) => void) { + registerMenuCommand(commandDescriptor: string, callback: (commandDescriptor: string) => void): void { menuCallbacks.set(commandDescriptor, callback); window.parent.postMessage({ 'type': 'registerMenuCommand', diff --git a/maps/tests/index.html b/maps/tests/index.html index a17a3b5d..527b435f 100644 --- a/maps/tests/index.html +++ b/maps/tests/index.html @@ -82,6 +82,54 @@
Test energy consumption + + + Success Failure Pending + + + Testing add a custom menu by scripting API + + + + + Success Failure Pending + + + Testing return current room attributes by Scripting API (Need to test from current user) + + + + + Success Failure Pending + + + Testing return current user attributes by Scripting API + + + + + Success Failure Pending + + + Test listening player movement by Scripting API + + + + + Success Failure Pending + + + Testing set a property on a layer by Scripting API + + + + + Success Failure Pending + + + Testing show or hide a layer by Scripting API + + From ba1bcf226abbd0bcc46419a328a87fe442cdb3dc Mon Sep 17 00:00:00 2001 From: jonny Date: Mon, 21 Jun 2021 18:22:31 +0200 Subject: [PATCH 41/46] menu command api --- docs/maps/api-ui.md | 23 ++ docs/maps/assets/menu-command.png | Bin 0 -> 9856 bytes front/src/Api/Events/IframeEvent.ts | 8 +- .../src/Api/Events/ui/MenuItemClickedEvent.ts | 21 + .../Api/Events/ui/MenuItemRegisterEvent.ts | 25 ++ front/src/Api/IframeListener.ts | 33 +- front/src/Api/iframe/ui.ts | 29 +- front/src/Phaser/Game/GameScene.ts | 387 +++++++++--------- front/src/Phaser/Menu/MenuScene.ts | 107 +++-- 9 files changed, 381 insertions(+), 252 deletions(-) create mode 100644 docs/maps/assets/menu-command.png create mode 100644 front/src/Api/Events/ui/MenuItemClickedEvent.ts create mode 100644 front/src/Api/Events/ui/MenuItemRegisterEvent.ts diff --git a/docs/maps/api-ui.md b/docs/maps/api-ui.md index edda8613..572de593 100644 --- a/docs/maps/api-ui.md +++ b/docs/maps/api-ui.md @@ -65,3 +65,26 @@ WA.room.onLeaveZone('myZone', () => { helloWorldPopup.close(); }); ``` + +### register additional menu entries + +adds an additional Entry to the main menu , these exist until the map is unloaded + + +```typescript +registerMenuCommand(menuCommand: string, callback: (menuCommand: string) => void): void +``` +Example: + + +```javascript + +WA.registerMenuCommand("test", () => { + WA.sendChatMessage("test clicked", "menu cmd") +}) + +``` + +
+ +
\ No newline at end of file diff --git a/docs/maps/assets/menu-command.png b/docs/maps/assets/menu-command.png new file mode 100644 index 0000000000000000000000000000000000000000..0caf75c9d772d63e43365040e033d3f93e69cf3c GIT binary patch literal 9856 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 Ho$&t!s2Avf literal 0 HcmV?d00001 diff --git a/front/src/Api/Events/IframeEvent.ts b/front/src/Api/Events/IframeEvent.ts index 4da8ea96..80fc5850 100644 --- a/front/src/Api/Events/IframeEvent.ts +++ b/front/src/Api/Events/IframeEvent.ts @@ -6,12 +6,14 @@ import type { ClosePopupEvent } from './ClosePopupEvent'; import type { EnterLeaveEvent } from './EnterLeaveEvent'; import type { GoToPageEvent } from './GoToPageEvent'; import type { LoadPageEvent } from './LoadPageEvent'; +import type { LoadSoundEvent } from "./LoadSoundEvent"; import type { OpenCoWebSiteEvent } from './OpenCoWebSiteEvent'; import type { OpenPopupEvent } from './OpenPopupEvent'; import type { OpenTabEvent } from './OpenTabEvent'; +import type { PlaySoundEvent } from "./PlaySoundEvent"; +import type { MenuItemClickedEvent } from './ui/MenuItemClickedEvent'; +import type { MenuItemRegisterEvent } from './ui/MenuItemRegisterEvent'; import type { UserInputChatEvent } from './UserInputChatEvent'; -import type { LoadSoundEvent} from "./LoadSoundEvent"; -import type {PlaySoundEvent} from "./PlaySoundEvent"; export interface TypedMessageEvent extends MessageEvent { @@ -36,6 +38,7 @@ export type IframeEventMap = { loadSound: LoadSoundEvent playSound: PlaySoundEvent stopSound: null, + registerMenuCommand: MenuItemRegisterEvent } export interface IframeEvent { type: T; @@ -52,6 +55,7 @@ export interface IframeResponseEventMap { leaveEvent: EnterLeaveEvent buttonClickedEvent: ButtonClickedEvent // gameState: GameStateEvent + menuItemClicked: MenuItemClickedEvent } export interface IframeResponseEvent { type: T; diff --git a/front/src/Api/Events/ui/MenuItemClickedEvent.ts b/front/src/Api/Events/ui/MenuItemClickedEvent.ts new file mode 100644 index 00000000..6444cb09 --- /dev/null +++ b/front/src/Api/Events/ui/MenuItemClickedEvent.ts @@ -0,0 +1,21 @@ +import * as tg from "generic-type-guard"; +import { iframeListener } from '../../IframeListener'; + +export const isMenuItemClickedEvent = + new tg.IsInterface().withProperties({ + menuItem: tg.isString + }).get(); +/** + * A message sent from the game to the iFrame when a menu item is clicked. + */ +export type MenuItemClickedEvent = tg.GuardedType; + + +export function sendMenuClickedEvent(menuItem: string) { + iframeListener.postMessage({ + 'type': 'menuItemClicked', + 'data': { + menuItem: menuItem, + } as MenuItemClickedEvent + }); +} \ No newline at end of file diff --git a/front/src/Api/Events/ui/MenuItemRegisterEvent.ts b/front/src/Api/Events/ui/MenuItemRegisterEvent.ts new file mode 100644 index 00000000..4a56d8a0 --- /dev/null +++ b/front/src/Api/Events/ui/MenuItemRegisterEvent.ts @@ -0,0 +1,25 @@ +import * as tg from "generic-type-guard"; +import { Subject } from 'rxjs'; + +export const isMenuItemRegisterEvent = + new tg.IsInterface().withProperties({ + menutItem: tg.isString + }).get(); +/** + * A message sent from the iFrame to the game to add a new menu item. + */ +export type MenuItemRegisterEvent = tg.GuardedType; + +export const isMenuItemRegisterIframeEvent = + new tg.IsInterface().withProperties({ + type: tg.isSingletonString("registerMenuCommand"), + data: isMenuItemRegisterEvent + }).get(); + + +const _registerMenuCommandStream: Subject = new Subject(); +export const registerMenuCommandStream = _registerMenuCommandStream.asObservable(); + +export function handleMenuItemRegistrationEvent(event: MenuItemRegisterEvent) { + _registerMenuCommandStream.next(event.menutItem) +} \ No newline at end of file diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index 232502a1..11852358 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -1,21 +1,22 @@ import { Subject } from "rxjs"; -import { ChatEvent, isChatEvent } from "./Events/ChatEvent"; import { HtmlUtils } from "../WebRtc/HtmlUtils"; +import type { ButtonClickedEvent } from "./Events/ButtonClickedEvent"; +import { ChatEvent, isChatEvent } from "./Events/ChatEvent"; +import { ClosePopupEvent, isClosePopupEvent } from "./Events/ClosePopupEvent"; import type { EnterLeaveEvent } from "./Events/EnterLeaveEvent"; +import { GoToPageEvent, isGoToPageEvent } from "./Events/GoToPageEvent"; +import { IframeEvent, IframeEventMap, IframeResponseEvent, IframeResponseEventMap, isIframeEventWrapper, TypedMessageEvent } from "./Events/IframeEvent"; +import { isLoadPageEvent } from './Events/LoadPageEvent'; +import { isLoadSoundEvent, LoadSoundEvent } from "./Events/LoadSoundEvent"; +import { isOpenCoWebsite, OpenCoWebSiteEvent } from "./Events/OpenCoWebSiteEvent"; import { isOpenPopupEvent, OpenPopupEvent } from "./Events/OpenPopupEvent"; import { isOpenTabEvent, OpenTabEvent } from "./Events/OpenTabEvent"; -import type { ButtonClickedEvent } from "./Events/ButtonClickedEvent"; -import { ClosePopupEvent, isClosePopupEvent } from "./Events/ClosePopupEvent"; -import { scriptUtils } from "./ScriptUtils"; -import { GoToPageEvent, isGoToPageEvent } from "./Events/GoToPageEvent"; -import { isOpenCoWebsite, OpenCoWebSiteEvent } from "./Events/OpenCoWebSiteEvent"; -import { IframeEventMap, IframeEvent, IframeResponseEvent, IframeResponseEventMap, isIframeEventWrapper, TypedMessageEvent } from "./Events/IframeEvent"; +import { isPlaySoundEvent, PlaySoundEvent } from "./Events/PlaySoundEvent"; +import { isStopSoundEvent, StopSoundEvent } from "./Events/StopSoundEvent"; +import { handleMenuItemRegistrationEvent, isMenuItemRegisterIframeEvent } from './Events/ui/MenuItemRegisterEvent'; import type { UserInputChatEvent } from "./Events/UserInputChatEvent"; -import { isLoadPageEvent } from './Events/LoadPageEvent'; -import {isPlaySoundEvent, PlaySoundEvent} from "./Events/PlaySoundEvent"; -import {isStopSoundEvent, StopSoundEvent} from "./Events/StopSoundEvent"; -import {isLoadSoundEvent, LoadSoundEvent} from "./Events/LoadSoundEvent"; +import { scriptUtils } from "./ScriptUtils"; /** * Listens to messages from iframes and turn those messages into easy to use observables. * Also allows to send messages to those iframes. @@ -33,7 +34,7 @@ class IframeListener { private readonly _goToPageStream: Subject = new Subject(); public readonly goToPageStream = this._goToPageStream.asObservable(); - + private readonly _loadPageStream: Subject = new Subject(); public readonly loadPageStream = this._loadPageStream.asObservable(); @@ -137,9 +138,11 @@ class IframeListener { } else if (payload.type === 'removeBubble') { this._removeBubbleStream.next(); - }else if (payload.type === 'loadPage' && isLoadPageEvent(payload.data)){ + } else if (payload.type === 'loadPage' && isLoadPageEvent(payload.data)) { this._loadPageStream.next(payload.data.url); - } + } else if (isMenuItemRegisterIframeEvent(payload)) [ + handleMenuItemRegistrationEvent(payload.data) + ] } @@ -263,7 +266,7 @@ class IframeListener { /** * Sends the message... to all allowed iframes. */ - private postMessage(message: IframeResponseEvent) { + public postMessage(message: IframeResponseEvent) { for (const iframe of this.iframes) { iframe.contentWindow?.postMessage(message, '*'); } diff --git a/front/src/Api/iframe/ui.ts b/front/src/Api/iframe/ui.ts index 629d3c36..8e9943b2 100644 --- a/front/src/Api/iframe/ui.ts +++ b/front/src/Api/iframe/ui.ts @@ -1,14 +1,17 @@ import { isButtonClickedEvent } from '../Events/ButtonClickedEvent'; -import type { ClosePopupEvent } from '../Events/ClosePopupEvent'; +import { isMenuItemClickedEvent } from '../Events/ui/MenuItemClickedEvent'; +import type { MenuItemRegisterEvent } from '../Events/ui/MenuItemRegisterEvent'; import { IframeApiContribution, sendToWorkadventure } from './IframeApiContribution'; import { apiCallback } from "./registeredCallbacks"; -import {Popup} from "./Ui/Popup"; -import type {ButtonClickedCallback, ButtonDescriptor} from "./Ui/ButtonDescriptor"; +import type { ButtonClickedCallback, ButtonDescriptor } from "./Ui/ButtonDescriptor"; +import { Popup } from "./Ui/Popup"; let popupId = 0; const popups: Map = new Map(); const popupCallbacks: Map> = new Map>(); +const menuCallbacks: Map void> = new Map() + interface ZonedPopupOptions { zone: string objectLayerName?: string, @@ -33,6 +36,16 @@ class WorkAdventureUiCommands extends IframeApiContribution { + const callback = menuCallbacks.get(event.menuItem); + if (callback) { + callback(event.menuItem) + } + } })]; @@ -71,6 +84,16 @@ class WorkAdventureUiCommands extends IframeApiContribution void) { + menuCallbacks.set(commandDescriptor, callback); + sendToWorkadventure({ + 'type': 'registerMenuCommand', + 'data': { + menutItem: commandDescriptor + } as MenuItemRegisterEvent + }); + } + displayBubble(): void { sendToWorkadventure({ 'type': 'displayBubble', data: null }); } diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index b5876d5a..317b27bb 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -1,4 +1,10 @@ -import {gameManager, HasMovedEvent} from "./GameManager"; +import { Queue } from 'queue-typescript'; +import type { Subscription } from "rxjs"; +import { ConsoleGlobalMessageManager } from "../../Administration/ConsoleGlobalMessageManager"; +import { GlobalMessageManager } from "../../Administration/GlobalMessageManager"; +import { userMessageManager } from "../../Administration/UserMessageManager"; +import { iframeListener } from "../../Api/IframeListener"; +import { connectionManager } from "../../Connexion/ConnectionManager"; import type { GroupCreatedUpdatedMessageInterface, MessageUserJoined, @@ -9,13 +15,50 @@ import type { PositionInterface, RoomJoinedMessageInterface } from "../../Connexion/ConnexionModels"; -import {hasMovedEventName, Player, requestEmoteEventName} from "../Player/Player"; +import { localUserStore } from "../../Connexion/LocalUserStore"; +import { Room } from "../../Connexion/Room"; +import type { RoomConnection } from "../../Connexion/RoomConnection"; +import { worldFullMessageStream } from "../../Connexion/WorldFullMessageStream"; import { DEBUG_MODE, JITSI_PRIVATE_MODE, MAX_PER_GROUP, - POSITION_DELAY, + POSITION_DELAY } from "../../Enum/EnvironmentVariable"; +import { TextureError } from "../../Exception/TextureError"; +import type { UserMovedMessage } from "../../Messages/generated/messages_pb"; +import { ProtobufClientUtils } from "../../Network/ProtobufClientUtils"; +import { peerStore } from "../../Stores/PeerStore"; +import { touchScreenManager } from "../../Touch/TouchScreenManager"; +import { urlManager } from "../../Url/UrlManager"; +import { audioManager } from "../../WebRtc/AudioManager"; +import { coWebsiteManager } from "../../WebRtc/CoWebsiteManager"; +import { HtmlUtils } from "../../WebRtc/HtmlUtils"; +import { jitsiFactory } from "../../WebRtc/JitsiFactory"; +import { + AUDIO_LOOP_PROPERTY, AUDIO_VOLUME_PROPERTY, CenterListener, + JITSI_MESSAGE_PROPERTIES, + layoutManager, + LayoutMode, + ON_ACTION_TRIGGER_BUTTON, + TRIGGER_JITSI_PROPERTIES, + TRIGGER_WEBSITE_PROPERTIES, + WEBSITE_MESSAGE_PROPERTIES +} from "../../WebRtc/LayoutManager"; +import { mediaManager } from "../../WebRtc/MediaManager"; +import { SimplePeer, UserSimplePeerInterface } from "../../WebRtc/SimplePeer"; +import { lazyLoadCompanionResource } from "../Companion/CompanionTexturesLoadingManager"; +import { ChatModeIcon } from "../Components/ChatModeIcon"; +import { addLoader } from "../Components/Loader"; +import { joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey } from "../Components/MobileJoystick"; +import { OpenChatIcon, openChatIconName } from "../Components/OpenChatIcon"; +import { PresentationModeIcon } from "../Components/PresentationModeIcon"; +import { TextUtils } from "../Components/TextUtils"; +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, @@ -24,80 +67,35 @@ import type { ITiledMapTileLayer, ITiledTileSet } from "../Map/ITiledMap"; -import type {AddPlayerInterface} from "./AddPlayerInterface"; -import {PlayerAnimationDirections} from "../Player/Animation"; -import {PlayerMovement} from "./PlayerMovement"; -import {PlayersPositionInterpolator} from "./PlayersPositionInterpolator"; -import {RemotePlayer} from "../Entity/RemotePlayer"; -import {Queue} from 'queue-typescript'; -import {SimplePeer, UserSimplePeerInterface} from "../../WebRtc/SimplePeer"; -import {ReconnectingSceneName} from "../Reconnecting/ReconnectingScene"; -import {lazyLoadPlayerCharacterTextures, loadCustomTexture} from "../Entity/PlayerTexturesLoadingManager"; -import { - CenterListener, - JITSI_MESSAGE_PROPERTIES, - layoutManager, - LayoutMode, - ON_ACTION_TRIGGER_BUTTON, - TRIGGER_JITSI_PROPERTIES, - TRIGGER_WEBSITE_PROPERTIES, - WEBSITE_MESSAGE_PROPERTIES, - AUDIO_VOLUME_PROPERTY, - AUDIO_LOOP_PROPERTY -} from "../../WebRtc/LayoutManager"; -import {GameMap} from "./GameMap"; -import {coWebsiteManager} from "../../WebRtc/CoWebsiteManager"; -import {mediaManager} from "../../WebRtc/MediaManager"; -import type {ItemFactoryInterface} from "../Items/ItemFactoryInterface"; -import type {ActionableItem} from "../Items/ActionableItem"; -import {UserInputManager} from "../UserInput/UserInputManager"; -import {soundManager} from "./SoundManager"; -import type {UserMovedMessage} from "../../Messages/generated/messages_pb"; -import {ProtobufClientUtils} from "../../Network/ProtobufClientUtils"; -import {connectionManager} from "../../Connexion/ConnectionManager"; -import type {RoomConnection} from "../../Connexion/RoomConnection"; -import {GlobalMessageManager} from "../../Administration/GlobalMessageManager"; -import {userMessageManager} from "../../Administration/UserMessageManager"; -import {ConsoleGlobalMessageManager} from "../../Administration/ConsoleGlobalMessageManager"; -import {ResizableScene} from "../Login/ResizableScene"; -import {Room} from "../../Connexion/Room"; -import {jitsiFactory} from "../../WebRtc/JitsiFactory"; -import {urlManager} from "../../Url/UrlManager"; -import {audioManager} from "../../WebRtc/AudioManager"; -import {PresentationModeIcon} from "../Components/PresentationModeIcon"; -import {ChatModeIcon} from "../Components/ChatModeIcon"; -import {OpenChatIcon, openChatIconName} from "../Components/OpenChatIcon"; -import {SelectCharacterScene, SelectCharacterSceneName} from "../Login/SelectCharacterScene"; -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 {HtmlUtils} from "../../WebRtc/HtmlUtils"; +import { MenuScene, MenuSceneName } from '../Menu/MenuScene'; +import { PlayerAnimationDirections } from "../Player/Animation"; +import { hasMovedEventName, Player, requestEmoteEventName } from "../Player/Player"; +import { ErrorSceneName } from "../Reconnecting/ErrorScene"; +import { ReconnectingSceneName } from "../Reconnecting/ReconnectingScene"; +import { waScaleManager } from "../Services/WaScaleManager"; +import { PinchManager } from "../UserInput/PinchManager"; +import { UserInputManager } from "../UserInput/UserInputManager"; +import type { AddPlayerInterface } from "./AddPlayerInterface"; +import { DEPTH_OVERLAY_INDEX } from "./DepthIndexes"; +import { DirtyScene } from "./DirtyScene"; +import { EmoteManager } from "./EmoteManager"; +import { gameManager, HasMovedEvent } from "./GameManager"; +import { GameMap } from "./GameMap"; +import { PlayerMovement } from "./PlayerMovement"; +import { PlayersPositionInterpolator } from "./PlayersPositionInterpolator"; +import { soundManager } from "./SoundManager"; 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 EVENT_TYPE =Phaser.Scenes.Events -import type {Subscription} from "rxjs"; -import {worldFullMessageStream} from "../../Connexion/WorldFullMessageStream"; -import { lazyLoadCompanionResource } from "../Companion/CompanionTexturesLoadingManager"; +import EVENT_TYPE = Phaser.Scenes.Events import RenderTexture = Phaser.GameObjects.RenderTexture; import Tilemap = Phaser.Tilemaps.Tilemap; -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 {DEPTH_OVERLAY_INDEX} from "./DepthIndexes"; -import {waScaleManager} from "../Services/WaScaleManager"; -import {peerStore} from "../../Stores/PeerStore"; -import {EmoteManager} from "./EmoteManager"; export interface GameSceneInitInterface { - initPosition: PointInterface|null, + initPosition: PointInterface | null, reconnecting: boolean } @@ -134,10 +132,10 @@ interface DeleteGroupEventInterface { const defaultStartLayerName = 'start'; export class GameScene extends DirtyScene implements CenterListener { - Terrains : Array; + Terrains: Array; CurrentPlayer!: Player; MapPlayers!: Phaser.Physics.Arcade.Group; - MapPlayersByKey : Map = new Map(); + MapPlayersByKey: Map = new Map(); Map!: Phaser.Tilemaps.Tilemap; Layers!: Array; Objects!: Array; @@ -147,10 +145,10 @@ export class GameScene extends DirtyScene implements CenterListener { startY!: number; circleTexture!: CanvasTexture; circleRedTexture!: CanvasTexture; - pendingEvents: Queue = new Queue(); - private initPosition: PositionInterface|null = null; + pendingEvents: Queue = new Queue(); + private initPosition: PositionInterface | null = null; private playersPositionInterpolator = new PlayersPositionInterpolator(); - public connection: RoomConnection|undefined; + public connection: RoomConnection | undefined; private simplePeer!: SimplePeer; private GlobalMessageManager!: GlobalMessageManager; public ConsoleGlobalMessageManager!: ConsoleGlobalMessageManager; @@ -159,7 +157,7 @@ export class GameScene extends DirtyScene implements CenterListener { // A promise that will resolve when the "create" method is called (signaling loading is ended) private createPromise: Promise; private createPromiseResolve!: (value?: void | PromiseLike) => void; - private iframeSubscriptionList! : Array; + private iframeSubscriptionList!: Array; private peerStoreUnsubscribe!: () => void; MapUrlFile: string; RoomId: string; @@ -179,22 +177,22 @@ export class GameScene extends DirtyScene implements CenterListener { private gameMap!: GameMap; private actionableItems: Map = new Map(); // The item that can be selected by pressing the space key. - private outlinedItem: ActionableItem|null = null; + private outlinedItem: ActionableItem | null = null; public userInputManager!: UserInputManager; - private isReconnecting: boolean|undefined = undefined; + private isReconnecting: boolean | undefined = undefined; private startLayerName!: string | null; private openChatIcon!: OpenChatIcon; private playerName!: string; private characterLayers!: string[]; - private companion!: string|null; - private messageSubscription: Subscription|null = null; - private popUpElements : Map = new Map(); - private originalMapUrl: string|undefined; - private pinchManager: PinchManager|undefined; + private companion!: string | null; + private messageSubscription: Subscription | null = null; + private popUpElements: Map = new Map(); + private originalMapUrl: string | undefined; + private pinchManager: PinchManager | undefined; private mapTransitioning: boolean = false; //used to prevent transitions happenning at the same time. private emoteManager!: EmoteManager; - constructor(private room: Room, MapUrlFile: string, customKey?: string|undefined) { + constructor(private room: Room, MapUrlFile: string, customKey?: string | undefined) { super({ key: customKey ?? room.id }); @@ -234,13 +232,13 @@ export class GameScene extends DirtyScene implements CenterListener { //this.load.audio('audio-report-message', '/resources/objects/report-message.mp3'); this.sound.pauseOnBlur = false; - this.load.on(FILE_LOAD_ERROR, (file: {src: string}) => { + this.load.on(FILE_LOAD_ERROR, (file: { src: string }) => { // If we happen to be in HTTP and we are trying to load a URL in HTTPS only... (this happens only in dev environments) if (window.location.protocol === 'http:' && file.src === this.MapUrlFile && file.src.startsWith('http:') && this.originalMapUrl === undefined) { this.originalMapUrl = this.MapUrlFile; this.MapUrlFile = this.MapUrlFile.replace('http://', 'https://'); this.load.tilemapTiledJSON(this.MapUrlFile, this.MapUrlFile); - this.load.on('filecomplete-tilemapJSON-'+this.MapUrlFile, (key: string, type: string, data: unknown) => { + this.load.on('filecomplete-tilemapJSON-' + this.MapUrlFile, (key: string, type: string, data: unknown) => { this.onMapLoad(data); }); return; @@ -254,7 +252,7 @@ export class GameScene extends DirtyScene implements CenterListener { this.originalMapUrl = this.MapUrlFile; this.MapUrlFile = this.MapUrlFile.replace('https://', 'http://'); this.load.tilemapTiledJSON(this.MapUrlFile, this.MapUrlFile); - this.load.on('filecomplete-tilemapJSON-'+this.MapUrlFile, (key: string, type: string, data: unknown) => { + this.load.on('filecomplete-tilemapJSON-' + this.MapUrlFile, (key: string, type: string, data: unknown) => { this.onMapLoad(data); }); return; @@ -266,7 +264,7 @@ export class GameScene extends DirtyScene implements CenterListener { message: this.originalMapUrl ?? file.src }); }); - this.load.on('filecomplete-tilemapJSON-'+this.MapUrlFile, (key: string, type: string, data: unknown) => { + this.load.on('filecomplete-tilemapJSON-' + this.MapUrlFile, (key: string, type: string, data: unknown) => { this.onMapLoad(data); }); //TODO strategy to add access token @@ -278,7 +276,7 @@ export class GameScene extends DirtyScene implements CenterListener { this.onMapLoad(data); } - this.load.spritesheet('layout_modes', 'resources/objects/layout_modes.png', {frameWidth: 32, frameHeight: 32}); + this.load.spritesheet('layout_modes', 'resources/objects/layout_modes.png', { frameWidth: 32, frameHeight: 32 }); this.load.bitmapFont('main_font', 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml'); //eslint-disable-next-line @typescript-eslint/no-explicit-any (this.load as any).rexWebFont({ @@ -315,7 +313,7 @@ export class GameScene extends DirtyScene implements CenterListener { for (const layer of this.mapFile.layers) { if (layer.type === 'objectgroup') { for (const object of layer.objects) { - let objectsOfType: ITiledMapObject[]|undefined; + let objectsOfType: ITiledMapObject[] | undefined; if (!objects.has(object.type)) { objectsOfType = new Array(); } else { @@ -343,7 +341,7 @@ export class GameScene extends DirtyScene implements CenterListener { } default: continue; - //throw new Error('Unsupported object type: "'+ itemType +'"'); + //throw new Error('Unsupported object type: "'+ itemType +'"'); } itemFactory.preload(this.load); @@ -378,7 +376,7 @@ export class GameScene extends DirtyScene implements CenterListener { } //hook initialisation - init(initData : GameSceneInitInterface) { + init(initData: GameSceneInitInterface) { if (initData.initPosition !== undefined) { this.initPosition = initData.initPosition; //todo: still used? } @@ -457,7 +455,7 @@ export class GameScene extends DirtyScene implements CenterListener { this.Objects = new Array(); //initialise list of other player - this.MapPlayers = this.physics.add.group({immovable: true}); + this.MapPlayers = this.physics.add.group({ immovable: true }); //create input to move @@ -563,7 +561,7 @@ export class GameScene extends DirtyScene implements CenterListener { bottom: camera.scrollY + camera.height, }, this.companion - ).then((onConnect: OnConnectInterface) => { + ).then((onConnect: OnConnectInterface) => { this.connection = onConnect.connection; this.connection.onUserJoins((message: MessageUserJoined) => { @@ -716,23 +714,23 @@ export class GameScene extends DirtyScene implements CenterListener { const contextRed = this.circleRedTexture.context; contextRed.beginPath(); contextRed.arc(48, 48, 48, 0, 2 * Math.PI, false); - //context.lineWidth = 5; + //context.lineWidth = 5; contextRed.strokeStyle = '#ff0000'; contextRed.stroke(); this.circleRedTexture.refresh(); } - private safeParseJSONstring(jsonString: string|undefined, propertyName: string) { + private safeParseJSONstring(jsonString: string | undefined, propertyName: string) { try { return jsonString ? JSON.parse(jsonString) : {}; - } catch(e) { + } catch (e) { console.warn('Invalid JSON found in property "' + propertyName + '" of the map:' + jsonString, e); return {} } } - private triggerOnMapLayerPropertyChange(){ + private triggerOnMapLayerPropertyChange() { this.gameMap.onPropertyChange('exitSceneUrl', (newValue, oldValue) => { if (newValue) this.onMapExit(newValue as string); }); @@ -743,22 +741,22 @@ export class GameScene extends DirtyScene implements CenterListener { if (newValue === undefined) { layoutManager.removeActionButton('openWebsite', this.userInputManager); coWebsiteManager.closeCoWebsite(); - }else{ + } else { const openWebsiteFunction = () => { coWebsiteManager.loadCoWebsite(newValue as string, this.MapUrlFile, allProps.get('openWebsiteAllowApi') as boolean | undefined, allProps.get('openWebsitePolicy') as string | undefined); layoutManager.removeActionButton('openWebsite', this.userInputManager); }; const openWebsiteTriggerValue = allProps.get(TRIGGER_WEBSITE_PROPERTIES); - if(openWebsiteTriggerValue && openWebsiteTriggerValue === ON_ACTION_TRIGGER_BUTTON) { + if (openWebsiteTriggerValue && openWebsiteTriggerValue === ON_ACTION_TRIGGER_BUTTON) { let message = allProps.get(WEBSITE_MESSAGE_PROPERTIES); - if(message === undefined){ + if (message === undefined) { message = 'Press SPACE or touch here to open web site'; } layoutManager.addActionButton('openWebsite', message.toString(), () => { openWebsiteFunction(); }, this.userInputManager); - }else{ + } else { openWebsiteFunction(); } } @@ -767,12 +765,12 @@ export class GameScene extends DirtyScene implements CenterListener { if (newValue === undefined) { layoutManager.removeActionButton('jitsiRoom', this.userInputManager); this.stopJitsi(); - }else{ + } else { const openJitsiRoomFunction = () => { const roomName = jitsiFactory.getRoomName(newValue.toString(), this.instance); - const jitsiUrl = allProps.get("jitsiUrl") as string|undefined; + const jitsiUrl = allProps.get("jitsiUrl") as string | undefined; if (JITSI_PRIVATE_MODE && !jitsiUrl) { - const adminTag = allProps.get("jitsiRoomAdminTag") as string|undefined; + const adminTag = allProps.get("jitsiRoomAdminTag") as string | undefined; this.connection?.emitQueryJitsiJwtMessage(roomName, adminTag); } else { @@ -782,7 +780,7 @@ export class GameScene extends DirtyScene implements CenterListener { } const jitsiTriggerValue = allProps.get(TRIGGER_JITSI_PROPERTIES); - if(jitsiTriggerValue && jitsiTriggerValue === ON_ACTION_TRIGGER_BUTTON) { + if (jitsiTriggerValue && jitsiTriggerValue === ON_ACTION_TRIGGER_BUTTON) { let message = allProps.get(JITSI_MESSAGE_PROPERTIES); if (message === undefined) { message = 'Press SPACE or touch here to enter Jitsi Meet room'; @@ -790,7 +788,7 @@ export class GameScene extends DirtyScene implements CenterListener { layoutManager.addActionButton('jitsiRoom', message.toString(), () => { openJitsiRoomFunction(); }, this.userInputManager); - }else{ + } else { openJitsiRoomFunction(); } } @@ -803,8 +801,8 @@ export class GameScene extends DirtyScene implements CenterListener { } }); 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; + 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 @@ -823,13 +821,13 @@ export class GameScene extends DirtyScene implements CenterListener { } private listenToIframeEvents(): void { - this.iframeSubscriptionList = []; - this.iframeSubscriptionList.push(iframeListener.openPopupStream.subscribe((openPopupEvent) => { + this.iframeSubscriptionList = []; + this.iframeSubscriptionList.push(iframeListener.openPopupStream.subscribe((openPopupEvent) => { - let objectLayerSquare : ITiledMapObject; + let objectLayerSquare: ITiledMapObject; const targetObjectData = this.getObjectLayerData(openPopupEvent.targetObject); - if (targetObjectData !== undefined){ - objectLayerSquare = targetObjectData; + if (targetObjectData !== undefined) { + objectLayerSquare = targetObjectData; } else { console.error("Error while opening a popup. Cannot find an object on the map with name '" + openPopupEvent.targetObject + "'. The first parameter of WA.openPopup() must be the name of a rectangle object in your map."); return; @@ -842,14 +840,14 @@ ${escapedMessage} html += buttonContainer; let id = 0; for (const button of openPopupEvent.buttons) { - html += ``; + html += ``; id++; } html += ''; - const domElement = this.add.dom(objectLayerSquare.x , + const domElement = this.add.dom(objectLayerSquare.x, objectLayerSquare.y).createFromHTML(html); - const container : HTMLDivElement = domElement.getChildByID("container") as HTMLDivElement; + const container: HTMLDivElement = domElement.getChildByID("container") as HTMLDivElement; container.style.width = objectLayerSquare.width + "px"; domElement.scale = 0; domElement.setClassName('popUpElement'); @@ -869,73 +867,70 @@ ${escapedMessage} id++; } this.tweens.add({ - targets : domElement , - scale : 1, - ease : "EaseOut", - duration : 400, + targets: domElement, + scale: 1, + ease: "EaseOut", + duration: 400, }); this.popUpElements.set(openPopupEvent.popupId, domElement); })); - this.iframeSubscriptionList.push(iframeListener.closePopupStream.subscribe((closePopupEvent) => { + this.iframeSubscriptionList.push(iframeListener.closePopupStream.subscribe((closePopupEvent) => { const popUpElement = this.popUpElements.get(closePopupEvent.popupId); if (popUpElement === undefined) { - console.error('Could not close popup with ID ', closePopupEvent.popupId,'. Maybe it has already been closed?'); + console.error('Could not close popup with ID ', closePopupEvent.popupId, '. Maybe it has already been closed?'); } this.tweens.add({ - targets : popUpElement , - scale : 0, - ease : "EaseOut", - duration : 400, - onComplete : () => { + targets: popUpElement, + scale: 0, + ease: "EaseOut", + duration: 400, + onComplete: () => { popUpElement?.destroy(); this.popUpElements.delete(closePopupEvent.popupId); }, }); })); - this.iframeSubscriptionList.push(iframeListener.disablePlayerControlStream.subscribe(()=>{ + this.iframeSubscriptionList.push(iframeListener.disablePlayerControlStream.subscribe(() => { this.userInputManager.disableControls(); })); - this.iframeSubscriptionList.push(iframeListener.playSoundStream.subscribe((playSoundEvent)=> - { - const url = new URL(playSoundEvent.url, this.MapUrlFile); - soundManager.playSound(this.load,this.sound,url.toString(),playSoundEvent.config); - })) + this.iframeSubscriptionList.push(iframeListener.playSoundStream.subscribe((playSoundEvent) => { + const url = new URL(playSoundEvent.url, this.MapUrlFile); + soundManager.playSound(this.load, this.sound, url.toString(), playSoundEvent.config); + })) - this.iframeSubscriptionList.push(iframeListener.stopSoundStream.subscribe((stopSoundEvent)=> - { + this.iframeSubscriptionList.push(iframeListener.stopSoundStream.subscribe((stopSoundEvent) => { const url = new URL(stopSoundEvent.url, this.MapUrlFile); - soundManager.stopSound(this.sound,url.toString()); + soundManager.stopSound(this.sound, url.toString()); })) - this.iframeSubscriptionList.push(iframeListener.loadSoundStream.subscribe((loadSoundEvent)=> - { + this.iframeSubscriptionList.push(iframeListener.loadSoundStream.subscribe((loadSoundEvent) => { const url = new URL(loadSoundEvent.url, this.MapUrlFile); - soundManager.loadSound(this.load,this.sound,url.toString()); + soundManager.loadSound(this.load, this.sound, url.toString()); })) - this.iframeSubscriptionList.push(iframeListener.enablePlayerControlStream.subscribe(()=>{ + this.iframeSubscriptionList.push(iframeListener.enablePlayerControlStream.subscribe(() => { this.userInputManager.restoreControls(); })); - this.iframeSubscriptionList.push(iframeListener.loadPageStream.subscribe((url:string)=>{ - this.loadNextGame(url).then(()=>{ - this.events.once(EVENT_TYPE.POST_UPDATE,()=>{ + this.iframeSubscriptionList.push(iframeListener.loadPageStream.subscribe((url: string) => { + this.loadNextGame(url).then(() => { + this.events.once(EVENT_TYPE.POST_UPDATE, () => { this.onMapExit(url); }) }) })); - let scriptedBubbleSprite : Sprite; - this.iframeSubscriptionList.push(iframeListener.displayBubbleStream.subscribe(()=>{ - scriptedBubbleSprite = new Sprite(this,this.CurrentPlayer.x + 25,this.CurrentPlayer.y,'circleSprite-white'); + let scriptedBubbleSprite: Sprite; + this.iframeSubscriptionList.push(iframeListener.displayBubbleStream.subscribe(() => { + scriptedBubbleSprite = new Sprite(this, this.CurrentPlayer.x + 25, this.CurrentPlayer.y, 'circleSprite-white'); scriptedBubbleSprite.setDisplayOrigin(48, 48); this.add.existing(scriptedBubbleSprite); })); - this.iframeSubscriptionList.push(iframeListener.removeBubbleStream.subscribe(()=>{ + this.iframeSubscriptionList.push(iframeListener.removeBubbleStream.subscribe(() => { scriptedBubbleSprite.destroy(); })); @@ -948,9 +943,11 @@ ${escapedMessage} private onMapExit(exitKey: string) { if (this.mapTransitioning) return; this.mapTransitioning = true; - const {roomId, hash} = Room.getIdFromIdentifier(exitKey, this.MapUrlFile, this.instance); - if (!roomId) throw new Error('Could not find the room from its exit key: '+exitKey); + const { roomId, hash } = Room.getIdFromIdentifier(exitKey, this.MapUrlFile, this.instance); + if (!roomId) throw new Error('Could not find the room from its exit key: ' + exitKey); urlManager.pushStartLayerNameToUrl(hash); + const menuScene: MenuScene = this.scene.get(MenuSceneName) as MenuScene + menuScene.reset() if (roomId !== this.scene.key) { if (this.scene.get(roomId) === null) { console.error("next room not loaded", exitKey); @@ -992,7 +989,7 @@ ${escapedMessage} mediaManager.hideGameOverlay(); - for(const iframeEvents of this.iframeSubscriptionList){ + for (const iframeEvents of this.iframeSubscriptionList) { iframeEvents.unsubscribe(); } } @@ -1012,7 +1009,7 @@ ${escapedMessage} private switchLayoutMode(): void { //if discussion is activated, this layout cannot be activated - if(mediaManager.activatedDiscussion){ + if (mediaManager.activatedDiscussion) { return; } const mode = layoutManager.getLayoutMode(); @@ -1053,24 +1050,24 @@ ${escapedMessage} private initPositionFromLayerName(layerName: string) { for (const layer of this.gameMap.layersIterator) { - if ((layerName === layer.name || layer.name.endsWith('/'+layerName)) && layer.type === 'tilelayer' && (layerName === defaultStartLayerName || this.isStartLayer(layer))) { + if ((layerName === layer.name || layer.name.endsWith('/' + layerName)) && layer.type === 'tilelayer' && (layerName === defaultStartLayerName || this.isStartLayer(layer))) { const startPosition = this.startUser(layer); - this.startX = startPosition.x + this.mapFile.tilewidth/2; - this.startY = startPosition.y + this.mapFile.tileheight/2; + this.startX = startPosition.x + this.mapFile.tilewidth / 2; + this.startY = startPosition.y + this.mapFile.tileheight / 2; } } } - private getExitUrl(layer: ITiledMapLayer): string|undefined { - return this.getProperty(layer, "exitUrl") as string|undefined; + private getExitUrl(layer: ITiledMapLayer): string | undefined { + return this.getProperty(layer, "exitUrl") as string | undefined; } /** * @deprecated the map property exitSceneUrl is deprecated */ - private getExitSceneUrl(layer: ITiledMapLayer): string|undefined { - return this.getProperty(layer, "exitSceneUrl") as string|undefined; + private getExitSceneUrl(layer: ITiledMapLayer): string | undefined { + return this.getProperty(layer, "exitSceneUrl") as string | undefined; } private isStartLayer(layer: ITiledMapLayer): boolean { @@ -1081,8 +1078,8 @@ ${escapedMessage} 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[]|undefined = layer.properties; + private getProperty(layer: ITiledMapLayer | ITiledMap, name: string): string | boolean | number | undefined { + const properties: ITiledMapLayerProperty[] | undefined = layer.properties; if (!properties) { return undefined; } @@ -1093,8 +1090,8 @@ ${escapedMessage} return obj.value; } - private getProperties(layer: ITiledMapLayer|ITiledMap, name: string): (string|number|boolean|undefined)[] { - const properties: ITiledMapLayerProperty[]|undefined = layer.properties; + private getProperties(layer: ITiledMapLayer | ITiledMap, name: string): (string | number | boolean | undefined)[] { + const properties: ITiledMapLayerProperty[] | undefined = layer.properties; if (!properties) { return []; } @@ -1103,29 +1100,29 @@ ${escapedMessage} //todo: push that into the gameManager private loadNextGame(exitSceneIdentifier: string): Promise { - const {roomId, hash} = Room.getIdFromIdentifier(exitSceneIdentifier, this.MapUrlFile, this.instance); + const { roomId, hash } = Room.getIdFromIdentifier(exitSceneIdentifier, this.MapUrlFile, this.instance); const room = new Room(roomId); - return gameManager.loadMap(room, this.scene).catch(() => {}); + return gameManager.loadMap(room, this.scene).catch(() => { }); } private startUser(layer: ITiledMapTileLayer): PositionInterface { const tiles = layer.data; - if (typeof(tiles) === 'string') { + if (typeof (tiles) === 'string') { throw new Error('The content of a JSON map must be filled as a JSON array, not as a string'); } - const possibleStartPositions : PositionInterface[] = []; - tiles.forEach((objectKey : number, key: number) => { - if(objectKey === 0){ + const possibleStartPositions: PositionInterface[] = []; + tiles.forEach((objectKey: number, key: number) => { + if (objectKey === 0) { return; } const y = Math.floor(key / layer.width); const x = key % layer.width; - possibleStartPositions.push({x: x * this.mapFile.tilewidth, y: y * this.mapFile.tilewidth}); + possibleStartPositions.push({ x: x * this.mapFile.tilewidth, y: y * this.mapFile.tilewidth }); }); // Get a value at random amongst allowed values if (possibleStartPositions.length === 0) { - console.warn('The start layer "'+layer.name+'" for this map is empty.'); + console.warn('The start layer "' + layer.name + '" for this map is empty.'); return { x: 0, y: 0 @@ -1137,12 +1134,12 @@ ${escapedMessage} //todo: in a dedicated class/function? initCamera() { - this.cameras.main.setBounds(0,0, this.Map.widthInPixels, this.Map.heightInPixels); + this.cameras.main.setBounds(0, 0, this.Map.widthInPixels, this.Map.heightInPixels); this.cameras.main.startFollow(this.CurrentPlayer, true); this.updateCameraOffset(); } - addLayer(Layer : Phaser.Tilemaps.TilemapLayer){ + addLayer(Layer: Phaser.Tilemaps.TilemapLayer) { this.Layers.push(Layer); } @@ -1152,7 +1149,7 @@ ${escapedMessage} this.physics.add.collider(this.CurrentPlayer, Layer, (object1: GameObject, object2: GameObject) => { //this.CurrentPlayer.say("Collision with layer : "+ (object2 as Tile).layer.name) }); - Layer.setCollisionByProperty({collides: true}); + Layer.setCollisionByProperty({ collides: true }); if (DEBUG_MODE) { //debug code to see the collision hitbox of the object in the top layer Layer.renderDebug(this.add.graphics(), { @@ -1164,7 +1161,7 @@ ${escapedMessage} }); } - createCurrentPlayer(){ + createCurrentPlayer() { //TODO create animation moving between exit and start const texturesPromise = lazyLoadPlayerCharacterTextures(this.load, this.characterLayers); try { @@ -1189,8 +1186,8 @@ ${escapedMessage} this.CurrentPlayer.on(requestEmoteEventName, (emoteKey: string) => { this.connection?.emitEmoteEvent(emoteKey); }) - }catch (err){ - if(err instanceof TextureError) { + } catch (err) { + if (err instanceof TextureError) { gameManager.leaveGame(this, SelectCharacterSceneName, new SelectCharacterScene()); } throw err; @@ -1251,7 +1248,7 @@ ${escapedMessage} } let shortestDistance: number = Infinity; - let selectedItem: ActionableItem|null = null; + let selectedItem: ActionableItem | null = null; for (const item of this.actionableItems.values()) { const distance = item.actionableDistance(x, y); if (distance !== null && distance < shortestDistance) { @@ -1285,7 +1282,7 @@ ${escapedMessage} * @param time * @param delta The delta time in ms since the last frame. This is a smoothed and capped value based on the FPS rate. */ - update(time: number, delta: number) : void { + update(time: number, delta: number): void { this.dirty = false; mediaManager.updateScene(); this.currentTick = time; @@ -1345,8 +1342,8 @@ ${escapedMessage} const currentPlayerId = this.connection?.getUserId(); this.removeAllRemotePlayers(); // load map - usersPosition.forEach((userPosition : MessageUserPositionInterface) => { - if(userPosition.userId === currentPlayerId){ + usersPosition.forEach((userPosition: MessageUserPositionInterface) => { + if (userPosition.userId === currentPlayerId) { return; } this.addPlayer(userPosition); @@ -1356,16 +1353,16 @@ ${escapedMessage} /** * Called by the connexion when a new player arrives on a map */ - public addPlayer(addPlayerData : AddPlayerInterface) : void { + public addPlayer(addPlayerData: AddPlayerInterface): void { this.pendingEvents.enqueue({ type: "AddPlayerEvent", event: addPlayerData }); } - private doAddPlayer(addPlayerData : AddPlayerInterface): void { + private doAddPlayer(addPlayerData: AddPlayerInterface): void { //check if exist player, if exist, move position - if(this.MapPlayersByKey.has(addPlayerData.userId)){ + if (this.MapPlayersByKey.has(addPlayerData.userId)) { this.updatePlayerPosition({ userId: addPlayerData.userId, position: addPlayerData.position @@ -1427,10 +1424,10 @@ ${escapedMessage} } private doUpdatePlayerPosition(message: MessageUserMovedInterface): void { - const player : RemotePlayer | undefined = this.MapPlayersByKey.get(message.userId); + const player: RemotePlayer | undefined = this.MapPlayersByKey.get(message.userId); if (player === undefined) { //throw new Error('Cannot find player with ID "' + message.userId +'"'); - console.error('Cannot update position of player with ID "' + message.userId +'": player not found'); + console.error('Cannot update position of player with ID "' + message.userId + '": player not found'); return; } @@ -1474,7 +1471,7 @@ ${escapedMessage} doDeleteGroup(groupId: number): void { const group = this.groups.get(groupId); - if(!group){ + if (!group) { return; } group.destroy(); @@ -1503,7 +1500,7 @@ ${escapedMessage} bottom: camera.scrollY + camera.height, }); } - private getObjectLayerData(objectName : string) : ITiledMapObject| undefined{ + private getObjectLayerData(objectName: string): ITiledMapObject | undefined { for (const layer of this.mapFile.layers) { if (layer.type === 'objectgroup' && layer.name === 'floorLayer') { for (const object of layer.objects) { @@ -1537,7 +1534,7 @@ ${escapedMessage} const game = HtmlUtils.querySelectorOrFail('#game canvas'); // Let's put this in Game coordinates by applying the zoom level: - this.cameras.main.setFollowOffset((xCenter - game.offsetWidth/2) * window.devicePixelRatio / this.scale.zoom , (yCenter - game.offsetHeight/2) * window.devicePixelRatio / this.scale.zoom); + this.cameras.main.setFollowOffset((xCenter - game.offsetWidth / 2) * window.devicePixelRatio / this.scale.zoom, (yCenter - game.offsetHeight / 2) * window.devicePixelRatio / this.scale.zoom); } public onCenterChange(): void { @@ -1546,16 +1543,16 @@ ${escapedMessage} public startJitsi(roomName: string, jwt?: string): void { 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; + 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, jitsiUrl); this.connection?.setSilent(true); mediaManager.hideGameOverlay(); //permit to stop jitsi when user close iframe - mediaManager.addTriggerCloseJitsiFrameButton('close-jisi',() => { + mediaManager.addTriggerCloseJitsiFrameButton('close-jisi', () => { this.stopJitsi(); }); } @@ -1569,7 +1566,7 @@ ${escapedMessage} } //todo: put this into an 'orchestrator' scene (EntryScene?) - private bannedUser(){ + private bannedUser() { this.cleanupClosingScene(); this.userInputManager.disableControls(); this.scene.start(ErrorSceneName, { @@ -1580,22 +1577,22 @@ ${escapedMessage} } //todo: put this into an 'orchestrator' scene (EntryScene?) - private showWorldFullError(message: string|null): void { + private showWorldFullError(message: string | null): void { this.cleanupClosingScene(); this.scene.stop(ReconnectingSceneName); this.scene.remove(ReconnectingSceneName); this.userInputManager.disableControls(); //FIX ME to use status code - if(message == undefined){ + if (message == undefined) { this.scene.start(ErrorSceneName, { title: 'Connection rejected', subTitle: 'The world you are trying to join is full. Try again later.', message: 'If you want more information, you may contact us at: workadventure@thecodingmachine.com' }); - }else{ + } else { this.scene.start(ErrorSceneName, { title: 'Connection rejected', - subTitle: 'You cannot join the World. Try again later. \n\r \n\r Error: '+message+'.', + subTitle: 'You cannot join the World. Try again later. \n\r \n\r Error: ' + message + '.', message: 'If you want more information, you may contact administrator or contact us at: workadventure@thecodingmachine.com' }); } diff --git a/front/src/Phaser/Menu/MenuScene.ts b/front/src/Phaser/Menu/MenuScene.ts index 54fa395a..d5d0387a 100644 --- a/front/src/Phaser/Menu/MenuScene.ts +++ b/front/src/Phaser/Menu/MenuScene.ts @@ -1,16 +1,20 @@ -import {LoginScene, LoginSceneName} from "../Login/LoginScene"; -import {SelectCharacterScene, SelectCharacterSceneName} from "../Login/SelectCharacterScene"; -import {SelectCompanionScene, SelectCompanionSceneName} from "../Login/SelectCompanionScene"; -import {gameManager} from "../Game/GameManager"; -import {localUserStore} from "../../Connexion/LocalUserStore"; -import {mediaManager} from "../../WebRtc/MediaManager"; -import {gameReportKey, gameReportRessource, ReportMenu} from "./ReportMenu"; -import {connectionManager} from "../../Connexion/ConnectionManager"; -import {GameConnexionTypes} from "../../Url/UrlManager"; -import {WarningContainer, warningContainerHtml, warningContainerKey} from "../Components/WarningContainer"; -import {worldFullWarningStream} from "../../Connexion/WorldFullWarningStream"; -import {menuIconVisible} from "../../Stores/MenuStore"; -import {videoConstraintStore} from "../../Stores/MediaStore"; +import { Subscription } from 'rxjs'; +import { sendMenuClickedEvent } from '../../Api/Events/ui/MenuItemClickedEvent'; +import { registerMenuCommandStream } from '../../Api/Events/ui/MenuItemRegisterEvent'; +import { connectionManager } from "../../Connexion/ConnectionManager"; +import { localUserStore } from "../../Connexion/LocalUserStore"; +import { worldFullWarningStream } from "../../Connexion/WorldFullWarningStream"; +import { videoConstraintStore } from "../../Stores/MediaStore"; +import { menuIconVisible } from "../../Stores/MenuStore"; +import { GameConnexionTypes } from "../../Url/UrlManager"; +import { HtmlUtils } from '../../WebRtc/HtmlUtils'; +import { mediaManager } from "../../WebRtc/MediaManager"; +import { WarningContainer, warningContainerHtml, warningContainerKey } from "../Components/WarningContainer"; +import { gameManager } from "../Game/GameManager"; +import { LoginScene, LoginSceneName } from "../Login/LoginScene"; +import { SelectCharacterScene, SelectCharacterSceneName } from "../Login/SelectCharacterScene"; +import { SelectCompanionScene, SelectCompanionSceneName } from "../Login/SelectCompanionScene"; +import { gameReportKey, gameReportRessource, ReportMenu } from "./ReportMenu"; export const MenuSceneName = 'MenuScene'; const gameMenuKey = 'gameMenu'; @@ -37,15 +41,38 @@ export class MenuScene extends Phaser.Scene { private menuButton!: Phaser.GameObjects.DOMElement; private warningContainer: WarningContainer | null = null; private warningContainerTimeout: NodeJS.Timeout | null = null; - + private subscriptions = new Subscription() constructor() { - super({key: MenuSceneName}); + super({ key: MenuSceneName }); this.gameQualityValue = localUserStore.getGameQualityValue(); this.videoQualityValue = localUserStore.getVideoQualityValue(); + + this.subscriptions.add(registerMenuCommandStream.subscribe(menuCommand => { + this.addMenuOption(menuCommand); + + })) } - preload () { + reset() { + const addedMenuItems = [...this.menuElement.node.querySelectorAll(".fromApi")]; + for (let index = addedMenuItems.length - 1; index >= 0; index--) { + addedMenuItems[index].remove() + } + } + + public addMenuOption(menuText: string) { + const wrappingSection = document.createElement("section") + const escapedHtml = HtmlUtils.escapeHtml(menuText); + wrappingSection.innerHTML = `` + const menuItemContainer = this.menuElement.node.querySelector("#gameMenu main"); + if (menuItemContainer) { + menuItemContainer.querySelector(`#${escapedHtml}.fromApi`)?.remove() + menuItemContainer.insertBefore(wrappingSection, menuItemContainer.querySelector("#socialLinks")) + } + } + + preload() { this.load.html(gameMenuKey, 'resources/html/gameMenu.html'); this.load.html(gameMenuIconKey, 'resources/html/gameMenuIcon.html'); this.load.html(gameSettingsMenuKey, 'resources/html/gameQualityMenu.html'); @@ -68,11 +95,11 @@ export class MenuScene extends Phaser.Scene { this.gameShareElement = this.add.dom(middleX, -400).createFromCache(gameShare); MenuScene.revealMenusAfterInit(this.gameShareElement, gameShare); this.gameShareElement.addListener('click'); - this.gameShareElement.on('click', (event:MouseEvent) => { + this.gameShareElement.on('click', (event: MouseEvent) => { event.preventDefault(); - if((event?.target as HTMLInputElement).id === 'gameShareFormSubmit') { + if ((event?.target as HTMLInputElement).id === 'gameShareFormSubmit') { this.copyLink(); - }else if((event?.target as HTMLInputElement).id === 'gameShareFormCancel') { + } else if ((event?.target as HTMLInputElement).id === 'gameShareFormCancel') { this.closeGameShare(); } }); @@ -128,8 +155,8 @@ export class MenuScene extends Phaser.Scene { } //TODO bind with future metadata of card //if (connectionManager.getConnexionType === GameConnexionTypes.anonymous){ - const adminSection = this.menuElement.getChildByID('socialLinks') as HTMLElement; - adminSection.hidden = false; + const adminSection = this.menuElement.getChildByID('socialLinks') as HTMLElement; + adminSection.hidden = false; //} this.tweens.add({ targets: this.menuElement, @@ -179,28 +206,28 @@ export class MenuScene extends Phaser.Scene { this.settingsMenuOpened = true; const gameQualitySelect = this.gameQualityMenuElement.getChildByID('select-game-quality') as HTMLInputElement; - gameQualitySelect.value = ''+this.gameQualityValue; + gameQualitySelect.value = '' + this.gameQualityValue; const videoQualitySelect = this.gameQualityMenuElement.getChildByID('select-video-quality') as HTMLInputElement; - videoQualitySelect.value = ''+this.videoQualityValue; + videoQualitySelect.value = '' + this.videoQualityValue; this.gameQualityMenuElement.addListener('click'); - this.gameQualityMenuElement.on('click', (event:MouseEvent) => { + this.gameQualityMenuElement.on('click', (event: MouseEvent) => { event.preventDefault(); if ((event?.target as HTMLInputElement).id === 'gameQualityFormSubmit') { const gameQualitySelect = this.gameQualityMenuElement.getChildByID('select-game-quality') as HTMLInputElement; const videoQualitySelect = this.gameQualityMenuElement.getChildByID('select-video-quality') as HTMLInputElement; this.saveSetting(parseInt(gameQualitySelect.value), parseInt(videoQualitySelect.value)); - } else if((event?.target as HTMLInputElement).id === 'gameQualityFormCancel') { + } else if ((event?.target as HTMLInputElement).id === 'gameQualityFormCancel') { this.closeGameQualityMenu(); } }); - let middleY = this.scale.height / 2 - 392/2; - if(middleY < 0){ + let middleY = this.scale.height / 2 - 392 / 2; + if (middleY < 0) { middleY = 0; } - let middleX = this.scale.width / 2 - 457/2; - if(middleX < 0){ + let middleX = this.scale.width / 2 - 457 / 2; + if (middleX < 0) { middleX = 0; } this.tweens.add({ @@ -226,7 +253,7 @@ export class MenuScene extends Phaser.Scene { } - private openGameShare(): void{ + private openGameShare(): void { if (this.gameShareOpened) { this.closeGameShare(); return; @@ -240,11 +267,11 @@ export class MenuScene extends Phaser.Scene { this.gameShareOpened = true; let middleY = this.scale.height / 2 - 85; - if(middleY < 0){ + if (middleY < 0) { middleY = 0; } let middleX = this.scale.width / 2 - 200; - if(middleX < 0){ + if (middleX < 0) { middleX = 0; } this.tweens.add({ @@ -256,7 +283,7 @@ export class MenuScene extends Phaser.Scene { }); } - private closeGameShare(): void{ + private closeGameShare(): void { const gameShareInfo = this.gameShareElement.getChildByID('gameShareInfo') as HTMLParagraphElement; gameShareInfo.innerText = ''; gameShareInfo.style.display = 'none'; @@ -269,12 +296,18 @@ export class MenuScene extends Phaser.Scene { }); } - private onMenuClick(event:MouseEvent) { - if((event?.target as HTMLInputElement).classList.contains('not-button')){ + private onMenuClick(event: MouseEvent) { + const htmlMenuItem = (event?.target as HTMLInputElement); + if (htmlMenuItem.classList.contains('not-button')) { return; } event.preventDefault(); + if (htmlMenuItem.classList.contains("fromApi")) { + sendMenuClickedEvent(htmlMenuItem.id) + return + } + switch ((event?.target as HTMLInputElement).id) { case 'changeNameButton': this.closeSideMenu(); @@ -316,7 +349,7 @@ export class MenuScene extends Phaser.Scene { gameShareInfo.style.display = 'block'; } - private saveSetting(valueGame: number, valueVideo: number){ + private saveSetting(valueGame: number, valueVideo: number) { if (valueGame !== this.gameQualityValue) { this.gameQualityValue = valueGame; localUserStore.setGameQualityValue(valueGame); @@ -337,7 +370,7 @@ export class MenuScene extends Phaser.Scene { window.open(sparkHost, '_blank'); } - private closeAll(){ + private closeAll() { this.closeGameQualityMenu(); this.closeGameShare(); this.gameReportElement.close(); From 64a00481f031699b3a5d9ef37fcd038294a80737 Mon Sep 17 00:00:00 2001 From: jonny Date: Mon, 21 Jun 2021 18:39:02 +0200 Subject: [PATCH 42/46] fixed wrong import --- .../src/Api/Events/ui/MenuItemClickedEvent.ts | 9 -------- front/src/Api/iframe/Ui/MenuItem.ts | 11 +++++++++ front/src/Phaser/Menu/MenuScene.ts | 2 +- front/src/iframe_api.ts | 23 ++++++++++--------- 4 files changed, 24 insertions(+), 21 deletions(-) create mode 100644 front/src/Api/iframe/Ui/MenuItem.ts diff --git a/front/src/Api/Events/ui/MenuItemClickedEvent.ts b/front/src/Api/Events/ui/MenuItemClickedEvent.ts index 6444cb09..fad2944f 100644 --- a/front/src/Api/Events/ui/MenuItemClickedEvent.ts +++ b/front/src/Api/Events/ui/MenuItemClickedEvent.ts @@ -1,5 +1,4 @@ import * as tg from "generic-type-guard"; -import { iframeListener } from '../../IframeListener'; export const isMenuItemClickedEvent = new tg.IsInterface().withProperties({ @@ -11,11 +10,3 @@ export const isMenuItemClickedEvent = export type MenuItemClickedEvent = tg.GuardedType; -export function sendMenuClickedEvent(menuItem: string) { - iframeListener.postMessage({ - 'type': 'menuItemClicked', - 'data': { - menuItem: menuItem, - } as MenuItemClickedEvent - }); -} \ No newline at end of file diff --git a/front/src/Api/iframe/Ui/MenuItem.ts b/front/src/Api/iframe/Ui/MenuItem.ts new file mode 100644 index 00000000..9782ea7a --- /dev/null +++ b/front/src/Api/iframe/Ui/MenuItem.ts @@ -0,0 +1,11 @@ +import type { MenuItemClickedEvent } from '../../Events/ui/MenuItemClickedEvent'; +import { iframeListener } from '../../IframeListener'; + +export function sendMenuClickedEvent(menuItem: string) { + iframeListener.postMessage({ + 'type': 'menuItemClicked', + 'data': { + menuItem: menuItem, + } as MenuItemClickedEvent + }); +} \ No newline at end of file diff --git a/front/src/Phaser/Menu/MenuScene.ts b/front/src/Phaser/Menu/MenuScene.ts index d5d0387a..e405a758 100644 --- a/front/src/Phaser/Menu/MenuScene.ts +++ b/front/src/Phaser/Menu/MenuScene.ts @@ -1,6 +1,6 @@ import { Subscription } from 'rxjs'; -import { sendMenuClickedEvent } from '../../Api/Events/ui/MenuItemClickedEvent'; import { registerMenuCommandStream } from '../../Api/Events/ui/MenuItemRegisterEvent'; +import { sendMenuClickedEvent } from '../../Api/iframe/Ui/MenuItem'; import { connectionManager } from "../../Connexion/ConnectionManager"; import { localUserStore } from "../../Connexion/LocalUserStore"; import { worldFullWarningStream } from "../../Connexion/WorldFullWarningStream"; diff --git a/front/src/iframe_api.ts b/front/src/iframe_api.ts index ae5321cf..874f0ace 100644 --- a/front/src/iframe_api.ts +++ b/front/src/iframe_api.ts @@ -1,4 +1,3 @@ -import {registeredCallbacks} from "./Api/iframe/registeredCallbacks"; import { IframeResponseEvent, IframeResponseEventMap, @@ -6,15 +5,17 @@ import { TypedMessageEvent } from "./Api/Events/IframeEvent"; import chat from "./Api/iframe/chat"; -import type {IframeCallback} from './Api/iframe/IframeApiContribution'; -import nav from "./Api/iframe/nav"; import controls from "./Api/iframe/controls"; -import ui from "./Api/iframe/ui"; -import sound from "./Api/iframe/sound"; +import type { IframeCallback } from './Api/iframe/IframeApiContribution'; +import nav from "./Api/iframe/nav"; +import { registeredCallbacks } from "./Api/iframe/registeredCallbacks"; import room from "./Api/iframe/room"; -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 sound from "./Api/iframe/sound"; +import type { Sound } from "./Api/iframe/Sound/Sound"; +import ui from "./Api/iframe/ui"; +import type { ButtonDescriptor } from "./Api/iframe/Ui/ButtonDescriptor"; +import type { Popup } from "./Api/iframe/Ui/Popup"; + const wa = { ui, @@ -77,7 +78,7 @@ const wa = { /** * @deprecated Use WA.sound.loadSound instead */ - loadSound(url: string) : Sound { + loadSound(url: string): Sound { console.warn('Method WA.loadSound is deprecated. Please use WA.sound.loadSound instead'); return sound.loadSound(url); }, @@ -85,7 +86,7 @@ const wa = { /** * @deprecated Use WA.nav.goToPage instead */ - goToPage(url : string) : void { + goToPage(url: string): void { console.warn('Method WA.goToPage is deprecated. Please use WA.nav.goToPage instead'); nav.goToPage(url); }, @@ -101,7 +102,7 @@ const wa = { /** * @deprecated Use WA.nav.openCoWebSite instead */ - openCoWebSite(url : string) : void{ + openCoWebSite(url: string): void { console.warn('Method WA.openCoWebSite is deprecated. Please use WA.nav.openCoWebSite instead'); nav.openCoWebSite(url); }, From 8be29062f6c17bcd7eb8c5b225944b0b345dd4e4 Mon Sep 17 00:00:00 2001 From: jonny Date: Mon, 21 Jun 2021 18:41:41 +0200 Subject: [PATCH 43/46] reverted import sorting --- front/src/iframe_api.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/front/src/iframe_api.ts b/front/src/iframe_api.ts index 874f0ace..7b6b2db9 100644 --- a/front/src/iframe_api.ts +++ b/front/src/iframe_api.ts @@ -1,3 +1,4 @@ +import { registeredCallbacks } from "./Api/iframe/registeredCallbacks"; import { IframeResponseEvent, IframeResponseEventMap, @@ -5,17 +6,15 @@ import { TypedMessageEvent } from "./Api/Events/IframeEvent"; import chat from "./Api/iframe/chat"; -import controls from "./Api/iframe/controls"; import type { IframeCallback } from './Api/iframe/IframeApiContribution'; import nav from "./Api/iframe/nav"; -import { registeredCallbacks } from "./Api/iframe/registeredCallbacks"; -import room from "./Api/iframe/room"; -import sound from "./Api/iframe/sound"; -import type { Sound } from "./Api/iframe/Sound/Sound"; +import controls from "./Api/iframe/controls"; import ui from "./Api/iframe/ui"; +import sound from "./Api/iframe/sound"; +import room from "./Api/iframe/room"; import type { ButtonDescriptor } from "./Api/iframe/Ui/ButtonDescriptor"; import type { Popup } from "./Api/iframe/Ui/Popup"; - +import type { Sound } from "./Api/iframe/Sound/Sound"; const wa = { ui, From be23db5bcf6736145638ff227cda0811ad9a6c0c Mon Sep 17 00:00:00 2001 From: GRL Date: Tue, 22 Jun 2021 16:07:31 +0200 Subject: [PATCH 44/46] Resolve import and LoadPageEvent issue --- front/src/Api/Events/IframeEvent.ts | 4 ++ front/src/Api/Events/LoadPageEvent.ts | 13 ++++++ front/src/Api/IframeListener.ts | 66 +++++++++++++++++---------- front/src/Api/iframe/nav.ts | 3 +- front/src/iframe_api.ts | 6 ++- front/yarn.lock | 9 +++- 6 files changed, 75 insertions(+), 26 deletions(-) create mode 100644 front/src/Api/Events/LoadPageEvent.ts diff --git a/front/src/Api/Events/IframeEvent.ts b/front/src/Api/Events/IframeEvent.ts index 99222e58..c3e2f6c9 100644 --- a/front/src/Api/Events/IframeEvent.ts +++ b/front/src/Api/Events/IframeEvent.ts @@ -15,6 +15,8 @@ import type { LayerEvent } from './LayerEvent'; import type { SetPropertyEvent } from "./setPropertyEvent"; import type { LoadSoundEvent } from "./LoadSoundEvent"; import type { PlaySoundEvent } from "./PlaySoundEvent"; +import type { MenuItemClickedEvent } from "./MenuItemClickedEvent"; +import type { HasPlayerMovedEvent } from "./HasPlayerMovedEvent"; export interface TypedMessageEvent extends MessageEvent { @@ -44,6 +46,8 @@ export type IframeEventMap = { loadSound: LoadSoundEvent playSound: PlaySoundEvent stopSound: null, + getState: undefined, + registerMenuCommand: undefined } export interface IframeEvent { type: T; diff --git a/front/src/Api/Events/LoadPageEvent.ts b/front/src/Api/Events/LoadPageEvent.ts new file mode 100644 index 00000000..9bc7f32a --- /dev/null +++ b/front/src/Api/Events/LoadPageEvent.ts @@ -0,0 +1,13 @@ +import * as tg from "generic-type-guard"; + + + +export const isLoadPageEvent = + new tg.IsInterface().withProperties({ + url: tg.isString, + }).get(); + +/** + * A message sent from the iFrame to the game to add a message in the chat. + */ +export type LoadPageEvent = tg.GuardedType; \ No newline at end of file diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index e7638d04..a495d92b 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -1,21 +1,36 @@ - -import { Subject } from "rxjs"; -import { ChatEvent, isChatEvent } from "./Events/ChatEvent"; -import { HtmlUtils } from "../WebRtc/HtmlUtils"; -import type { EnterLeaveEvent } from "./Events/EnterLeaveEvent"; -import { isOpenPopupEvent, OpenPopupEvent } from "./Events/OpenPopupEvent"; -import { isOpenTabEvent, OpenTabEvent } from "./Events/OpenTabEvent"; -import type { ButtonClickedEvent } from "./Events/ButtonClickedEvent"; -import { ClosePopupEvent, isClosePopupEvent } from "./Events/ClosePopupEvent"; -import { scriptUtils } from "./ScriptUtils"; -import { GoToPageEvent, isGoToPageEvent } from "./Events/GoToPageEvent"; -import { isOpenCoWebsite, OpenCoWebSiteEvent } from "./Events/OpenCoWebSiteEvent"; -import { IframeEventMap, IframeEvent, IframeResponseEvent, IframeResponseEventMap, isIframeEventWrapper, TypedMessageEvent } from "./Events/IframeEvent"; -import type { UserInputChatEvent } from "./Events/UserInputChatEvent"; -import { isLoadPageEvent } from './Events/LoadPageEvent'; +import {Subject} from "rxjs"; +import {ChatEvent, isChatEvent} from "./Events/ChatEvent"; +import {HtmlUtils} from "../WebRtc/HtmlUtils"; +import type {EnterLeaveEvent} from "./Events/EnterLeaveEvent"; +import {isOpenPopupEvent, OpenPopupEvent} from "./Events/OpenPopupEvent"; +import {isOpenTabEvent, OpenTabEvent} from "./Events/OpenTabEvent"; +import type {ButtonClickedEvent} from "./Events/ButtonClickedEvent"; +import {ClosePopupEvent, isClosePopupEvent} from "./Events/ClosePopupEvent"; +import {scriptUtils} from "./ScriptUtils"; +import {GoToPageEvent, isGoToPageEvent} from "./Events/GoToPageEvent"; +import {isOpenCoWebsite, OpenCoWebSiteEvent} from "./Events/OpenCoWebSiteEvent"; +import { + IframeEvent, + IframeEventMap, + IframeResponseEvent, + IframeResponseEventMap, + isIframeEventWrapper, + TypedMessageEvent +} from "./Events/IframeEvent"; +import type {UserInputChatEvent} from "./Events/UserInputChatEvent"; +//import { isLoadPageEvent } from './Events/LoadPageEvent'; import {isPlaySoundEvent, PlaySoundEvent} from "./Events/PlaySoundEvent"; import {isStopSoundEvent, StopSoundEvent} from "./Events/StopSoundEvent"; import {isLoadSoundEvent, LoadSoundEvent} from "./Events/LoadSoundEvent"; +import {isSetPropertyEvent, SetPropertyEvent} from "./Events/setPropertyEvent"; +import {isLayerEvent, LayerEvent} from "./Events/LayerEvent"; +import {isMenuItemRegisterEvent} from "./Events/MenuItemRegisterEvent"; +import type {DataLayerEvent} from "./Events/DataLayerEvent"; +import type {GameStateEvent} from "./Events/GameStateEvent"; +import type {MenuItemClickedEvent} from "./Events/MenuItemClickedEvent"; +import type {HasPlayerMovedEvent} from "./Events/HasPlayerMovedEvent"; +import {isLoadPageEvent} from "./Events/LoadPageEvent"; + /** * Listens to messages from iframes and turn those messages into easy to use observables. * Also allows to send messages to those iframes. @@ -34,6 +49,9 @@ class IframeListener { private readonly _goToPageStream: Subject = new Subject(); public readonly goToPageStream = this._goToPageStream.asObservable(); + private readonly _loadPageStream: Subject = new Subject(); + public readonly loadPageStream = this._loadPageStream.asObservable(); + private readonly _openCoWebSiteStream: Subject = new Subject(); public readonly openCoWebSiteStream = this._openCoWebSiteStream.asObservable(); @@ -135,6 +153,9 @@ class IframeListener { 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); } @@ -216,7 +237,7 @@ class IframeListener { 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.id = IframeListener.getIFrameId(scriptUrl); iframe.style.display = 'none'; iframe.src = '/iframe.html?script=' + encodeURIComponent(scriptUrl); @@ -231,25 +252,24 @@ class IframeListener { } else { // production code const iframe = document.createElement('iframe'); - iframe.id = this.getIFrameId(scriptUrl); + iframe.id = IframeListener.getIFrameId(scriptUrl); iframe.style.display = 'none'; // 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' + + //iframe.src = "data:text/html;charset=utf-8," + escape(html); + iframe.srcdoc = '\n' + '\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); @@ -259,12 +279,12 @@ class IframeListener { } - private getIFrameId(scriptUrl: string): string { + private static getIFrameId(scriptUrl: string): string { return 'script' + btoa(scriptUrl); } unregisterScript(scriptUrl: string): void { - const iFrameId = this.getIFrameId(scriptUrl); + const iFrameId = IframeListener.getIFrameId(scriptUrl); const iframe = HtmlUtils.getElementByIdOrFail(iFrameId); if (!iframe) { throw new Error('Unknown iframe for script "' + scriptUrl + '"'); diff --git a/front/src/Api/iframe/nav.ts b/front/src/Api/iframe/nav.ts index b6798330..f6add88e 100644 --- a/front/src/Api/iframe/nav.ts +++ b/front/src/Api/iframe/nav.ts @@ -2,6 +2,7 @@ import type { GoToPageEvent } from '../Events/GoToPageEvent'; import type { OpenTabEvent } from '../Events/OpenTabEvent'; import { IframeApiContribution, sendToWorkadventure } from './IframeApiContribution'; import type {OpenCoWebSiteEvent} from "../Events/OpenCoWebSiteEvent"; +import type {LoadPageEvent} from "../Events/LoadPageEvent"; class WorkadventureNavigationCommands extends IframeApiContribution { @@ -31,7 +32,7 @@ class WorkadventureNavigationCommands extends IframeApiContribution void> = new Map() const wa = { ui, nav, @@ -100,7 +103,8 @@ const wa = { /** * @deprecated Use WA.nav.openCoWebSite instead - */openCoWebSite(url: string): void { + */ + openCoWebSite(url: string): void { console.warn('Method WA.openCoWebSite is deprecated. Please use WA.nav.openCoWebSite instead'); nav.openCoWebSite(url); }, diff --git a/front/yarn.lock b/front/yarn.lock index e64a76c1..a96be8aa 100644 --- a/front/yarn.lock +++ b/front/yarn.lock @@ -1417,6 +1417,13 @@ create-require@^1.1.0: resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== +cross-env@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" + integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw== + dependencies: + cross-spawn "^7.0.1" + cross-spawn@^6.0.0, cross-spawn@^6.0.5: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" @@ -1428,7 +1435,7 @@ cross-spawn@^6.0.0, cross-spawn@^6.0.5: shebang-command "^1.2.0" which "^1.2.9" -cross-spawn@^7.0.2, cross-spawn@^7.0.3: +cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== From bdb32a29e184f0c3e2f40266fcbb23e6efa88451 Mon Sep 17 00:00:00 2001 From: GRL Date: Wed, 23 Jun 2021 11:32:11 +0200 Subject: [PATCH 45/46] New methods refactored --- CHANGELOG.md | 16 ++-- docs/maps/api-player.md | 21 +++++ docs/maps/api-reference.md | 118 +----------------------- docs/maps/api-room.md | 81 ++++++++++++++++ docs/maps/api-ui.md | 9 +- docs/maps/scripting.md | 2 +- front/src/Api/IframeListener.ts | 2 +- front/src/Api/iframe/player.ts | 29 ++++++ front/src/Api/iframe/room.ts | 91 +++++++++++++++++- front/src/Phaser/Game/GameScene.ts | 2 +- front/src/iframe_api.ts | 2 + maps/tests/Metadata/getCurrentRoom.html | 2 +- maps/tests/Metadata/getCurrentUser.html | 2 +- maps/tests/Metadata/playerMove.html | 2 +- maps/tests/Metadata/setProperty.html | 4 +- maps/tests/Metadata/showHideLayer.html | 4 +- 16 files changed, 246 insertions(+), 141 deletions(-) create mode 100644 docs/maps/api-player.md create mode 100644 front/src/Api/iframe/player.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index fa9d743e..fb4f6d27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ - Enabled outlines on actionable item again (they were disabled when migrating to Phaser 3.50) #1218 - Enabled outlines on player names (when the mouse hovers on a player you can interact with) #1219 - Migrated the admin console to Svelte, and redesigned the console #1211 +- New scripting API features : + - Use `WA.room.showLayer(): void` to show a layer + - Use `WA.room.hideLayer(): void` to hide a layer + - Use `WA.room.setProperty() : void` to add or change existing property of a layer + - Use `WA.player.onPlayerMove(): void` to track the movement of the current player + - Use `WA.room.getCurrentUser(): Promise` to get the ID, name and tags of the current player + - Use `WA.room.getCurrentRoom(): Promise` to get the ID, JSON map file, url of the map of the current room and the layer where the current player started + - Use `WA.ui.registerMenuCommand(): void` to add a custom menu ## Version 1.4.1 @@ -48,13 +56,7 @@ - Added a new `DISPLAY_TERMS_OF_USE` environment variable to trigger the display of terms of use - New scripting API features: - Use `WA.loadSound(): Sound` to load / play / stop a sound - - Use `WA.showLayer(): void` to show a layer - - Use `WA.hideLayer(): void` to hide a layer - - Use `WA.setProperty() : void` to add or change existing property of a layer - - Use `WA.onPlayerMove(): void` to track the movement of the current player - - Use `WA.getCurrentUser(): Promise` to get the ID, name and tags of the current player - - Use `WA.getCurrentRoom(): Promise` to get the ID, JSON map file, url of the map of the current room and the layer where the current player started - - Use `WA.registerMenuCommand(): void` to add a custom menu + ### Bug Fixes diff --git a/docs/maps/api-player.md b/docs/maps/api-player.md new file mode 100644 index 00000000..f483731e --- /dev/null +++ b/docs/maps/api-player.md @@ -0,0 +1,21 @@ +{.section-title.accent.text-primary} +# API Player functions Reference + +### Listen to player movement +``` +WA.player.onPlayerMove(callback: HasPlayerMovedEventCallback): void; +``` +Listens to the movement of the current user and calls the callback. Sends an event when the user stops moving, changes direction and every 200ms when moving in the same direction. + +The event has the following attributes : +* **moving (boolean):** **true** when the current player is moving, **false** otherwise. +* **direction (string):** **"right"** | **"left"** | **"down"** | **"top"** the direction where the current player is moving. +* **x (number):** coordinate X of the current player. +* **y (number):** coordinate Y of the current player. + +**callback:** the function that will be called when the current player is moving. It contains the event. + +Example : +```javascript +WA.player.onPlayerMove(console.log); +``` \ No newline at end of file diff --git a/docs/maps/api-reference.md b/docs/maps/api-reference.md index a39ec1db..30a11b2a 100644 --- a/docs/maps/api-reference.md +++ b/docs/maps/api-reference.md @@ -4,123 +4,9 @@ - [Navigation functions](api-nav.md) - [Chat functions](api-chat.md) - [Room functions](api-room.md) +- [Player functions](api-player.md) - [UI functions](api-ui.md) - [Sound functions](api-sound.md) - [Controls functions](api-controls.md) -- [List of deprecated functions](api-deprecated.md) - -### Show / Hide a layer -``` -WA.showLayer(layerName : string): void -WA.hideLayer(layerName : string) : void -``` -These 2 methods can be used to show and hide a layer. - -Example : -```javascript -WA.showLayer('bottom'); -//... -WA.hideLayer('bottom'); -``` - -### Set/Create properties in a layer - -``` -WA.setProperty(layerName : string, propertyName : string, propertyValue : string | number | boolean | undefined) : void; -``` - -Set the value of the `propertyName` property of the layer `layerName` at `propertyValue`. If the property doesn't exist, create the property `propertyName` and set the value of the property at `propertyValue`. - -Example : -```javascript -WA.setProperty('wikiLayer', 'openWebsite', 'https://www.wikipedia.org/'); -``` - -### Listen to player movement - -``` -onPlayerMove(callback: HasPlayerMovedEventCallback): void; -``` -Listens to the movement of the current user and calls the callback. Sends an event when the user stops moving, changes direction and every 200ms when moving in the same direction. - -The event has the following attributes : -* **moving (boolean):** **true** when the current player is moving, **false** otherwise. -* **direction (string):** **"right"** | **"left"** | **"down"** | **"top"** the direction where the current player is moving. -* **x (number):** coordinate X of the current player. -* **y (number):** coordinate Y of the current player. - -**callback:** the function that will be called when the current player is moving. It contains the event. - -Example : -```javascript -WA.onPlayerMove(console.log); -``` - -### Getting informations on the current user -``` -getCurrentUser(): Promise -``` -Return a promise that resolves to a `User` object with the following attributes : -* **id (string) :** ID of the current user -* **nickName (string) :** name displayed above the current user -* **tags (string[]) :** list of all the tags of the current user - -Example : -```javascript -WA.getCurrentUser().then((user) => { - if (user.nickName === 'ABC') { - console.log(user.tags); - } -}) -``` - -### Getting informations on the current room -``` -getCurrentRoom(): Promise -``` -Return a promise that resolves to a `Room` object with the following attributes : -* **id (string) :** ID of the current room -* **map (ITiledMap) :** contains the JSON map file with the properties that were setted by the script if `setProperty` was called. -* **mapUrl (string) :** Url of the JSON map file -* **startLayer (string | null) :** Name of the layer where the current user started, only if different from `start` layer - -Example : -```javascript -WA.getCurrentRoom((room) => { - if (room.id === '42') { - console.log(room.map); - window.open(room.mapUrl, '_blank'); - } -}) -``` - -### Add a custom menu -``` -registerMenuCommand(commandDescriptor: string, callback: (commandDescriptor: string) => void): void -``` -Add a custom menu item containing the text `commandDescriptor`. A click on the menu will trigger the `callback`. - -Example : -```javascript -WA.registerMenuCommand('About', () => { - console.log("The About menu was clicked"); -}); -``` - - -### Working with group layers -If you use group layers in your map, to reference a layer in a group you will need to use a `/` to join layer names together. - -Example : -
-
- -
-
- -The name of the layers of this map are : -* `entries/start` -* `bottom/ground/under` -* `bottom/build/carpet` -* `wall` \ No newline at end of file +- [List of deprecated functions](api-deprecated.md) \ No newline at end of file diff --git a/docs/maps/api-room.md b/docs/maps/api-room.md index dc7a8612..d8381cc6 100644 --- a/docs/maps/api-room.md +++ b/docs/maps/api-room.md @@ -1,6 +1,22 @@ {.section-title.accent.text-primary} # API Room functions Reference +### Working with group layers +If you use group layers in your map, to reference a layer in a group you will need to use a `/` to join layer names together. + +Example : +
+
+ +
+
+ +The name of the layers of this map are : +* `entries/start` +* `bottom/ground/under` +* `bottom/build/carpet` +* `wall` + ### Detecting when the user enters/leaves a zone ``` @@ -31,3 +47,68 @@ WA.room.onLeaveZone('myZone', () => { WA.chat.sendChatMessage("Goodbye!", 'Mr Robot'); }) ``` + +### Show / Hide a layer +``` +WA.room.showLayer(layerName : string): void +WA.room.hideLayer(layerName : string) : void +``` +These 2 methods can be used to show and hide a layer. + +Example : +```javascript +WA.room.showLayer('bottom'); +//... +WA.room.hideLayer('bottom'); +``` + +### Set/Create properties in a layer + +``` +WA.room.setProperty(layerName : string, propertyName : string, propertyValue : string | number | boolean | undefined) : void; +``` + +Set the value of the `propertyName` property of the layer `layerName` at `propertyValue`. If the property doesn't exist, create the property `propertyName` and set the value of the property at `propertyValue`. + +Example : +```javascript +WA.room.setProperty('wikiLayer', 'openWebsite', 'https://www.wikipedia.org/'); +``` + +### Getting information on the current room +``` +WA.room.getCurrentRoom(): Promise +``` +Return a promise that resolves to a `Room` object with the following attributes : +* **id (string) :** ID of the current room +* **map (ITiledMap) :** contains the JSON map file with the properties that were setted by the script if `setProperty` was called. +* **mapUrl (string) :** Url of the JSON map file +* **startLayer (string | null) :** Name of the layer where the current user started, only if different from `start` layer + +Example : +```javascript +WA.room.getCurrentRoom((room) => { + if (room.id === '42') { + console.log(room.map); + window.open(room.mapUrl, '_blank'); + } +}) +``` + +### Getting information on the current user +``` +WA.player.getCurrentUser(): Promise +``` +Return a promise that resolves to a `User` object with the following attributes : +* **id (string) :** ID of the current user +* **nickName (string) :** name displayed above the current user +* **tags (string[]) :** list of all the tags of the current user + +Example : +```javascript +WA.room.getCurrentUser().then((user) => { + if (user.nickName === 'ABC') { + console.log(user.tags); + } +}) +``` diff --git a/docs/maps/api-ui.md b/docs/maps/api-ui.md index 44248b53..fc38dc41 100644 --- a/docs/maps/api-ui.md +++ b/docs/maps/api-ui.md @@ -66,16 +66,15 @@ WA.room.onLeaveZone('myZone', () => { }); ``` -### register additional menu entries - -adds an additional Entry to the main menu , these exist until the map is unloaded - +### Add custom menu ```typescript WA.ui.registerMenuCommand(menuCommand: string, callback: (menuCommand: string) => void): void ``` -Example: +Add a custom menu item containing the text `commandDescriptor` in the main menu. A click on the menu will trigger the `callback`. +Custom menu exist only until the map is unloaded, or you leave the iframe zone of the script. +Example: ```javascript diff --git a/docs/maps/scripting.md b/docs/maps/scripting.md index b9dee484..5f645b81 100644 --- a/docs/maps/scripting.md +++ b/docs/maps/scripting.md @@ -101,7 +101,7 @@ You can now start by testing this with a simple message sent to the chat. ```html ... ... ``` diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index c4aa29b1..9311d7b6 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -204,7 +204,7 @@ class IframeListener { } - sendFrozenGameStateEvent(gameStateEvent: GameStateEvent) { + sendGameStateEvent(gameStateEvent: GameStateEvent) { this.postMessage({ 'type': 'gameState', 'data': gameStateEvent diff --git a/front/src/Api/iframe/player.ts b/front/src/Api/iframe/player.ts new file mode 100644 index 00000000..67c012f7 --- /dev/null +++ b/front/src/Api/iframe/player.ts @@ -0,0 +1,29 @@ +import {IframeApiContribution, sendToWorkadventure} from "./IframeApiContribution"; +import type {HasPlayerMovedEvent, HasPlayerMovedEventCallback} from "../Events/HasPlayerMovedEvent"; +import {Subject} from "rxjs"; +import {apiCallback} from "./registeredCallbacks"; +import {isHasPlayerMovedEvent} from "../Events/HasPlayerMovedEvent"; + +const moveStream = new Subject(); + +class WorkadventurePlayerCommands extends IframeApiContribution { + callbacks = [ + apiCallback({ + type: 'hasPlayerMoved', + typeChecker: isHasPlayerMovedEvent, + callback: (payloadData) => { + moveStream.next(payloadData); + } + }), + ] + + onPlayerMove(callback: HasPlayerMovedEventCallback): void { + moveStream.subscribe(callback); + sendToWorkadventure({ + type: 'onPlayerMove', + data: null + }) + } +} + +export default new WorkadventurePlayerCommands(); \ No newline at end of file diff --git a/front/src/Api/iframe/room.ts b/front/src/Api/iframe/room.ts index ed412166..ea990d90 100644 --- a/front/src/Api/iframe/room.ts +++ b/front/src/Api/iframe/room.ts @@ -1,10 +1,54 @@ import { Subject } from "rxjs"; import { EnterLeaveEvent, isEnterLeaveEvent } from '../Events/EnterLeaveEvent'; -import { IframeApiContribution } from './IframeApiContribution'; +import {IframeApiContribution, sendToWorkadventure} from './IframeApiContribution'; import { apiCallback } from "./registeredCallbacks"; +import type {LayerEvent} from "../Events/LayerEvent"; +import type {SetPropertyEvent} from "../Events/setPropertyEvent"; +import type {GameStateEvent} from "../Events/GameStateEvent"; +import type {ITiledMap} from "../../Phaser/Map/ITiledMap"; +import type {DataLayerEvent} from "../Events/DataLayerEvent"; +import {isGameStateEvent} from "../Events/GameStateEvent"; +import {isDataLayerEvent} from "../Events/DataLayerEvent"; const enterStreams: Map> = new Map>(); const leaveStreams: Map> = new Map>(); +const dataLayerResolver = new Subject(); +const stateResolvers = new Subject(); + +let immutableData: GameStateEvent; + +interface Room { + id: string, + mapUrl: string, + map: ITiledMap, + startLayer: string | null +} + +interface User { + id: string | undefined, + nickName: string | null, + tags: string[] +} + + +function getGameState(): Promise { + if (immutableData) { + return Promise.resolve(immutableData); + } + else { + return new Promise((resolver, thrower) => { + stateResolvers.subscribe(resolver); + sendToWorkadventure({type: "getState", data: null}); + }) + } +} + +function getDataLayer(): Promise { + return new Promise((resolver, thrower) => { + dataLayerResolver.subscribe(resolver); + sendToWorkadventure({type: "getDataLayer", data: null}) + }) +} class WorkadventureRoomCommands extends IframeApiContribution { callbacks = [ @@ -21,8 +65,21 @@ class WorkadventureRoomCommands extends IframeApiContribution { leaveStreams.get(payloadData.name)?.next(); } - }) - + }), + apiCallback({ + type: "gameState", + typeChecker: isGameStateEvent, + callback: (payloadData) => { + stateResolvers.next(payloadData); + } + }), + apiCallback({ + type: "dataLayer", + typeChecker: isDataLayerEvent, + callback: (payloadData) => { + dataLayerResolver.next(payloadData); + } + }), ] @@ -43,6 +100,34 @@ class WorkadventureRoomCommands extends IframeApiContribution { + return getGameState().then((gameState) => { + return getDataLayer().then((mapJson) => { + return {id: gameState.roomId, map: mapJson.data as ITiledMap, mapUrl: gameState.mapUrl, startLayer: gameState.startLayerName}; + }) + }) + } + getCurrentUser(): Promise { + return getGameState().then((gameState) => { + return {id: gameState.uuid, nickName: gameState.nickname, tags: gameState.tags}; + }) + } } diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 8494503c..52d678ad 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -945,7 +945,7 @@ ${escapedMessage} })) this.iframeSubscriptionList.push(iframeListener.gameStateStream.subscribe(() => { - iframeListener.sendFrozenGameStateEvent({ + iframeListener.sendGameStateEvent({ mapUrl: this.MapUrlFile, startLayerName: this.startLayerName, uuid: localUserStore.getLocalUser()?.uuid, diff --git a/front/src/iframe_api.ts b/front/src/iframe_api.ts index 7b6b2db9..cc17fff2 100644 --- a/front/src/iframe_api.ts +++ b/front/src/iframe_api.ts @@ -12,6 +12,7 @@ import controls from "./Api/iframe/controls"; import ui from "./Api/iframe/ui"; import sound from "./Api/iframe/sound"; import room from "./Api/iframe/room"; +import player 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"; @@ -23,6 +24,7 @@ const wa = { chat, sound, room, + player, // All methods below are deprecated and should not be used anymore. // They are kept here for backward compatibility. diff --git a/maps/tests/Metadata/getCurrentRoom.html b/maps/tests/Metadata/getCurrentRoom.html index b290c6a4..7429b2a8 100644 --- a/maps/tests/Metadata/getCurrentRoom.html +++ b/maps/tests/Metadata/getCurrentRoom.html @@ -5,7 +5,7 @@ \ No newline at end of file diff --git a/maps/tests/Metadata/setProperty.html b/maps/tests/Metadata/setProperty.html index 06b029da..5259ec0a 100644 --- a/maps/tests/Metadata/setProperty.html +++ b/maps/tests/Metadata/setProperty.html @@ -5,8 +5,8 @@ \ No newline at end of file diff --git a/maps/tests/Metadata/showHideLayer.html b/maps/tests/Metadata/showHideLayer.html index 391ec449..4677f9e5 100644 --- a/maps/tests/Metadata/showHideLayer.html +++ b/maps/tests/Metadata/showHideLayer.html @@ -10,10 +10,10 @@ From 95d8cf92577e8ab847fa7873883243b96be922ec Mon Sep 17 00:00:00 2001 From: GRL Date: Wed, 23 Jun 2021 14:54:06 +0200 Subject: [PATCH 46/46] Change requested --- docs/maps/api-ui.md | 2 +- docs/maps/assets/menu-command.png | Bin 9856 -> 0 bytes front/src/Api/iframe/chat.ts | 2 +- front/src/Api/iframe/nav.ts | 8 ++++---- front/src/Api/iframe/room.ts | 6 +++--- front/src/Api/iframe/ui.ts | 2 +- front/src/WebRtc/MediaManager.ts | 8 ++++---- 7 files changed, 14 insertions(+), 14 deletions(-) delete mode 100644 docs/maps/assets/menu-command.png diff --git a/docs/maps/api-ui.md b/docs/maps/api-ui.md index fc38dc41..286f2ac7 100644 --- a/docs/maps/api-ui.md +++ b/docs/maps/api-ui.md @@ -85,5 +85,5 @@ WA.ui.registerMenuCommand("test", () => { ```
- +
\ No newline at end of file diff --git a/docs/maps/assets/menu-command.png b/docs/maps/assets/menu-command.png deleted file mode 100644 index 0caf75c9d772d63e43365040e033d3f93e69cf3c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9856 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 Ho$&t!s2Avf diff --git a/front/src/Api/iframe/chat.ts b/front/src/Api/iframe/chat.ts index 5f73b744..7d8e6f71 100644 --- a/front/src/Api/iframe/chat.ts +++ b/front/src/Api/iframe/chat.ts @@ -23,7 +23,7 @@ class WorkadventureChatCommands extends IframeApiContribution { diff --git a/front/src/Api/iframe/ui.ts b/front/src/Api/iframe/ui.ts index 8e9943b2..c7655b84 100644 --- a/front/src/Api/iframe/ui.ts +++ b/front/src/Api/iframe/ui.ts @@ -90,7 +90,7 @@ class WorkAdventureUiCommands extends IframeApiContribution { - const elementChildre = element.children.item(index); - if (!elementChildre) { + const elementChildren = element.children.item(index); + if (!elementChildren) { return; } - elementChildre.classList.remove('active'); + elementChildren.classList.remove('active'); if ((index + 1) > volume) { return; } - elementChildre.classList.add('active'); + elementChildren.classList.add('active'); }); }