From 3836d5037c95d2ec78a3a8855ed2c7c0df5ed20b Mon Sep 17 00:00:00 2001 From: jonny Date: Wed, 21 Apr 2021 15:51:01 +0200 Subject: [PATCH 01/82] 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/82] 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/82] 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/82] 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/82] 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/82] 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/82] 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/82] 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/82] 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/82] 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/82] 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/82] 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/82] 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/82] 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/82] 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/82] 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/82] 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/82] 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/82] 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/82] 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/82] 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/82] 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/82] 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/82] 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 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·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 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/82] 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/82] 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/82] 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/82] 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/82] 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/82] 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/82] 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/82] 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/82] 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/82] 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/82] 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/82] 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/82] 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/82] 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/82] 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 e7b0f859a567f21dffe9e88a831206b399dbeb31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Fri, 11 Jun 2021 11:29:36 +0200 Subject: [PATCH 41/82] Migrating the video overlay in Svelte (WIP) --- front/src/Components/App.svelte | 6 +- .../HorizontalSoundMeterWidget.svelte | 2 +- front/src/Components/SoundMeterWidget.svelte | 10 +- .../Components/Video/LocalStreamMedia.svelte | 20 ++++ front/src/Components/Video/Peer.svelte | 20 ++++ .../Video/ScreenSharingMedia.svelte | 57 +++++++++ front/src/Components/Video/VideoMedia.svelte | 75 ++++++++++++ .../src/Components/Video/VideoOverlay.svelte | 32 +++++ .../src/Components/Video/images/blockSign.svg | 22 ++++ front/src/Components/Video/images/report.svg | 1 + front/src/Phaser/Game/GameScene.ts | 7 +- .../src/Stores/GameOverlayStoreVisibility.ts | 17 +++ front/src/Stores/LayoutStore.ts | 58 +++++++++ front/src/Stores/MediaStore.ts | 17 +-- front/src/Stores/PeerStore.ts | 111 ++++++++++++++++-- front/src/Stores/ScreenSharingStore.ts | 46 ++++++-- front/src/WebRtc/MediaManager.ts | 37 ++---- front/src/WebRtc/ScreenSharingPeer.ts | 73 ++++++++++-- front/src/WebRtc/SimplePeer.ts | 36 ++++-- front/src/WebRtc/VideoPeer.ts | 80 ++++++++++++- 20 files changed, 630 insertions(+), 97 deletions(-) create mode 100644 front/src/Components/Video/LocalStreamMedia.svelte create mode 100644 front/src/Components/Video/Peer.svelte create mode 100644 front/src/Components/Video/ScreenSharingMedia.svelte create mode 100644 front/src/Components/Video/VideoMedia.svelte create mode 100644 front/src/Components/Video/VideoOverlay.svelte create mode 100644 front/src/Components/Video/images/blockSign.svg create mode 100644 front/src/Components/Video/images/report.svg create mode 100644 front/src/Stores/GameOverlayStoreVisibility.ts create mode 100644 front/src/Stores/LayoutStore.ts diff --git a/front/src/Components/App.svelte b/front/src/Components/App.svelte index c973a6c2..1d492ab7 100644 --- a/front/src/Components/App.svelte +++ b/front/src/Components/App.svelte @@ -1,5 +1,5 @@
@@ -66,6 +69,7 @@ --> {#if $gameOverlayVisibilityStore}
+
diff --git a/front/src/Components/EnableCamera/HorizontalSoundMeterWidget.svelte b/front/src/Components/EnableCamera/HorizontalSoundMeterWidget.svelte index a22da2fa..79ad1810 100644 --- a/front/src/Components/EnableCamera/HorizontalSoundMeterWidget.svelte +++ b/front/src/Components/EnableCamera/HorizontalSoundMeterWidget.svelte @@ -58,7 +58,7 @@
- {#each [...Array(NB_BARS).keys()] as i} + {#each [...Array(NB_BARS).keys()] as i (i)}
{/each}
diff --git a/front/src/Components/SoundMeterWidget.svelte b/front/src/Components/SoundMeterWidget.svelte index 30650e3f..94e0cdd2 100644 --- a/front/src/Components/SoundMeterWidget.svelte +++ b/front/src/Components/SoundMeterWidget.svelte @@ -23,7 +23,7 @@ timeout = setInterval(() => { try{ - volume = parseInt((soundMeter.getVolume() / 100 * NB_BARS).toFixed(0)); + volume = soundMeter.getVolume(); //console.log(volume); }catch(err){ @@ -45,9 +45,9 @@
- 1}> - 2}> - 3}> - 4}> 5}> + 10}> + 15}> + 40}> + 70}>
diff --git a/front/src/Components/Video/LocalStreamMedia.svelte b/front/src/Components/Video/LocalStreamMedia.svelte new file mode 100644 index 00000000..43b1d117 --- /dev/null +++ b/front/src/Components/Video/LocalStreamMedia.svelte @@ -0,0 +1,20 @@ + + + +
+ +
diff --git a/front/src/Components/Video/Peer.svelte b/front/src/Components/Video/Peer.svelte new file mode 100644 index 00000000..c73d620e --- /dev/null +++ b/front/src/Components/Video/Peer.svelte @@ -0,0 +1,20 @@ + + +
+ {#if peer instanceof VideoPeer} + + {:else if peer instanceof ScreenSharingPeer} + + {:else} + + {/if} +
diff --git a/front/src/Components/Video/ScreenSharingMedia.svelte b/front/src/Components/Video/ScreenSharingMedia.svelte new file mode 100644 index 00000000..e16fac58 --- /dev/null +++ b/front/src/Components/Video/ScreenSharingMedia.svelte @@ -0,0 +1,57 @@ + + +
+ {#if $statusStore === 'connecting'} +
+ {/if} + {#if $statusStore === 'error'} +
+ {/if} + {#if $streamStore === null} + {name} + {/if} + +
+ + diff --git a/front/src/Components/Video/VideoMedia.svelte b/front/src/Components/Video/VideoMedia.svelte new file mode 100644 index 00000000..fbc5c6f7 --- /dev/null +++ b/front/src/Components/Video/VideoMedia.svelte @@ -0,0 +1,75 @@ + + +
+ {#if $statusStore === 'connecting'} +
+ {/if} + {#if $statusStore === 'error'} +
+ {/if} + {#if !$constraintStore || $constraintStore.video === false} + {name} + {/if} + {#if $constraintStore && $constraintStore.audio === false} + Muted + {/if} + + + + {#if $constraintStore && $constraintStore.audio !== false} + + {/if} +
+ + diff --git a/front/src/Components/Video/VideoOverlay.svelte b/front/src/Components/Video/VideoOverlay.svelte new file mode 100644 index 00000000..5ba7fbc7 --- /dev/null +++ b/front/src/Components/Video/VideoOverlay.svelte @@ -0,0 +1,32 @@ + + +
+
+ {#each [...$layoutStore.get(DivImportance.Important).values()] as peer (peer.uniqueId)} + + {/each} +
+ + + + +
+ + diff --git a/front/src/Components/Video/images/blockSign.svg b/front/src/Components/Video/images/blockSign.svg new file mode 100644 index 00000000..c64ba294 --- /dev/null +++ b/front/src/Components/Video/images/blockSign.svg @@ -0,0 +1,22 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/front/src/Components/Video/images/report.svg b/front/src/Components/Video/images/report.svg new file mode 100644 index 00000000..14753256 --- /dev/null +++ b/front/src/Components/Video/images/report.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index b5876d5a..1bec39fc 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -30,7 +30,7 @@ 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 {SimplePeer} from "../../WebRtc/SimplePeer"; import {ReconnectingSceneName} from "../Reconnecting/ReconnectingScene"; import {lazyLoadPlayerCharacterTextures, loadCustomTexture} from "../Entity/PlayerTexturesLoadingManager"; import { @@ -93,7 +93,7 @@ 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 {peerStore, screenSharingPeerStore} from "../../Stores/PeerStore"; import {EmoteManager} from "./EmoteManager"; export interface GameSceneInitInterface { @@ -646,12 +646,13 @@ export class GameScene extends DirtyScene implements CenterListener { // When connection is performed, let's connect SimplePeer this.simplePeer = new SimplePeer(this.connection, !this.room.isPublic, this.playerName); peerStore.connectToSimplePeer(this.simplePeer); + screenSharingPeerStore.connectToSimplePeer(this.simplePeer); this.GlobalMessageManager = new GlobalMessageManager(this.connection); userMessageManager.setReceiveBanListener(this.bannedUser.bind(this)); const self = this; this.simplePeer.registerPeerConnectionListener({ - onConnect(user: UserSimplePeerInterface) { + onConnect(peer) { self.presentationModeSprite.setVisible(true); self.chatModeSprite.setVisible(true); self.openChatIcon.setVisible(true); diff --git a/front/src/Stores/GameOverlayStoreVisibility.ts b/front/src/Stores/GameOverlayStoreVisibility.ts new file mode 100644 index 00000000..c58c929d --- /dev/null +++ b/front/src/Stores/GameOverlayStoreVisibility.ts @@ -0,0 +1,17 @@ +import {writable} from "svelte/store"; + +/** + * A store that contains whether the game overlay is shown or not. + * Typically, the overlay is hidden when entering Jitsi meet. + */ +function createGameOverlayVisibilityStore() { + const { subscribe, set, update } = writable(false); + + return { + subscribe, + showGameOverlay: () => set(true), + hideGameOverlay: () => set(false), + }; +} + +export const gameOverlayVisibilityStore = createGameOverlayVisibilityStore(); diff --git a/front/src/Stores/LayoutStore.ts b/front/src/Stores/LayoutStore.ts new file mode 100644 index 00000000..45e6f604 --- /dev/null +++ b/front/src/Stores/LayoutStore.ts @@ -0,0 +1,58 @@ +import {derived, get} from "svelte/store"; +import {ScreenSharingLocalMedia, screenSharingLocalMedia} from "./ScreenSharingStore"; +import {DivImportance} from "../WebRtc/LayoutManager"; +import { peerStore, screenSharingStreamStore} from "./PeerStore"; +import type {RemotePeer} from "../WebRtc/SimplePeer"; + +export type DisplayableMedia = RemotePeer | ScreenSharingLocalMedia; + +/** + * A store that contains the layout of the streams + */ +function createLayoutStore() { + + let unsubscribes: (()=>void)[] = []; + + return derived([ + screenSharingStreamStore, + peerStore, + screenSharingLocalMedia, + ], ([ + $screenSharingStreamStore, + $peerStore, + $screenSharingLocalMedia, + ], set) => { + for (const unsubscribe of unsubscribes) { + unsubscribe(); + } + unsubscribes = []; + + const peers = new Map>(); + peers.set(DivImportance.Normal, new Map()); + peers.set(DivImportance.Important, new Map()); + + const addPeer = (peer: DisplayableMedia) => { + const importance = get(peer.importanceStore); + + peers.get(importance)?.set(peer.uniqueId, peer); + + unsubscribes.push(peer.importanceStore.subscribe((importance) => { + peers.forEach((category) => { + category.delete(peer.uniqueId); + }); + peers.get(importance)?.set(peer.uniqueId, peer); + set(peers); + })); + }; + + $screenSharingStreamStore.forEach(addPeer); + $peerStore.forEach(addPeer); + if ($screenSharingLocalMedia?.stream) { + addPeer($screenSharingLocalMedia); + } + + set(peers); + }); +} + +export const layoutStore = createLayoutStore(); diff --git a/front/src/Stores/MediaStore.ts b/front/src/Stores/MediaStore.ts index d622511e..b2cd9f42 100644 --- a/front/src/Stores/MediaStore.ts +++ b/front/src/Stores/MediaStore.ts @@ -1,13 +1,13 @@ import {derived, get, Readable, readable, writable, Writable} from "svelte/store"; import {peerStore} from "./PeerStore"; import {localUserStore} from "../Connexion/LocalUserStore"; -import {ITiledMapGroupLayer, ITiledMapObjectLayer, ITiledMapTileLayer} from "../Phaser/Map/ITiledMap"; import {userMovingStore} from "./GameStore"; import {HtmlUtils} from "../WebRtc/HtmlUtils"; import {BrowserTooOldError} from "./Errors/BrowserTooOldError"; import {errorStore} from "./ErrorStore"; import {isIOS} from "../WebRtc/DeviceUtils"; import {WebviewOnOldIOS} from "./Errors/WebviewOnOldIOS"; +import {gameOverlayVisibilityStore} from "./GameOverlayStoreVisibility"; /** * A store that contains the camera state requested by the user (on or off). @@ -50,20 +50,6 @@ export const visibilityStore = readable(document.visibilityState === 'visible', }; }); -/** - * A store that contains whether the game overlay is shown or not. - * Typically, the overlay is hidden when entering Jitsi meet. - */ -function createGameOverlayVisibilityStore() { - const { subscribe, set, update } = writable(false); - - return { - subscribe, - showGameOverlay: () => set(true), - hideGameOverlay: () => set(false), - }; -} - /** * A store that contains whether the EnableCameraScene is shown or not. */ @@ -79,7 +65,6 @@ function createEnableCameraSceneVisibilityStore() { export const requestedCameraState = createRequestedCameraState(); export const requestedMicrophoneState = createRequestedMicrophoneState(); -export const gameOverlayVisibilityStore = createGameOverlayVisibilityStore(); export const enableCameraSceneVisibilityStore = createEnableCameraSceneVisibilityStore(); /** diff --git a/front/src/Stores/PeerStore.ts b/front/src/Stores/PeerStore.ts index a582e692..df8b17ce 100644 --- a/front/src/Stores/PeerStore.ts +++ b/front/src/Stores/PeerStore.ts @@ -1,26 +1,64 @@ -import { derived, writable, Writable } from "svelte/store"; -import type {UserSimplePeerInterface} from "../WebRtc/SimplePeer"; -import type {SimplePeer} from "../WebRtc/SimplePeer"; +import {derived, get, readable, writable} from "svelte/store"; +import type {RemotePeer, SimplePeer} from "../WebRtc/SimplePeer"; +import {VideoPeer} from "../WebRtc/VideoPeer"; +import {ScreenSharingPeer} from "../WebRtc/ScreenSharingPeer"; /** - * A store that contains the camera state requested by the user (on or off). + * A store that contains the list of (video) peers we are connected to. */ function createPeerStore() { - let users = new Map(); + let peers = new Map(); - const { subscribe, set, update } = writable(users); + const { subscribe, set, update } = writable(peers); return { subscribe, connectToSimplePeer: (simplePeer: SimplePeer) => { - users = new Map(); - set(users); + peers = new Map(); + set(peers); simplePeer.registerPeerConnectionListener({ - onConnect(user: UserSimplePeerInterface) { + onConnect(peer: RemotePeer) { + if (peer instanceof VideoPeer) { + update(users => { + users.set(peer.userId, peer); + return users; + }); + } + console.log('CONNECT VIDEO', peers); + }, + onDisconnect(userId: number) { update(users => { - users.set(user.userId, user); + users.delete(userId); return users; }); + console.log('DISCONNECT VIDEO', peers); + } + }) + } + }; +} + +/** + * A store that contains the list of screen sharing peers we are connected to. + */ +function createScreenSharingPeerStore() { + let peers = new Map(); + + const { subscribe, set, update } = writable(peers); + + return { + subscribe, + connectToSimplePeer: (simplePeer: SimplePeer) => { + peers = new Map(); + set(peers); + simplePeer.registerPeerConnectionListener({ + onConnect(peer: RemotePeer) { + if (peer instanceof ScreenSharingPeer) { + update(users => { + users.set(peer.userId, peer); + return users; + }); + } }, onDisconnect(userId: number) { update(users => { @@ -34,3 +72,56 @@ function createPeerStore() { } export const peerStore = createPeerStore(); +export const screenSharingPeerStore = createScreenSharingPeerStore(); + +/** + * A store that contains ScreenSharingPeer, ONLY if those ScreenSharingPeer are emitting a stream towards us! + */ +function createScreenSharingStreamStore() { + let peers = new Map(); + + return readable>(peers, function start(set) { + + let unsubscribes: (()=>void)[] = []; + + const unsubscribe = screenSharingPeerStore.subscribe((screenSharingPeers) => { + for (const unsubscribe of unsubscribes) { + unsubscribe(); + } + unsubscribes = []; + + peers = new Map(); + + screenSharingPeers.forEach((screenSharingPeer: ScreenSharingPeer, key: number) => { + + if (screenSharingPeer.isReceivingScreenSharingStream()) { + peers.set(key, screenSharingPeer); + } + + unsubscribes.push(screenSharingPeer.streamStore.subscribe((stream) => { + if (stream) { + peers.set(key, screenSharingPeer); + } else { + peers.delete(key); + } + set(peers); + })); + + }); + + set(peers); + + }); + + return function stop() { + unsubscribe(); + for (const unsubscribe of unsubscribes) { + unsubscribe(); + } + }; + }) +} + +export const screenSharingStreamStore = createScreenSharingStreamStore(); + + diff --git a/front/src/Stores/ScreenSharingStore.ts b/front/src/Stores/ScreenSharingStore.ts index ec5aa46f..41d450c2 100644 --- a/front/src/Stores/ScreenSharingStore.ts +++ b/front/src/Stores/ScreenSharingStore.ts @@ -1,16 +1,10 @@ import {derived, get, Readable, readable, writable, Writable} from "svelte/store"; import {peerStore} from "./PeerStore"; -import {localUserStore} from "../Connexion/LocalUserStore"; -import {ITiledMapGroupLayer, ITiledMapObjectLayer, ITiledMapTileLayer} from "../Phaser/Map/ITiledMap"; -import {userMovingStore} from "./GameStore"; -import {HtmlUtils} from "../WebRtc/HtmlUtils"; -import { - audioConstraintStore, cameraEnergySavingStore, - enableCameraSceneVisibilityStore, - gameOverlayVisibilityStore, LocalStreamStoreValue, privacyShutdownStore, - requestedCameraState, - requestedMicrophoneState, videoConstraintStore +import type { + LocalStreamStoreValue, } from "./MediaStore"; +import {DivImportance} from "../WebRtc/LayoutManager"; +import {gameOverlayVisibilityStore} from "./GameOverlayStoreVisibility"; declare const navigator:any; // eslint-disable-line @typescript-eslint/no-explicit-any @@ -191,3 +185,35 @@ export const screenSharingAvailableStore = derived(peerStore, ($peerStore, set) set($peerStore.size !== 0); }); + +export interface ScreenSharingLocalMedia { + uniqueId: string; + importanceStore: Writable; + stream: MediaStream|null; + //subscribe(this: void, run: Subscriber, invalidate?: (value?: ScreenSharingLocalMedia) => void): Unsubscriber; +} + +/** + * The representation of the screen sharing stream. + */ +export const screenSharingLocalMedia = readable(null, function start(set) { + + const localMedia: ScreenSharingLocalMedia = { + uniqueId: "localScreenSharingStream", + importanceStore: writable(DivImportance.Normal), + stream: null + } + + const unsubscribe = screenSharingLocalStreamStore.subscribe((screenSharingLocalStream) => { + if (screenSharingLocalStream.type === "success") { + localMedia.stream = screenSharingLocalStream.stream; + } else { + localMedia.stream = null; + } + set(localMedia); + }); + + return function stop() { + unsubscribe(); + }; +}) diff --git a/front/src/WebRtc/MediaManager.ts b/front/src/WebRtc/MediaManager.ts index efc9660a..a71618e0 100644 --- a/front/src/WebRtc/MediaManager.ts +++ b/front/src/WebRtc/MediaManager.ts @@ -7,7 +7,7 @@ import type {UserSimplePeerInterface} from "./SimplePeer"; import {SoundMeter} from "../Phaser/Components/SoundMeter"; import {DISABLE_NOTIFICATIONS} from "../Enum/EnvironmentVariable"; import { - gameOverlayVisibilityStore, localStreamStore, + localStreamStore, } from "../Stores/MediaStore"; import { screenSharingLocalStreamStore @@ -22,6 +22,7 @@ export type ShowReportCallBack = (userId: string, userName: string|undefined) => export type HelpCameraSettingsCallBack = () => void; import {cowebsiteCloseButtonId} from "./CoWebsiteManager"; +import {gameOverlayVisibilityStore} from "../Stores/GameOverlayStoreVisibility"; export class MediaManager { private remoteVideo: Map = new Map(); @@ -65,7 +66,7 @@ export class MediaManager { } }); - let isScreenSharing = false; + //let isScreenSharing = false; screenSharingLocalStreamStore.subscribe((result) => { if (result.type === 'error') { console.error(result.error); @@ -75,7 +76,7 @@ export class MediaManager { return; } - if (result.stream !== null) { + /*if (result.stream !== null) { isScreenSharing = true; this.addScreenSharingActiveVideo('me', DivImportance.Normal); HtmlUtils.getElementByIdOrFail('screen-sharing-me').srcObject = result.stream; @@ -84,7 +85,7 @@ export class MediaManager { isScreenSharing = false; this.removeActiveScreenSharingVideo('me'); } - } + }*/ }); @@ -134,7 +135,7 @@ export class MediaManager { gameOverlayVisibilityStore.hideGameOverlay(); } - addActiveVideo(user: UserSimplePeerInterface, userName: string = ""){ + /*addActiveVideo(user: UserSimplePeerInterface, userName: string = ""){ const userId = ''+user.userId userName = userName.toUpperCase(); @@ -194,7 +195,7 @@ export class MediaManager { layoutManager.add(divImportance, userId, html); this.remoteVideo.set(userId, HtmlUtils.getElementByIdOrFail(userId)); - } + }*/ private getScreenSharingId(userId: string): string { return `screen-sharing-${userId}`; @@ -242,19 +243,12 @@ export class MediaManager { const blockLogoElement = HtmlUtils.getElementByIdOrFail('blocking-'+userId); show ? blockLogoElement.classList.add('active') : blockLogoElement.classList.remove('active'); } - addStreamRemoteVideo(userId: string, stream : MediaStream): void { + /*addStreamRemoteVideo(userId: string, stream : MediaStream): void { const remoteVideo = this.remoteVideo.get(userId); if (remoteVideo === undefined) { throw `Unable to find video for ${userId}`; } remoteVideo.srcObject = stream; - - //FIX ME SOUNDMETER: check stalability of sound meter calculation - //sound metter - /*const soundMeter = new SoundMeter(); - soundMeter.connectToSource(stream, new AudioContext()); - this.soundMeters.set(userId, soundMeter); - this.soundMeterElements.set(userId, HtmlUtils.getElementByIdOrFail('soundMeter-'+userId));*/ } addStreamRemoteScreenSharing(userId: string, stream : MediaStream){ // In the case of screen sharing (going both ways), we may need to create the HTML element if it does not exist yet @@ -264,23 +258,18 @@ export class MediaManager { } this.addStreamRemoteVideo(this.getScreenSharingId(userId), stream); - } + }*/ removeActiveVideo(userId: string){ - layoutManager.remove(userId); - this.remoteVideo.delete(userId); - - //FIX ME SOUNDMETER: check stalability of sound meter calculation - /*this.soundMeters.get(userId)?.stop(); - this.soundMeters.delete(userId); - this.soundMeterElements.delete(userId);*/ + //layoutManager.remove(userId); + //this.remoteVideo.delete(userId); //permit to remove user in discussion part this.removeParticipant(userId); } - removeActiveScreenSharingVideo(userId: string) { + /*removeActiveScreenSharingVideo(userId: string) { this.removeActiveVideo(this.getScreenSharingId(userId)) - } + }*/ isConnecting(userId: string): void { const connectingSpinnerDiv = this.getSpinner(userId); diff --git a/front/src/WebRtc/ScreenSharingPeer.ts b/front/src/WebRtc/ScreenSharingPeer.ts index d797f59b..49026971 100644 --- a/front/src/WebRtc/ScreenSharingPeer.ts +++ b/front/src/WebRtc/ScreenSharingPeer.ts @@ -1,9 +1,11 @@ import type * as SimplePeerNamespace from "simple-peer"; import {mediaManager} from "./MediaManager"; -import {STUN_SERVER, TURN_SERVER, TURN_USER, TURN_PASSWORD} from "../Enum/EnvironmentVariable"; +import {STUN_SERVER, TURN_PASSWORD, TURN_SERVER, TURN_USER} from "../Enum/EnvironmentVariable"; import type {RoomConnection} from "../Connexion/RoomConnection"; import {MESSAGE_TYPE_CONSTRAINT} from "./VideoPeer"; import type {UserSimplePeerInterface} from "./SimplePeer"; +import {Readable, readable, writable, Writable} from "svelte/store"; +import {DivImportance} from "./LayoutManager"; const Peer: SimplePeerNamespace.SimplePeer = require('simple-peer'); @@ -17,9 +19,13 @@ export class ScreenSharingPeer extends Peer { private isReceivingStream:boolean = false; public toClose: boolean = false; public _connected: boolean = false; - private userId: number; + public readonly userId: number; + public readonly uniqueId: string; + public readonly streamStore: Readable; + public readonly importanceStore: Writable; + public readonly statusStore: Readable<"connecting" | "connected" | "error" | "closed">; - constructor(user: UserSimplePeerInterface, initiator: boolean, private connection: RoomConnection, stream: MediaStream | null) { + constructor(user: UserSimplePeerInterface, initiator: boolean, public readonly userName: string, private connection: RoomConnection, stream: MediaStream | null) { super({ initiator: initiator ? initiator : false, //reconnectTimer: 10000, @@ -38,6 +44,56 @@ export class ScreenSharingPeer extends Peer { }); this.userId = user.userId; + this.uniqueId = 'screensharing_'+this.userId; + + this.streamStore = readable(null, (set) => { + const onStream = (stream: MediaStream|null) => { + set(stream); + }; + const onData = (chunk: Buffer) => { + // We unfortunately need to rely on an event to let the other party know a stream has stopped. + // It seems there is no native way to detect that. + // TODO: we might rely on the "ended" event: https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/ended_event + const message = JSON.parse(chunk.toString('utf8')); + if (message.streamEnded !== true) { + console.error('Unexpected message on screen sharing peer connection'); + return; + } + set(null); + } + + this.on('stream', onStream); + this.on('data', onData); + + return () => { + this.off('stream', onStream); + this.off('data', onData); + }; + }); + + this.importanceStore = writable(DivImportance.Important); + + this.statusStore = readable<"connecting" | "connected" | "error" | "closed">("connecting", (set) => { + const onConnect = () => { + set('connected'); + }; + const onError = () => { + set('error'); + }; + const onClose = () => { + set('closed'); + }; + + this.on('connect', onConnect); + this.on('error', onError); + this.on('close', onClose); + + return () => { + this.off('connect', onConnect); + this.off('error', onError); + this.off('close', onClose); + }; + }); //start listen signal for the peer connection this.on('signal', (data: unknown) => { @@ -54,16 +110,17 @@ export class ScreenSharingPeer extends Peer { this.destroy(); }); - this.on('data', (chunk: Buffer) => { + /*this.on('data', (chunk: Buffer) => { // We unfortunately need to rely on an event to let the other party know a stream has stopped. // It seems there is no native way to detect that. + // TODO: we might rely on the "ended" event: https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/ended_event const message = JSON.parse(chunk.toString('utf8')); if (message.streamEnded !== true) { console.error('Unexpected message on screen sharing peer connection'); return; } mediaManager.removeActiveScreenSharingVideo("" + this.userId); - }); + });*/ // eslint-disable-next-line @typescript-eslint/no-explicit-any this.on('error', (err: any) => { @@ -103,10 +160,10 @@ export class ScreenSharingPeer extends Peer { //console.log(`ScreenSharingPeer::stream => ${this.userId}`, stream); //console.log(`stream => ${this.userId} => `, stream); if(!stream){ - mediaManager.removeActiveScreenSharingVideo("" + this.userId); + //mediaManager.removeActiveScreenSharingVideo("" + this.userId); this.isReceivingStream = false; } else { - mediaManager.addStreamRemoteScreenSharing("" + this.userId, stream); + //mediaManager.addStreamRemoteScreenSharing("" + this.userId, stream); this.isReceivingStream = true; } } @@ -121,7 +178,7 @@ export class ScreenSharingPeer extends Peer { if(!this.toClose){ return; } - mediaManager.removeActiveScreenSharingVideo("" + this.userId); + //mediaManager.removeActiveScreenSharingVideo("" + this.userId); // FIXME: I don't understand why "Closing connection with" message is displayed TWICE before "Nb users in peerConnectionArray" // I do understand the method closeConnection is called twice, but I don't understand how they manage to run in parallel. //console.log('Closing connection with '+userId); diff --git a/front/src/WebRtc/SimplePeer.ts b/front/src/WebRtc/SimplePeer.ts index 2a502bab..7c6264a9 100644 --- a/front/src/WebRtc/SimplePeer.ts +++ b/front/src/WebRtc/SimplePeer.ts @@ -28,8 +28,10 @@ export interface UserSimplePeerInterface{ webRtcPassword?: string|undefined; } +export type RemotePeer = VideoPeer | ScreenSharingPeer; + export interface PeerConnectionListener { - onConnect(user: UserSimplePeerInterface): void; + onConnect(user: RemotePeer): void; onDisconnect(userId: number): void; } @@ -159,20 +161,17 @@ export class SimplePeer { let name = user.name; if (!name) { - const userSearch = this.Users.find((userSearch: UserSimplePeerInterface) => userSearch.userId === user.userId); - if (userSearch) { - name = userSearch.name; - } + name = this.getName(user.userId); } mediaManager.removeActiveVideo("" + user.userId); - mediaManager.addActiveVideo(user, name); + //mediaManager.addActiveVideo(user, name); this.lastWebrtcUserName = user.webRtcUser; this.lastWebrtcPassword = user.webRtcPassword; - const peer = new VideoPeer(user, user.initiator ? user.initiator : false, this.Connection, localStream); + const peer = new VideoPeer(user, user.initiator ? user.initiator : false, name, this.Connection, localStream); //permit to send message mediaManager.addSendMessageCallback(user.userId,(message: string) => { @@ -196,11 +195,20 @@ export class SimplePeer { this.PeerConnectionArray.set(user.userId, peer); for (const peerConnectionListener of this.peerConnectionListeners) { - peerConnectionListener.onConnect(user); + peerConnectionListener.onConnect(peer); } return peer; } + private getName(userId: number): string { + const userSearch = this.Users.find((userSearch: UserSimplePeerInterface) => userSearch.userId === userId); + if (userSearch) { + return userSearch.name || ''; + } else { + return ''; + } + } + /** * create peer connection to bind users */ @@ -222,10 +230,10 @@ export class SimplePeer { } // We should display the screen sharing ONLY if we are not initiator - if (!user.initiator) { +/* if (!user.initiator) { mediaManager.removeActiveScreenSharingVideo("" + user.userId); mediaManager.addScreenSharingActiveVideo("" + user.userId); - } + }*/ // Enrich the user with last known credentials (if they are not set in the user object, which happens when a user triggers the screen sharing) if (user.webRtcUser === undefined) { @@ -233,11 +241,13 @@ export class SimplePeer { user.webRtcPassword = this.lastWebrtcPassword; } - const peer = new ScreenSharingPeer(user, user.initiator ? user.initiator : false, this.Connection, stream); + const name = this.getName(user.userId); + + const peer = new ScreenSharingPeer(user, user.initiator ? user.initiator : false, name, this.Connection, stream); this.PeerScreenSharingConnectionArray.set(user.userId, peer); for (const peerConnectionListener of this.peerConnectionListeners) { - peerConnectionListener.onConnect(user); + peerConnectionListener.onConnect(peer); } return peer; } @@ -288,7 +298,7 @@ export class SimplePeer { */ private closeScreenSharingConnection(userId : number) { try { - mediaManager.removeActiveScreenSharingVideo("" + userId); + //mediaManager.removeActiveScreenSharingVideo("" + userId); const peer = this.PeerScreenSharingConnectionArray.get(userId); if (peer === undefined) { console.warn("closeScreenSharingConnection => Tried to close connection for user "+userId+" but could not find user") diff --git a/front/src/WebRtc/VideoPeer.ts b/front/src/WebRtc/VideoPeer.ts index 5ca8952c..c69bc4cd 100644 --- a/front/src/WebRtc/VideoPeer.ts +++ b/front/src/WebRtc/VideoPeer.ts @@ -5,8 +5,9 @@ import type {RoomConnection} from "../Connexion/RoomConnection"; import {blackListManager} from "./BlackListManager"; import type {Subscription} from "rxjs"; import type {UserSimplePeerInterface} from "./SimplePeer"; -import {get} from "svelte/store"; +import {get, readable, Readable, writable, Writable} from "svelte/store"; import {obtainedMediaConstraintStore} from "../Stores/MediaStore"; +import {DivImportance} from "./LayoutManager"; const Peer: SimplePeerNamespace.SimplePeer = require('simple-peer'); @@ -22,12 +23,16 @@ export class VideoPeer extends Peer { public _connected: boolean = false; private remoteStream!: MediaStream; private blocked: boolean = false; - private userId: number; - private userName: string; + public readonly userId: number; + public readonly uniqueId: string; private onBlockSubscribe: Subscription; private onUnBlockSubscribe: Subscription; + public readonly streamStore: Readable; + public readonly importanceStore: Writable; + public readonly statusStore: Readable<"connecting" | "connected" | "error" | "closed">; + public readonly constraintsStore: Readable; - constructor(public user: UserSimplePeerInterface, initiator: boolean, private connection: RoomConnection, localStream: MediaStream | null) { + constructor(public user: UserSimplePeerInterface, initiator: boolean, public readonly userName: string, private connection: RoomConnection, localStream: MediaStream | null) { super({ initiator: initiator ? initiator : false, //reconnectTimer: 10000, @@ -46,7 +51,70 @@ export class VideoPeer extends Peer { }); this.userId = user.userId; - this.userName = user.name || ''; + this.uniqueId = 'video_'+this.userId; + + this.streamStore = readable(null, (set) => { + const onStream = (stream: MediaStream|null) => { + set(stream); + }; + const onData = (chunk: Buffer) => { + this.on('data', (chunk: Buffer) => { + const message = JSON.parse(chunk.toString('utf8')); + if (message.type === MESSAGE_TYPE_CONSTRAINT) { + if (!message.video) { + set(null); + } + } + }); + } + + this.on('stream', onStream); + this.on('data', onData); + + return () => { + this.off('stream', onStream); + this.off('data', onData); + }; + }); + + this.constraintsStore = readable(null, (set) => { + const onData = (chunk: Buffer) => { + const message = JSON.parse(chunk.toString('utf8')); + if(message.type === MESSAGE_TYPE_CONSTRAINT) { + set(message); + } + } + + this.on('data', onData); + + return () => { + this.off('data', onData); + }; + }); + + this.importanceStore = writable(DivImportance.Normal); + + this.statusStore = readable<"connecting" | "connected" | "error" | "closed">("connecting", (set) => { + const onConnect = () => { + set('connected'); + }; + const onError = () => { + set('error'); + }; + const onClose = () => { + set('closed'); + }; + + this.on('connect', onConnect); + this.on('error', onError); + this.on('close', onClose); + + return () => { + this.off('connect', onConnect); + this.off('error', onError); + this.off('close', onClose); + }; + }); //start listen signal for the peer connection this.on('signal', (data: unknown) => { @@ -152,7 +220,7 @@ export class VideoPeer extends Peer { if (blackListManager.isBlackListed(this.userId) || this.blocked) { this.toggleRemoteStream(false); } - mediaManager.addStreamRemoteVideo("" + this.userId, stream); + //mediaManager.addStreamRemoteVideo("" + this.userId, stream); }catch (err){ console.error(err); } From ac7fa164b6e39052bd8354e8c917418949abbf94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Mon, 14 Jun 2021 17:59:50 +0200 Subject: [PATCH 42/82] Adding importance handling --- .../Components/Video/LocalStreamMedia.svelte | 7 +++-- front/src/Components/Video/Peer.svelte | 2 +- .../Video/ScreenSharingMedia.svelte | 2 +- front/src/Components/Video/VideoMedia.svelte | 5 +++- front/src/Stores/ImportanceStore.ts | 26 +++++++++++++++++++ front/src/Stores/ScreenSharingStore.ts | 5 ++-- front/src/WebRtc/ScreenSharingPeer.ts | 6 +++-- front/src/WebRtc/VideoPeer.ts | 6 +++-- 8 files changed, 48 insertions(+), 11 deletions(-) create mode 100644 front/src/Stores/ImportanceStore.ts diff --git a/front/src/Components/Video/LocalStreamMedia.svelte b/front/src/Components/Video/LocalStreamMedia.svelte index 43b1d117..375612a6 100644 --- a/front/src/Components/Video/LocalStreamMedia.svelte +++ b/front/src/Components/Video/LocalStreamMedia.svelte @@ -1,4 +1,6 @@
- +
diff --git a/front/src/Components/Video/Peer.svelte b/front/src/Components/Video/Peer.svelte index c73d620e..8d5637f6 100644 --- a/front/src/Components/Video/Peer.svelte +++ b/front/src/Components/Video/Peer.svelte @@ -15,6 +15,6 @@ {:else if peer instanceof ScreenSharingPeer} {:else} - + {/if}
diff --git a/front/src/Components/Video/ScreenSharingMedia.svelte b/front/src/Components/Video/ScreenSharingMedia.svelte index e16fac58..0dbc3c16 100644 --- a/front/src/Components/Video/ScreenSharingMedia.svelte +++ b/front/src/Components/Video/ScreenSharingMedia.svelte @@ -45,7 +45,7 @@ {#if $streamStore === null} {name} {/if} - + diff --git a/front/src/Components/Video/VideoOverlay.svelte b/front/src/Components/Video/VideoOverlay.svelte index 5ba7fbc7..88c1cd42 100644 --- a/front/src/Components/Video/VideoOverlay.svelte +++ b/front/src/Components/Video/VideoOverlay.svelte @@ -3,18 +3,23 @@ import {DivImportance} from "../../WebRtc/LayoutManager"; import Peer from "./Peer.svelte"; import {layoutStore} from "../../Stores/LayoutStore"; + import {videoFocusStore} from "../../Stores/VideoFocusStore";
- {#each [...$layoutStore.get(DivImportance.Important).values()] as peer (peer.uniqueId)} - + {#each [...$layoutStore.values()] as peer (peer.uniqueId)} + {#if $videoFocusStore && peer === $videoFocusStore } + + {/if} {/each}