Merge pull request #899 from thecodingmachine/Lurkars/map-group-layer-support

Map group layer support (2)
This commit is contained in:
David Négrier 2021-04-26 11:26:21 +02:00 committed by GitHub
commit e37ab7d8ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 377 additions and 19 deletions

10
CHANGELOG.md Normal file
View File

@ -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

View File

@ -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<string, string | boolean | number>) => void; export type PropertyChangeCallback = (newValue: string | number | boolean | undefined, oldValue: string | number | boolean | undefined, allProps: Map<string, string | boolean | number>) => void;
@ -10,8 +11,10 @@ export class GameMap {
private key: number|undefined; private key: number|undefined;
private lastProperties = new Map<string, string|boolean|number>(); private lastProperties = new Map<string, string|boolean|number>();
private callbacks = new Map<string, Array<PropertyChangeCallback>>(); private callbacks = new Map<string, Array<PropertyChangeCallback>>();
public readonly layersIterator: LayersIterator;
public constructor(private map: ITiledMap) { public constructor(private map: ITiledMap) {
this.layersIterator = new LayersIterator(map);
} }
/** /**
@ -55,7 +58,7 @@ export class GameMap {
private getProperties(key: number): Map<string, string|boolean|number> { private getProperties(key: number): Map<string, string|boolean|number> {
const properties = new Map<string, string|boolean|number>(); const properties = new Map<string, string|boolean|number>();
for (const layer of this.map.layers) { for (const layer of this.layersIterator) {
if (layer.type !== 'tilelayer') { if (layer.type !== 'tilelayer') {
continue; continue;
} }

View File

@ -18,7 +18,14 @@ import {
RESOLUTION, RESOLUTION,
ZOOM_LEVEL ZOOM_LEVEL
} from "../../Enum/EnvironmentVariable"; } 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 {AddPlayerInterface} from "./AddPlayerInterface";
import {PlayerAnimationDirections} from "../Player/Animation"; import {PlayerAnimationDirections} from "../Player/Animation";
import {PlayerMovement} from "./PlayerMovement"; import {PlayerMovement} from "./PlayerMovement";
@ -77,6 +84,7 @@ import DOMElement = Phaser.GameObjects.DOMElement;
import {Subscription} from "rxjs"; import {Subscription} from "rxjs";
import {worldFullMessageStream} from "../../Connexion/WorldFullMessageStream"; import {worldFullMessageStream} from "../../Connexion/WorldFullMessageStream";
import { lazyLoadCompanionResource } from "../Companion/CompanionTexturesLoadingManager"; import { lazyLoadCompanionResource } from "../Companion/CompanionTexturesLoadingManager";
import {LayersIterator} from "../Map/LayersIterator";
import {touchScreenManager} from "../../Touch/TouchScreenManager"; import {touchScreenManager} from "../../Touch/TouchScreenManager";
import {PinchManager} from "../UserInput/PinchManager"; import {PinchManager} from "../UserInput/PinchManager";
import {joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey} from "../Components/MobileJoystick"; import {joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey} from "../Components/MobileJoystick";
@ -386,8 +394,9 @@ export class GameScene extends ResizableScene implements CenterListener {
//add layer on map //add layer on map
this.Layers = new Array<Phaser.Tilemaps.StaticTilemapLayer>(); this.Layers = new Array<Phaser.Tilemaps.StaticTilemapLayer>();
let depth = -2; let depth = -2;
for (const layer of this.mapFile.layers) { for (const layer of this.gameMap.layersIterator) {
if (layer.type === 'tilelayer') { if (layer.type === 'tilelayer') {
this.addLayer(this.Map.createStaticLayer(layer.name, this.Terrains, 0, 0).setDepth(depth)); 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) { 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(); this.initStartXAndStartY();
@ -956,13 +965,14 @@ ${escapedMessage}
} }
private initPositionFromLayerName(layerName: string) { private initPositionFromLayerName(layerName: string) {
for (const layer of this.mapFile.layers) { for (const layer of this.gameMap.layersIterator) {
if (layerName === layer.name && 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); const startPosition = this.startUser(layer);
this.startX = startPosition.x + this.mapFile.tilewidth/2; this.startX = startPosition.x + this.mapFile.tilewidth/2;
this.startY = startPosition.y + this.mapFile.tileheight/2; this.startY = startPosition.y + this.mapFile.tileheight/2;
} }
} }
} }
private getExitUrl(layer: ITiledMapLayer): string|undefined { private getExitUrl(layer: ITiledMapLayer): string|undefined {
@ -985,7 +995,7 @@ ${escapedMessage}
} }
private getProperty(layer: ITiledMapLayer|ITiledMap, name: string): string|boolean|number|undefined { private getProperty(layer: ITiledMapLayer|ITiledMap, name: string): string|boolean|number|undefined {
const properties: ITiledMapLayerProperty[] = layer.properties; const properties: ITiledMapLayerProperty[]|undefined = layer.properties;
if (!properties) { if (!properties) {
return undefined; return undefined;
} }
@ -997,7 +1007,7 @@ ${escapedMessage}
} }
private getProperties(layer: ITiledMapLayer|ITiledMap, name: string): (string|number|boolean|undefined)[] { private getProperties(layer: ITiledMapLayer|ITiledMap, name: string): (string|number|boolean|undefined)[] {
const properties: ITiledMapLayerProperty[] = layer.properties; const properties: ITiledMapLayerProperty[]|undefined = layer.properties;
if (!properties) { if (!properties) {
return []; return [];
} }
@ -1011,7 +1021,7 @@ ${escapedMessage}
await gameManager.loadMap(room, this.scene); await gameManager.loadMap(room, this.scene);
} }
private startUser(layer: ITiledMapLayer): PositionInterface { private startUser(layer: ITiledMapTileLayer): PositionInterface {
const tiles = layer.data; 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'); throw new Error('The content of a JSON map must be filled as a JSON array, not as a string');

View File

@ -14,7 +14,7 @@ export interface ITiledMap {
* Map orientation (orthogonal) * Map orientation (orthogonal)
*/ */
orientation: string; orientation: string;
properties: ITiledMapLayerProperty[]; properties?: ITiledMapLayerProperty[];
/** /**
* Render order (right-down) * Render order (right-down)
@ -24,6 +24,11 @@ export interface ITiledMap {
tilewidth: number; tilewidth: number;
tilesets: ITiledTileSet[]; tilesets: ITiledTileSet[];
version: number; version: number;
compressionlevel?: number;
infinite?: boolean;
nextlayerid?: number;
tiledversion?: string;
type?: string;
} }
export interface ITiledMapLayerProperty { export interface ITiledMapLayerProperty {
@ -38,19 +43,35 @@ export interface ITiledMapLayerProperty {
value: boolean 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; data: number[]|string;
height: number; height: number;
name: string; name: string;
opacity: number; opacity: number;
properties: ITiledMapLayerProperty[]; properties?: ITiledMapLayerProperty[];
encoding: string; encoding?: string;
compression?: string; compression?: string;
/** type: "tilelayer";
* Type of layer (tilelayer, objectgroup)
*/
type: string;
visible: boolean; visible: boolean;
width: number; width: number;
x: number; x: number;
@ -59,7 +80,28 @@ export interface ITiledMapLayer {
/** /**
* Draw order (topdown (default), index) * 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[]; objects: ITiledMapObject[];
} }

View File

@ -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<ITiledMapLayer> {
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<ITiledMapLayer> {
if (this.pointer < this.layers.length) {
return {
done: false,
value: this.layers[this.pointer++]
}
} else {
return {
done: true,
value: null
}
}
}
[Symbol.iterator](): IterableIterator<ITiledMapLayer> {
return new LayersIterator(this.map);
}
}

View File

@ -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']);
});
});

102
maps/tests/grouped_map.json Normal file
View File

@ -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
}