From 43aad4ab143242086124e4519612145666589eb4 Mon Sep 17 00:00:00 2001 From: GRL Date: Wed, 12 May 2021 14:30:12 +0200 Subject: [PATCH] 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 @@ }));
- +
+