diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..93940b0c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,10 @@ +## Version 1.3.0 - in dev + +### New Features + +* Maps can now contain "group" layers (layers that contain other layers) - #899 #779 (@Lurkars @moufmouf) + +### Updates + + +### Bug Fixes diff --git a/front/src/Phaser/Game/GameMap.ts b/front/src/Phaser/Game/GameMap.ts index 110a29d6..5fe91b62 100644 --- a/front/src/Phaser/Game/GameMap.ts +++ b/front/src/Phaser/Game/GameMap.ts @@ -1,4 +1,5 @@ -import {ITiledMap} from "../Map/ITiledMap"; +import {ITiledMap, ITiledMapLayer} from "../Map/ITiledMap"; +import {LayersIterator} from "../Map/LayersIterator"; export type PropertyChangeCallback = (newValue: string | number | boolean | undefined, oldValue: string | number | boolean | undefined, allProps: Map) => void; @@ -10,8 +11,10 @@ export class GameMap { private key: number|undefined; private lastProperties = new Map(); private callbacks = new Map>(); + public readonly layersIterator: LayersIterator; public constructor(private map: ITiledMap) { + this.layersIterator = new LayersIterator(map); } /** @@ -55,7 +58,7 @@ export class GameMap { private getProperties(key: number): Map { const properties = new Map(); - for (const layer of this.map.layers) { + for (const layer of this.layersIterator) { if (layer.type !== 'tilelayer') { continue; } diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 144890a3..806fc32e 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -18,7 +18,14 @@ import { RESOLUTION, ZOOM_LEVEL } from "../../Enum/EnvironmentVariable"; -import {ITiledMap, ITiledMapLayer, ITiledMapLayerProperty, ITiledMapObject, ITiledTileSet} from "../Map/ITiledMap"; +import { + ITiledMap, + ITiledMapLayer, + ITiledMapLayerProperty, + ITiledMapObject, + ITiledMapTileLayer, + ITiledTileSet +} from "../Map/ITiledMap"; import {AddPlayerInterface} from "./AddPlayerInterface"; import {PlayerAnimationDirections} from "../Player/Animation"; import {PlayerMovement} from "./PlayerMovement"; @@ -77,6 +84,7 @@ import DOMElement = Phaser.GameObjects.DOMElement; import {Subscription} from "rxjs"; import {worldFullMessageStream} from "../../Connexion/WorldFullMessageStream"; import { lazyLoadCompanionResource } from "../Companion/CompanionTexturesLoadingManager"; +import {LayersIterator} from "../Map/LayersIterator"; import {touchScreenManager} from "../../Touch/TouchScreenManager"; import {PinchManager} from "../UserInput/PinchManager"; import {joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey} from "../Components/MobileJoystick"; @@ -386,8 +394,9 @@ export class GameScene extends ResizableScene implements CenterListener { //add layer on map this.Layers = new Array(); + let depth = -2; - for (const layer of this.mapFile.layers) { + for (const layer of this.gameMap.layersIterator) { if (layer.type === 'tilelayer') { this.addLayer(this.Map.createStaticLayer(layer.name, this.Terrains, 0, 0).setDepth(depth)); @@ -405,7 +414,7 @@ export class GameScene extends ResizableScene 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.'); + 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(); @@ -956,13 +965,14 @@ ${escapedMessage} } private initPositionFromLayerName(layerName: string) { - for (const layer of this.mapFile.layers) { - if (layerName === layer.name && layer.type === 'tilelayer' && (layerName === defaultStartLayerName || this.isStartLayer(layer))) { + for (const layer of this.gameMap.layersIterator) { + 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; } } + } private getExitUrl(layer: ITiledMapLayer): string|undefined { @@ -985,7 +995,7 @@ ${escapedMessage} } private getProperty(layer: ITiledMapLayer|ITiledMap, name: string): string|boolean|number|undefined { - const properties: ITiledMapLayerProperty[] = layer.properties; + const properties: ITiledMapLayerProperty[]|undefined = layer.properties; if (!properties) { return undefined; } @@ -997,7 +1007,7 @@ ${escapedMessage} } private getProperties(layer: ITiledMapLayer|ITiledMap, name: string): (string|number|boolean|undefined)[] { - const properties: ITiledMapLayerProperty[] = layer.properties; + const properties: ITiledMapLayerProperty[]|undefined = layer.properties; if (!properties) { return []; } @@ -1011,7 +1021,7 @@ ${escapedMessage} await gameManager.loadMap(room, this.scene); } - private startUser(layer: ITiledMapLayer): PositionInterface { + private startUser(layer: ITiledMapTileLayer): PositionInterface { const tiles = layer.data; if (typeof(tiles) === 'string') { throw new Error('The content of a JSON map must be filled as a JSON array, not as a string'); diff --git a/front/src/Phaser/Map/ITiledMap.ts b/front/src/Phaser/Map/ITiledMap.ts index 39e0a1f5..359b8e52 100644 --- a/front/src/Phaser/Map/ITiledMap.ts +++ b/front/src/Phaser/Map/ITiledMap.ts @@ -14,7 +14,7 @@ export interface ITiledMap { * Map orientation (orthogonal) */ orientation: string; - properties: ITiledMapLayerProperty[]; + properties?: ITiledMapLayerProperty[]; /** * Render order (right-down) @@ -24,6 +24,11 @@ export interface ITiledMap { tilewidth: number; tilesets: ITiledTileSet[]; version: number; + compressionlevel?: number; + infinite?: boolean; + nextlayerid?: number; + tiledversion?: string; + type?: string; } export interface ITiledMapLayerProperty { @@ -38,19 +43,35 @@ export interface ITiledMapLayerProperty { value: boolean }*/ -export interface ITiledMapLayer { +export type ITiledMapLayer = ITiledMapGroupLayer | ITiledMapObjectLayer | ITiledMapTileLayer; + +export interface ITiledMapGroupLayer { + id?: number, + name: string; + opacity: number; + properties?: ITiledMapLayerProperty[]; + + type: "group"; + visible: boolean; + x: number; + y: number; + /** + * Layers for group layer + */ + layers: ITiledMapLayer[]; +} + +export interface ITiledMapTileLayer { + id?: number, data: number[]|string; height: number; name: string; opacity: number; - properties: ITiledMapLayerProperty[]; - encoding: string; + properties?: ITiledMapLayerProperty[]; + encoding?: string; compression?: string; - /** - * Type of layer (tilelayer, objectgroup) - */ - type: string; + type: "tilelayer"; visible: boolean; width: number; x: number; @@ -59,7 +80,28 @@ export interface ITiledMapLayer { /** * Draw order (topdown (default), index) */ - draworder: string; + draworder?: string; +} + +export interface ITiledMapObjectLayer { + id?: number, + height: number; + name: string; + opacity: number; + properties?: ITiledMapLayerProperty[]; + encoding?: string; + compression?: string; + + type: "objectgroup"; + visible: boolean; + width: number; + x: number; + y: number; + + /** + * Draw order (topdown (default), index) + */ + draworder?: string; objects: ITiledMapObject[]; } diff --git a/front/src/Phaser/Map/LayersIterator.ts b/front/src/Phaser/Map/LayersIterator.ts new file mode 100644 index 00000000..501a5f7b --- /dev/null +++ b/front/src/Phaser/Map/LayersIterator.ts @@ -0,0 +1,44 @@ +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/tests/Phaser/Map/LayersIteratorTest.ts b/front/tests/Phaser/Map/LayersIteratorTest.ts new file mode 100644 index 00000000..3b9d0d9b --- /dev/null +++ b/front/tests/Phaser/Map/LayersIteratorTest.ts @@ -0,0 +1,147 @@ +import "jasmine"; +import {Room} from "../../../src/Connexion/Room"; +import {LayersIterator} from "../../../src/Phaser/Map/LayersIterator"; + +describe("Layers iterator", () => { + it("should iterate maps with no group", () => { + const layersIterator = new LayersIterator({ + "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 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 + }) + + const layers = []; + for (const layer of layersIterator) { + 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":[ + { + "id":6, + "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":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 + }, + { + "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 + }], + "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 + }], + "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) { + 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/grouped_map.json b/maps/tests/grouped_map.json new file mode 100644 index 00000000..1e6c3e35 --- /dev/null +++ b/maps/tests/grouped_map.json @@ -0,0 +1,102 @@ +{ "compressionlevel":-1, + "height":10, + "infinite":false, + "layers":[ + { + "id":7, + "layers":[ + { + "data":[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + "height":10, + "id":1, + "name":"floor", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":10, + "id":2, + "name":"start", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }], + "name":"Group 2", + "opacity":1, + "type":"group", + "visible":true, + "x":0, + "y":0 + }, + { + "id":6, + "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, 34, 34, 34, 34, 34, 0, 0, 0, 0, 0, 34, 34, 34, 34, 34, 0, 0, 0, 0, 0, 34, 34, 34, 34, 34, 0, 0, 0, 0, 0, 34, 34, 34, 34, 34, 0, 0, 0, 0, 0, 34, 34, 34, 34, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":10, + "id":5, + "name":"jitsiConf", + "opacity":1, + "properties":[ + { + "name":"jitsiRoom", + "type":"string", + "value":"myRoom" + }], + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }], + "name":"Group 1", + "opacity":1, + "type":"group", + "visible":true, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":3, + "name":"floorLayer", + "objects":[], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":8, + "nextobjectid":1, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"2021.03.23", + "tileheight":32, + "tilesets":[ + { + "columns":11, + "firstgid":1, + "image":"tileset1.png", + "imageheight":352, + "imagewidth":352, + "margin":0, + "name":"tileset1", + "spacing":0, + "tilecount":121, + "tileheight":32, + "tilewidth":32 + }], + "tilewidth":32, + "type":"map", + "version":1.5, + "width":10 +} \ No newline at end of file