[TileMap] Collision mask object (#3313)

* The collision mask is read from the tile maps exported by Tiled 1.9+.
* Several collision masks can be used for instance to have platforms and ladders.
* Take a look to the wiki for more details https://wiki.gdevelop.io/gdevelop5/objects/tilemap
This commit is contained in:
D8H
2022-07-13 15:12:12 +02:00
committed by GitHub
parent 9441d3a2d2
commit f871b64011
73 changed files with 8458 additions and 1064 deletions

View File

@@ -1032,6 +1032,7 @@ namespace gdjs {
) {
continue;
}
if (
gdjs.RuntimeObject.collisionTest(
this.owner,

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,150 @@
/// <reference path="helper/TileMapHelper.d.ts" />
namespace gdjs {
export interface RuntimeScene {
tileMapCollisionMaskManager: gdjs.TileMap.TileMapRuntimeManager;
}
export namespace TileMap {
import PIXI = GlobalPIXIModule.PIXI;
const logger = new gdjs.Logger('Tilemap object');
/**
* A holder to share tile maps across the 2 extension objects.
*
* Every instance with the same files path in properties will
* share the same {@link EditableTileMap} and {@link TileTextureCache}.
*
* To use a tile map with collisions, a user can create 4 objects:
* - one for the the rendering
* - one for the solid platforms
* - one for the jumpthrus
* - one for the ladders
*
* To avoid to have 4 copies of the same tile map in memory, this manager
* puts the tile map in cache and avoid unnecessary parsing.
*
* @see {@link TileMapManager}
*/
export class TileMapRuntimeManager {
private _runtimeScene: gdjs.RuntimeScene;
/**
* Delegate that actually manage the caches without anything specific to
* GDJS.
* It allows to factorize code with the IDE.
*/
private _manager: TileMapHelper.TileMapManager;
/**
* @param runtimeScene The scene.
*/
private constructor(runtimeScene: gdjs.RuntimeScene) {
this._runtimeScene = runtimeScene;
this._manager = new TileMapHelper.TileMapManager();
}
/**
* @param runtimeScene Where to set the manager instance.
* @returns The shared manager.
*/
static getManager(
runtimeScene: gdjs.RuntimeScene
): TileMapRuntimeManager {
if (!runtimeScene.tileMapCollisionMaskManager) {
// Create the shared manager if necessary.
runtimeScene.tileMapCollisionMaskManager = new TileMapRuntimeManager(
runtimeScene
);
}
return runtimeScene.tileMapCollisionMaskManager;
}
/**
* @param tileMapJsonResourceName The resource name of the tile map.
* @param tileSetJsonResourceName The resource name of the tile set.
* @param callback A function called when the tile map is parsed.
*/
getOrLoadTileMap(
tileMapJsonResourceName: string,
tileSetJsonResourceName: string,
callback: (tileMap: TileMapHelper.EditableTileMap | null) => void
): void {
this._manager.getOrLoadTileMap(
this._loadTiledMap.bind(this),
tileMapJsonResourceName,
tileSetJsonResourceName,
pako,
callback
);
}
/**
* @param getTexture The method that loads the atlas image file in memory.
* @param atlasImageResourceName The resource name of the atlas image.
* @param tileMapJsonResourceName The resource name of the tile map.
* @param tileSetJsonResourceName The resource name of the tile set.
* @param callback A function called when the tiles textures are split.
*/
getOrLoadTextureCache(
getTexture: (textureName: string) => PIXI.BaseTexture<PIXI.Resource>,
atlasImageResourceName: string,
tileMapJsonResourceName: string,
tileSetJsonResourceName: string,
callback: (textureCache: TileMapHelper.TileTextureCache | null) => void
): void {
this._manager.getOrLoadTextureCache(
this._loadTiledMap.bind(this),
getTexture,
atlasImageResourceName,
tileMapJsonResourceName,
tileSetJsonResourceName,
callback
);
}
/**
* Parse both JSON and set the content of the tile set in the right
* attribute in the tile map to merge both parsed data.
*/
private _loadTiledMap(
tileMapJsonResourceName: string,
tileSetJsonResourceName: string,
callback: (tiledMap: TileMapHelper.TiledMap | null) => void
): void {
this._runtimeScene
.getGame()
.getJsonManager()
.loadJson(tileMapJsonResourceName, (error, tileMapJsonData) => {
if (error) {
logger.error(
'An error happened while loading a Tilemap JSON data:',
error
);
callback(null);
return;
}
const tiledMap = tileMapJsonData as TileMapHelper.TiledMap;
if (tileSetJsonResourceName) {
this._runtimeScene
.getGame()
.getJsonManager()
.loadJson(tileSetJsonResourceName, (error, tileSetJsonData) => {
if (error) {
logger.error(
'An error happened while loading Tileset JSON data:',
error
);
callback(null);
return;
}
const tileSet = tileSetJsonData as TileMapHelper.TiledTileset;
tileSet.firstgid = tiledMap.tilesets[0].firstgid;
tiledMap.tilesets = [tileSet];
callback(tiledMap);
});
} else {
callback(tiledMap);
}
});
}
}
}
}

View File

@@ -0,0 +1,87 @@
namespace gdjs {
export namespace TileMap {
import PIXI = GlobalPIXIModule.PIXI;
/**
* This render is only useful for debugging purposes.
* @see {@link PixiTileMapHelper.updatePixiCollisionMask}, the render used by the GUI.
*/
export class TileMapCollisionMaskRenderer {
_object: gdjs.TileMapCollisionMaskRuntimeObject;
_graphics: PIXI.Graphics;
constructor(
runtimeObject: gdjs.TileMapCollisionMaskRuntimeObject,
runtimeScene: gdjs.RuntimeScene
) {
this._object = runtimeObject;
this._graphics = new PIXI.Graphics();
runtimeScene
.getLayer('')
.getRenderer()
.addRendererObject(this._graphics, runtimeObject.getZOrder());
}
redrawCollisionMask() {
this._graphics.clear();
if (!this._object._debugMode) {
return;
}
this._graphics.lineStyle(
this._object._outlineSize,
this._object._outlineColor,
this._object._outlineOpacity / 255
);
for (const polygon of this._object.getHitBoxes()) {
const vertices = polygon.vertices;
if (vertices.length === 0) continue;
this._graphics.beginFill(
this._object._fillColor,
this._object._fillOpacity / 255
);
this._graphics.moveTo(vertices[0][0], vertices[0][1]);
for (let index = 1; index < vertices.length; index++) {
this._graphics.lineTo(vertices[index][0], vertices[index][1]);
}
this._graphics.closePath();
this._graphics.endFill();
}
}
getRendererObject() {
return this._graphics;
}
setWidth(width: float): void {
const tileMap = this._object._collisionTileMap;
this._graphics.scale.x = width / tileMap.getWidth();
this._graphics.pivot.x = width / 2;
}
setHeight(height: float): void {
const tileMap = this._object._collisionTileMap;
this._graphics.scale.y = height / tileMap.getHeight();
this._graphics.pivot.y = height / 2;
}
getWidth(): float {
const tileMap = this._object._collisionTileMap;
return tileMap.getWidth() * this._graphics.scale.x;
}
getHeight(): float {
const tileMap = this._object._collisionTileMap;
return tileMap.getHeight() * this._graphics.scale.y;
}
getScaleX(): float {
return this._graphics.scale.x;
}
getScaleY(): float {
return this._graphics.scale.y;
}
}
}
}

View File

@@ -0,0 +1,747 @@
/// <reference path="../helper/TileMapHelper.d.ts" />
namespace gdjs {
export namespace TileMap {
/**
* A tile map transformed with an affine transformation.
*
* @see {@link getHitboxesAround} It gives a fast access to hitboxes for collision handling.
*/
export class TransformedCollisionTileMap {
/**
* The model that describes the tile map.
*/
private _source: TileMapHelper.EditableTileMap;
tag: string;
private _layers: Map<integer, TransformedCollisionTileMapLayer>;
// TODO Tiled allows to offset the layers
/**
* The transformation from the time map coordinate (in pixels)
* to the scene coordinate (in pixels).
*/
private _transformation: gdjs.AffineTransformation = new gdjs.AffineTransformation();
/**
* The transformation from the scene coordinate (in pixels)
* to the time map coordinate (in pixels).
*/
private _inverseTransformation: gdjs.AffineTransformation = new gdjs.AffineTransformation();
/**
* This allows tiles to know if their hitboxes must be updated.
* @see {@link TransformedCollisionTile.affineTransformationUpToDateCount}
*/
_transformationUpToDateCount: integer = 1;
/**
* An reusable Point to avoid allocations.
*/
private static readonly workingPoint: FloatPoint = [0, 0];
/**
* @param source The model that describes the tile map.
*/
constructor(source: TileMapHelper.EditableTileMap, tag: string) {
this._source = source;
this.tag = tag;
this._layers = new Map<integer, TransformedCollisionTileMapLayer>();
for (const sourceLayer of source.getLayers()) {
// TODO A visitor could be used to avoid a cast.
if (!(sourceLayer instanceof TileMapHelper.EditableTileMapLayer)) {
// TODO Collision mask for object layers is not handled.
continue;
}
const tileLayer = sourceLayer as TileMapHelper.EditableTileMapLayer;
this._layers.set(
tileLayer.id,
new TransformedCollisionTileMapLayer(this, tileLayer)
);
}
}
/**
* @returns The transformation from the time map coordinate (in pixels)
* to the scene coordinate (in pixels).
*/
getTransformation(): gdjs.AffineTransformation {
return this._transformation;
}
/**
* @param transformation the transformation from the time map coordinate
* (in pixels) to the scene coordinate (in pixels).
*/
setTransformation(transformation: gdjs.AffineTransformation) {
this._transformation = transformation;
const inverseTransformation = this._inverseTransformation;
inverseTransformation.copyFrom(transformation);
inverseTransformation.invert();
this._invalidate();
}
private _invalidate() {
this._transformationUpToDateCount =
(this._transformationUpToDateCount + 1) % Number.MAX_SAFE_INTEGER;
}
/**
* @returns The tile map width in pixels.
*/
getWidth() {
return this._source.getWidth();
}
/**
* @returns The tile map height in pixels.
*/
getHeight() {
return this._source.getHeight();
}
/**
* @returns The tile width in pixels.
*/
getTileHeight() {
return this._source.getTileHeight();
}
/**
* @returns The tile height in pixels.
*/
getTileWidth() {
return this._source.getTileWidth();
}
/**
* @returns The number of tile columns in the map.
*/
getDimensionX() {
return this._source.getDimensionX();
}
/**
* @returns The number of tile rows in the map.
*/
getDimensionY() {
return this._source.getDimensionY();
}
/**
* @param tileId The tile identifier
* @returns The tile definition form the tile set.
*/
getTileDefinition(tileId: integer) {
return this._source.getTileDefinition(tileId);
}
/**
* @param layerId The layer identifier.
* @returns the layer
*/
getLayer(layerId: integer): TransformedCollisionTileMapLayer | undefined {
return this._layers.get(layerId);
}
/**
* @returns All the layers of the tile map.
*/
getLayers(): Iterable<TransformedCollisionTileMapLayer> {
return this._layers.values();
}
/**
* Check if a point is inside a tile with a given tag.
*
* It doesn't use the tile hitboxes.
* It only check the point is inside the tile square.
*
* @param x The X coordinate of the point to check.
* @param y The Y coordinate of the point to check.
* @param tag The tile tag
* @returns true when the point is inside a tile with a given tag.
*/
pointIsInsideTile(x: float, y: float, tag: string): boolean {
const workingPoint: FloatPoint =
TransformedCollisionTileMap.workingPoint;
workingPoint[0] = x;
workingPoint[1] = y;
this._inverseTransformation.transform(workingPoint, workingPoint);
return this._source.pointIsInsideTile(
workingPoint[0],
workingPoint[1],
tag
);
}
/**
* @param tag The tile tag.
* @param left The left border of the area in the scene.
* @param top The top border of the area in the scene.
* @param right The right border of the area in the scene.
* @param bottom The left border of the area in the scene.
* @returns At least all the hitboxes from the given area
* where tiles have the right tag.
*
* @see {@link gdjs.RuntimeObject.getHitboxesAround}
*/
getHitboxesAround(
tag: string,
left: float,
top: float,
right: float,
bottom: float
): Iterable<gdjs.Polygon> {
// Return the hitboxes from the tiles that overlap
// the AABB of the area in the tile map basis.
// Some of these tiles are not event in the given area
// but this is a good trade of between the number of
// useless returned hitboxes and the time to find them.
// Transform the vertices of the area
// from the scene basis to the tile map basis.
const inverseTransformation = this._inverseTransformation;
const workingPoint: FloatPoint =
TransformedCollisionTileMap.workingPoint;
workingPoint[0] = left;
workingPoint[1] = top;
inverseTransformation.transform(workingPoint, workingPoint);
const topLeftX = workingPoint[0];
const topLeftY = workingPoint[1];
workingPoint[0] = right;
workingPoint[1] = top;
inverseTransformation.transform(workingPoint, workingPoint);
const topRightX = workingPoint[0];
const topRightY = workingPoint[1];
workingPoint[0] = right;
workingPoint[1] = bottom;
inverseTransformation.transform(workingPoint, workingPoint);
const bottomRightX = workingPoint[0];
const bottomRightY = workingPoint[1];
workingPoint[0] = left;
workingPoint[1] = bottom;
inverseTransformation.transform(workingPoint, workingPoint);
const bottomLeftX = workingPoint[0];
const bottomLeftY = workingPoint[1];
// Calculate the AABB of the area in the tile map basis.
const xMin = Math.max(
0,
Math.floor(
Math.min(topLeftX, topRightX, bottomRightX, bottomLeftX) /
this._source.getTileWidth()
)
);
const xMax = Math.min(
this.getDimensionX() - 1,
Math.floor(
Math.max(topLeftX, topRightX, bottomRightX, bottomLeftX) /
this._source.getTileWidth()
)
);
const yMin = Math.max(
0,
Math.floor(
Math.min(topLeftY, topRightY, bottomRightY, bottomLeftY) /
this._source.getTileHeight()
)
);
const yMax = Math.min(
this.getDimensionY() - 1,
Math.floor(
Math.max(topLeftY, topRightY, bottomRightY, bottomLeftY) /
this._source.getTileHeight()
)
);
return this.getHitboxes(tag, xMin, yMin, xMax, yMax);
}
/**
* @param tag The tile tag.
* @param xMin The fist column to include.
* @param yMin The fist row to include.
* @param xMax The last column to include.
* @param yMax The last row to include.
* @returns All the hitboxes from the tiles overlapping
* the given area where tiles have the right tag.
*/
getHitboxes(
tag: string,
xMin: integer,
yMin: integer,
xMax: integer,
yMax: integer
): Iterable<gdjs.Polygon> {
return new MapCollisionMaskIterable(this, tag, xMin, yMin, xMax, yMax);
}
/**
* @param tag The tile tag.
* @returns All the hitboxes from the tiles having the right tag.
*/
getAllHitboxes(tag: string): Iterable<gdjs.Polygon> {
return this.getHitboxes(
tag,
0,
0,
this._source.getDimensionX() - 1,
this._source.getDimensionY() - 1
);
}
}
/**
* Iterable over the tile hitboxes of a given area and tag.
*/
class MapCollisionMaskIterable implements Iterable<gdjs.Polygon> {
map: TransformedCollisionTileMap;
tag: string;
xMin: integer;
yMin: integer;
xMax: integer;
yMax: integer;
/**
* Avoid to allocate an empty iterator each time
* the iterable is initialized.
*/
static emptyItr: Iterator<gdjs.Polygon> = {
next: () => ({ value: undefined, done: true }),
};
/**
* @param map The tile map.
* @param tag The tile tag.
* @param xMin The fist column to include.
* @param yMin The fist row to include.
* @param xMax The last column to include.
* @param yMax The last row to include.
*/
constructor(
map: TransformedCollisionTileMap,
tag: string,
xMin: integer,
yMin: integer,
xMax: integer,
yMax: integer
) {
this.map = map;
this.tag = tag;
this.xMin = xMin;
this.yMin = yMin;
this.xMax = xMax;
this.yMax = yMax;
}
[Symbol.iterator]() {
// Flatten the iterable of each layers into one.
let layerItr = this.map.getLayers()[Symbol.iterator]();
let listItr: Iterator<gdjs.Polygon> = MapCollisionMaskIterable.emptyItr;
return {
next: () => {
let listNext = listItr.next();
while (listNext.done) {
const layerNext = layerItr.next();
if (layerNext.done) {
return listNext;
}
listItr = layerNext.value
.getHitboxes(
this.tag,
this.xMin,
this.yMin,
this.xMax,
this.yMax
)
[Symbol.iterator]();
listNext = listItr.next();
}
return listNext;
},
};
}
}
/**
* A tile map layer transformed with an affine transformation.
*/
export class TransformedCollisionTileMapLayer {
/**
* The time map that contains this layer.
*/
readonly tileMap: TransformedCollisionTileMap;
/**
* The model that describes the tile map.
*/
readonly _source: TileMapHelper.EditableTileMapLayer;
private readonly _tiles: TransformedCollisionTile[][];
/**
* @param tileMap The time map that contains this layer.
* @param source The model that describes the tile map.
*/
constructor(
tileMap: TransformedCollisionTileMap,
source: TileMapHelper.EditableTileMapLayer
) {
this.tileMap = tileMap;
this._source = source;
this._tiles = [];
const dimX = this._source.getDimensionX();
const dimY = this._source.getDimensionY();
this._tiles.length = dimY;
for (let y = 0; y < dimY; y++) {
this._tiles[y] = [];
this._tiles[y].length = dimX;
for (let x = 0; x < dimX; x++) {
this._tiles[y][x] = new TransformedCollisionTile(this, x, y);
}
}
}
/**
* @param x The layer column.
* @param y The layer row.
* @return The tile from the tile set.
*/
get(x: integer, y: integer): TransformedCollisionTile | undefined {
const row = this._tiles[y];
return row ? row[x] : undefined;
}
/**
* The number of tile columns in the layer.
*/
getDimensionX() {
return this._tiles.length === 0 ? 0 : this._tiles[0].length;
}
/**
* The number of tile rows in the layer.
*/
getDimensionY() {
return this._tiles.length;
}
/**
* @returns The layer width in pixels.
*/
getWidth() {
return this._source.getWidth();
}
/**
* @returns The layer height in pixels.
*/
getHeight() {
return this._source.getHeight();
}
/**
* @param x The layer column.
* @param y The layer row.
* @returns true if the tile is flipped diagonally.
*/
isFlippedDiagonally(x: integer, y: integer) {
return this._source.isFlippedDiagonally(x, y);
}
/**
* @param x The layer column.
* @param y The layer row.
* @returns true if the tile is flipped vertically.
*/
isFlippedVertically(x: integer, y: integer) {
return this._source.isFlippedVertically(x, y);
}
/**
* @param x The layer column.
* @param y The layer row.
* @returns true if the tile is flipped horizontally.
*/
isFlippedHorizontally(x: integer, y: integer) {
return this._source.isFlippedHorizontally(x, y);
}
/**
* @param tag The tile tag.
* @param xMin The fist column to include.
* @param yMin The fist row to include.
* @param xMax The last column to include.
* @param yMax The last row to include.
* @returns All the hitboxes from the tiles overlapping
* the given area where tiles have the right tag.
*/
getHitboxes(
tag: string,
xMin: integer,
yMin: integer,
xMax: integer,
yMax: integer
): Iterable<gdjs.Polygon> {
return new LayerCollisionMaskIterable(
this,
tag,
xMin,
yMin,
xMax,
yMax
);
}
/**
* @param tag The tile tag.
* @returns All the hitboxes from the tiles having the right tag.
*/
getAllHitboxes(tag: string): Iterable<gdjs.Polygon> {
return this.getHitboxes(
tag,
0,
0,
this.getDimensionX() - 1,
this.getDimensionY() - 1
);
}
}
/**
* Iterable over the tile hitboxes of a given area and tag.
*/
class LayerCollisionMaskIterable implements Iterable<gdjs.Polygon> {
layer: TransformedCollisionTileMapLayer;
tag: string;
xMin: integer;
yMin: integer;
xMax: integer;
yMax: integer;
/**
* Avoid to allocate an empty iterator each time
* the iterable is initialized.
*/
static emptyItr: Iterator<gdjs.Polygon> = {
next: () => ({ value: undefined, done: true }),
};
/**
* @param map The tile map.
* @param tag The tile tag.
* @param xMin The fist column to include.
* @param yMin The fist row to include.
* @param xMax The last column to include.
* @param yMax The last row to include.
*/
constructor(
layer: TransformedCollisionTileMapLayer,
tag: string,
xMin: integer,
yMin: integer,
xMax: integer,
yMax: integer
) {
this.layer = layer;
this.tag = tag;
this.xMin = xMin;
this.yMin = yMin;
this.xMax = xMax;
this.yMax = yMax;
}
[Symbol.iterator]() {
// Flatten the iterable of each tile into one.
// xMin and yMin next increment
let x = this.xMax;
let y = this.yMin - 1;
let polygonItr: Iterator<gdjs.Polygon> =
LayerCollisionMaskIterable.emptyItr;
return {
next: () => {
let listNext = polygonItr.next();
while (listNext.done) {
x++;
if (x > this.xMax) {
y++;
x = this.xMin;
}
if (y > this.yMax) {
// done
return listNext;
}
const tile = this.layer.get(x, y);
if (!tile) {
continue;
}
const definition = tile.getDefinition();
if (!definition) {
continue;
}
if (definition.hasTag(this.tag)) {
polygonItr = tile.getHitboxes()[Symbol.iterator]();
listNext = polygonItr.next();
}
}
return listNext;
},
};
}
}
/**
* A tile transformed with an affine transformation.
*/
class TransformedCollisionTile {
/**
* The layer that contains this tile.
*/
readonly layer: TransformedCollisionTileMapLayer;
/**
* The column index in the layer.
*/
readonly x: integer;
/**
* The row index in the layer.
*/
readonly y: integer;
private readonly hitBoxes: gdjs.Polygon[];
private affineTransformationUpToDateCount: integer = 0;
/**
* An reusable AffineTransformation to avoid allocations.
*/
private static readonly workingTransformation: gdjs.AffineTransformation = new gdjs.AffineTransformation();
/**
*
* @param layer The layer that contains this tile.
* @param x The column index in the layer.
* @param y The row index in the layer.
*/
constructor(
layer: TransformedCollisionTileMapLayer,
x: integer,
y: integer
) {
this.layer = layer;
this.x = x;
this.y = y;
const definition = this.getDefinition();
this.hitBoxes = [];
if (definition) {
const tag = this.layer.tileMap.tag;
const definitionHitboxes = definition.getHitBoxes(tag);
if (definitionHitboxes) {
this.hitBoxes.length = definitionHitboxes.length;
for (
let polygonIndex = 0;
polygonIndex < this.hitBoxes.length;
polygonIndex++
) {
const polygon = new gdjs.Polygon();
this.hitBoxes[polygonIndex] = polygon;
polygon.vertices.length = definitionHitboxes[polygonIndex].length;
for (
let vertexIndex = 0;
vertexIndex < polygon.vertices.length;
vertexIndex++
) {
polygon.vertices[vertexIndex] = [0, 0];
}
}
}
}
}
/**
* @returns The tile definition from the tile set.
*/
getDefinition(): TileMapHelper.TileDefinition {
return this.layer.tileMap.getTileDefinition(
this.layer._source.get(this.x, this.y)!
)!;
}
private _isHitboxesUpToDate() {
return (
this.affineTransformationUpToDateCount ===
this.layer.tileMap._transformationUpToDateCount
);
}
private _setHitboxesUpToDate() {
this.affineTransformationUpToDateCount = this.layer.tileMap._transformationUpToDateCount;
}
/**
* @returns The hitboxes of this tile in the scene basis.
*/
getHitboxes(): Polygon[] {
if (this._isHitboxesUpToDate()) {
return this.hitBoxes;
}
const definition = this.getDefinition();
if (!definition) {
this._setHitboxesUpToDate();
// It should already be []
this.hitBoxes.length = 0;
return this.hitBoxes;
}
const tag = this.layer.tileMap.tag;
const definitionHitboxes = definition.getHitBoxes(tag);
if (!definitionHitboxes) {
this._setHitboxesUpToDate();
// It should already be []
this.hitBoxes.length = 0;
return this.hitBoxes;
}
const layerTransformation = this.layer.tileMap.getTransformation();
const width = this.layer.tileMap.getTileWidth();
const height = this.layer.tileMap.getTileHeight();
const tileTransformation =
TransformedCollisionTile.workingTransformation;
tileTransformation.setToTranslation(width * this.x, height * this.y);
if (this.layer.isFlippedHorizontally(this.x, this.y)) {
tileTransformation.flipX(width / 2);
}
if (this.layer.isFlippedVertically(this.x, this.y)) {
tileTransformation.flipY(height / 2);
}
if (this.layer.isFlippedDiagonally(this.x, this.y)) {
tileTransformation.flipDiagonally();
}
tileTransformation.preConcatenate(layerTransformation);
// The tile map can't change at runtime so the existing arrays can be
// reused safely.
for (
let polygonIndex = 0;
polygonIndex < this.hitBoxes.length;
polygonIndex++
) {
const defPolygon = definitionHitboxes[polygonIndex];
const polygon = this.hitBoxes[polygonIndex];
for (
let vertexIndex = 0;
vertexIndex < polygon.vertices.length;
vertexIndex++
) {
const defVertex = defPolygon[vertexIndex];
const vertex = polygon.vertices[vertexIndex];
tileTransformation.transform(defVertex, vertex);
}
}
this._setHitboxesUpToDate();
return this.hitBoxes;
}
}
}
}

View File

@@ -0,0 +1 @@
This library sources are located in [SharedLibs/TileMapHelper/](../../../SharedLibs/TileMapHelper/).

View File

@@ -0,0 +1,23 @@
import {
EditableTileMap,
EditableTileMapLayer,
TileDefinition,
TiledMap,
TiledTileset,
TileMapManager,
TileTextureCache,
PixiTileMapHelper,
} from './dts/index';
declare global {
namespace TileMapHelper {
export { EditableTileMap };
export { EditableTileMapLayer };
export { TileDefinition };
export { TiledMap };
export { TiledTileset };
export { TileMapManager };
export { TileTextureCache };
export { PixiTileMapHelper };
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,23 @@
/**
* @packageDocumentation
* @module TileMapHelper
*/
import { TiledMap, TiledTileset } from './tiled/TiledFormat';
import {
EditableTileMap,
EditableTileMapLayer,
TileDefinition,
} from './model/TileMapModel';
import { TileMapManager } from './render/TileMapManager';
import { TileTextureCache } from './render/TileTextureCache';
import { PixiTileMapHelper } from './render/TileMapPixiHelper';
export * from './model/CommonTypes';
export { EditableTileMap };
export { EditableTileMapLayer };
export { TileDefinition };
export { TiledMap };
export { TiledTileset };
export { TileMapManager };
export { TileTextureCache };
export { PixiTileMapHelper };
//# sourceMappingURL=index.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,EACL,eAAe,EACf,oBAAoB,EACpB,cAAc,EACf,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACzD,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAC7D,OAAO,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAE/D,cAAc,qBAAqB,CAAC;AAEpC,OAAO,EAAE,eAAe,EAAE,CAAC;AAC3B,OAAO,EAAE,oBAAoB,EAAE,CAAC;AAChC,OAAO,EAAE,cAAc,EAAE,CAAC;AAE1B,OAAO,EAAE,QAAQ,EAAE,CAAC;AACpB,OAAO,EAAE,YAAY,EAAE,CAAC;AAExB,OAAO,EAAE,cAAc,EAAE,CAAC;AAC1B,OAAO,EAAE,gBAAgB,EAAE,CAAC;AAC5B,OAAO,EAAE,iBAAiB,EAAE,CAAC"}

View File

@@ -0,0 +1,5 @@
export declare type integer = number;
export declare type float = number;
export declare type FloatPoint = [float, float];
export declare type PolygonVertices = FloatPoint[];
//# sourceMappingURL=CommonTypes.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"CommonTypes.d.ts","sourceRoot":"","sources":["../../src/model/CommonTypes.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,OAAO,MAAM,OAAO,GAAG,MAAM,CAAC;AACrC,MAAM,CAAC,OAAO,MAAM,KAAK,GAAG,MAAM,CAAC;AACnC,oBAAY,UAAU,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;AAExC,oBAAY,eAAe,GAAG,UAAU,EAAE,CAAC"}

View File

@@ -0,0 +1,315 @@
import { PolygonVertices, integer, float } from './CommonTypes';
/**
* A tile map model.
*
* Tile map files are parsed into this model by {@link TiledTileMapLoader}.
* This model is used for rending ({@link TileMapRuntimeObjectPixiRenderer})
* and hitboxes handling ({@link TransformedCollisionTileMap}).
* This allows to support new file format with only a new parser.
*/
export declare class EditableTileMap {
private _tileSet;
private _layers;
/**
* The width of a tile.
*/
private readonly tileWidth;
/**
* The height of a tile.
*/
private readonly tileHeight;
/**
* The number of tile columns in the map.
*/
private readonly dimX;
/**
* The number of tile rows in the map.
*/
private readonly dimY;
/**
* @param tileWidth The width of a tile.
* @param tileHeight The height of a tile.
* @param dimX The number of tile columns in the map.
* @param dimY The number of tile rows in the map.
* @param tileSet The tile set.
*/
constructor(
tileWidth: integer,
tileHeight: integer,
dimX: integer,
dimY: integer,
tileSet: Map<integer, TileDefinition>
);
/**
* @returns The tile map width in pixels.
*/
getWidth(): integer;
/**
* @returns The tile map height in pixels.
*/
getHeight(): integer;
/**
* @returns The tile width in pixels.
*/
getTileHeight(): integer;
/**
* @returns The tile height in pixels.
*/
getTileWidth(): integer;
/**
* @returns The number of tile columns in the map.
*/
getDimensionX(): integer;
/**
* @returns The number of tile rows in the map.
*/
getDimensionY(): integer;
/**
* @param tileId The tile identifier
* @returns The tile definition form the tile set.
*/
getTileDefinition(tileId: integer): TileDefinition | undefined;
/**
* @returns All the tile definitions form the tile set.
*/
getTileDefinitions(): Iterable<TileDefinition>;
/**
* @param id The identifier of the new layer.
* @returns The new layer.
*/
addTileLayer(id: integer): EditableTileMapLayer;
/**
* @param id The identifier of the new layer.
* @returns The new layer.
*/
addObjectLayer(id: integer): EditableObjectLayer;
/**
* @returns All the layers of the tile map.
*/
getLayers(): Iterable<AbstractEditableLayer>;
/**
* Check if a point is inside a tile with a given tag.
*
* It doesn't use the tile hitboxes.
* It only check the point is inside the tile square.
*
* @param x The X coordinate of the point to check.
* @param y The Y coordinate of the point to check.
* @param tag The tile tag
* @returns true when the point is inside a tile with a given tag.
*/
pointIsInsideTile(x: float, y: float, tag: string): boolean;
}
/**
* A tile map layer.
*/
declare abstract class AbstractEditableLayer {
/**
* The layer tile map.
*/
readonly tileMap: EditableTileMap;
/**
* The layer identifier.
*/
readonly id: integer;
private visible;
/**
* @param tileMap The layer tile map.
* @param id The layer identifier.
*/
constructor(tileMap: EditableTileMap, id: integer);
setVisible(visible: boolean): void;
/**
* @returns true if the layer is visible.
*/
isVisible(): boolean;
}
/**
* A layer where tiles are placed with pixel coordinates.
*/
export declare class EditableObjectLayer extends AbstractEditableLayer {
readonly objects: TileObject[];
/**
* @param tileMap The layer tile map.
* @param id The layer identifier.
*/
constructor(tileMap: EditableTileMap, id: integer);
add(object: TileObject): void;
}
/**
* A tile that is placed with pixel coordinates.
*/
export declare class TileObject {
/**
* The tile identifier in the tile set.
*/
private tileId;
/**
* The coordinate of the tile left side.
*/
readonly x: float;
/**
* The coordinate of the tile top side.
*/
readonly y: float;
/**
* @param x The coordinate of the tile left side.
* @param y The coordinate of the tile top side.
* @param tileId The tile identifier in the tile set.
*/
constructor(x: float, y: float, tileId: integer);
/**
* @return The tile identifier in the tile set.
*/
getTileId(): integer;
setFlippedHorizontally(flippedHorizontally: boolean): void;
setFlippedVertically(flippedVertically: boolean): void;
setFlippedDiagonally(flippedDiagonally: boolean): void;
/**
* @returns true if the tile is flipped horizontally.
*/
isFlippedHorizontally(): boolean;
/**
* @returns true if the tile is flipped vertically.
*/
isFlippedVertically(): boolean;
/**
* @returns true if the tile is flipped diagonally.
*/
isFlippedDiagonally(): boolean;
}
/**
* A tile map layer with tile organized in grid.
*/
export declare class EditableTileMapLayer extends AbstractEditableLayer {
private readonly _tiles;
/**
* @param tileMap The layer tile map.
* @param id The layer identifier.
*/
constructor(tileMap: EditableTileMap, id: integer);
/**
* @param x The layer column.
* @param y The layer row.
* @param tileId The tile identifier in the tile set.
*/
setTile(x: integer, y: integer, tileId: integer): void;
/**
* @param x The layer column.
* @param y The layer row.
*/
removeTile(x: integer, y: integer): void;
/**
* @param x The layer column.
* @param y The layer row.
* @param flippedHorizontally true if the tile is flipped horizontally.
*/
setFlippedHorizontally(
x: integer,
y: integer,
flippedHorizontally: boolean
): void;
/**
* @param x The layer column.
* @param y The layer row.
* @param flippedVertically true if the tile is flipped vertically.
*/
setFlippedVertically(
x: integer,
y: integer,
flippedVertically: boolean
): void;
/**
* @param x The layer column.
* @param y The layer row.
* @param flippedDiagonally true if the tile is flipped diagonally.
*/
setFlippedDiagonally(
x: integer,
y: integer,
flippedDiagonally: boolean
): void;
/**
* @param x The layer column.
* @param y The layer row.
* @returns true if the tile is flipped horizontally.
*/
isFlippedHorizontally(x: integer, y: integer): boolean;
/**
* @param x The layer column.
* @param y The layer row.
* @returns true if the tile is flipped vertically.
*/
isFlippedVertically(x: integer, y: integer): boolean;
/**
* @param x The layer column.
* @param y The layer row.
* @returns true if the tile is flipped diagonally.
*/
isFlippedDiagonally(x: integer, y: integer): boolean;
/**
* @param x The layer column.
* @param y The layer row.
* @returns The tile identifier from the tile set.
*/
get(x: integer, y: integer): integer | undefined;
/**
* The number of tile columns in the layer.
*/
getDimensionX(): integer;
/**
* The number of tile rows in the layer.
*/
getDimensionY(): integer;
/**
* @returns The layer width in pixels.
*/
getWidth(): integer;
/**
* @returns The layer height in pixels.
*/
getHeight(): integer;
}
/**
* A tile definition from the tile set.
*/
export declare class TileDefinition {
/**
* There will probably be at most 4 tags on a tile.
* An array lookup should take less time than using a Map.
*/
private readonly taggedHitBoxes;
private readonly animationLength;
/**
* @param animationLength The number of frame in the tile animation.
*/
constructor(animationLength: integer);
/**
* Add a polygon for the collision layer
* @param tag The tag to allow collision layer filtering.
* @param polygon The polygon to use for collisions.
*/
add(tag: string, polygon: PolygonVertices): void;
/**
* This property is used by {@link TransformedCollisionTileMap}
* to make collision classes.
* @param tag The tag to allow collision layer filtering.
* @returns true if this tile contains any polygon with the given tag.
*/
hasTag(tag: string): boolean;
/**
* The hitboxes positioning is done by {@link TransformedCollisionTileMap}.
* @param tag The tag to allow collision layer filtering.
* @returns The hit boxes for this tile.
*/
getHitBoxes(tag: string): PolygonVertices[] | undefined;
/**
* Animated tiles have a limitation:
* they are only able to use frames arranged horizontally one next
* to each other on the atlas.
* @returns The number of frame in the tile animation.
*/
getAnimationLength(): integer;
}
export {};
//# sourceMappingURL=TileMapModel.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"TileMapModel.d.ts","sourceRoot":"","sources":["../../src/model/TileMapModel.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAEhE;;;;;;;GAOG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAA+B;IAC/C,OAAO,CAAC,OAAO,CAA+B;IAC9C;;OAEG;IACH,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAU;IACpC;;OAEG;IACH,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAU;IACrC;;OAEG;IACH,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAU;IAC/B;;OAEG;IACH,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAU;IAE/B;;;;;;OAMG;gBAED,SAAS,EAAE,OAAO,EAClB,UAAU,EAAE,OAAO,EACnB,IAAI,EAAE,OAAO,EACb,IAAI,EAAE,OAAO,EAGb,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,cAAc,CAAC;IAUvC;;OAEG;IACH,QAAQ,IAAI,OAAO;IAInB;;OAEG;IACH,SAAS,IAAI,OAAO;IAIpB;;OAEG;IACH,aAAa,IAAI,OAAO;IAIxB;;OAEG;IACH,YAAY,IAAI,OAAO;IAIvB;;OAEG;IACH,aAAa,IAAI,OAAO;IAIxB;;OAEG;IACH,aAAa,IAAI,OAAO;IAIxB;;;OAGG;IACH,iBAAiB,CAAC,MAAM,EAAE,OAAO,GAAG,cAAc,GAAG,SAAS;IAI9D;;OAEG;IACH,kBAAkB,IAAI,QAAQ,CAAC,cAAc,CAAC;IAI9C;;;OAGG;IACH,YAAY,CAAC,EAAE,EAAE,OAAO,GAAG,oBAAoB;IAM/C;;;OAGG;IACH,cAAc,CAAC,EAAE,EAAE,OAAO,GAAG,mBAAmB;IAMhD;;OAEG;IACH,SAAS,IAAI,QAAQ,CAAC,qBAAqB,CAAC;IAI5C;;;;;;;;;;OAUG;IACH,iBAAiB,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO;CAmB5D;AAED;;GAEG;AACH,uBAAe,qBAAqB;IAClC;;OAEG;IACH,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;IAClC;;OAEG;IACH,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,OAAO,CAAC,OAAO,CAAiB;IAEhC;;;OAGG;gBACS,OAAO,EAAE,eAAe,EAAE,EAAE,EAAE,OAAO;IAKjD,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAIlC;;OAEG;IACH,SAAS,IAAI,OAAO;CAGrB;AAED;;GAEG;AACH,qBAAa,mBAAoB,SAAQ,qBAAqB;IAC5D,QAAQ,CAAC,OAAO,EAAE,UAAU,EAAE,CAAC;IAE/B;;;OAGG;gBACS,OAAO,EAAE,eAAe,EAAE,EAAE,EAAE,OAAO;IAKjD,GAAG,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI;CAG9B;AAED;;GAEG;AACH,qBAAa,UAAU;IACrB;;OAEG;IACH,OAAO,CAAC,MAAM,CAAU;IACxB;;OAEG;IACH,QAAQ,CAAC,CAAC,EAAE,KAAK,CAAC;IAClB;;OAEG;IACH,QAAQ,CAAC,CAAC,EAAE,KAAK,CAAC;IAElB;;;;OAIG;gBACS,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO;IAM/C;;OAEG;IACH,SAAS,IAAI,OAAO;IAIpB,sBAAsB,CAAC,mBAAmB,EAAE,OAAO,GAAG,IAAI;IAO1D,oBAAoB,CAAC,iBAAiB,EAAE,OAAO,GAAG,IAAI;IAOtD,oBAAoB,CAAC,iBAAiB,EAAE,OAAO,GAAG,IAAI;IAOtD;;OAEG;IACH,qBAAqB,IAAI,OAAO;IAIhC;;OAEG;IACH,mBAAmB,IAAI,OAAO;IAI9B;;OAEG;IACH,mBAAmB,IAAI,OAAO;CAG/B;AAiED;;GAEG;AACH,qBAAa,oBAAqB,SAAQ,qBAAqB;IAC7D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAoB;IAE3C;;;OAGG;gBACS,OAAO,EAAE,eAAe,EAAE,EAAE,EAAE,OAAO;IASjD;;;;OAIG;IACH,OAAO,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,GAAG,IAAI;IAUtD;;;OAGG;IACH,UAAU,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,GAAG,IAAI;IAKxC;;;;OAIG;IACH,sBAAsB,CACpB,CAAC,EAAE,OAAO,EACV,CAAC,EAAE,OAAO,EACV,mBAAmB,EAAE,OAAO,GAC3B,IAAI;IAWP;;;;OAIG;IACH,oBAAoB,CAClB,CAAC,EAAE,OAAO,EACV,CAAC,EAAE,OAAO,EACV,iBAAiB,EAAE,OAAO,GACzB,IAAI;IAWP;;;;OAIG;IACH,oBAAoB,CAClB,CAAC,EAAE,OAAO,EACV,CAAC,EAAE,OAAO,EACV,iBAAiB,EAAE,OAAO,GACzB,IAAI;IAWP;;;;OAIG;IACH,qBAAqB,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,GAAG,OAAO;IAItD;;;;OAIG;IACH,mBAAmB,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,GAAG,OAAO;IAIpD;;;;OAIG;IACH,mBAAmB,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,GAAG,OAAO;IAIpD;;;;OAIG;IACH,GAAG,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,GAAG,OAAO,GAAG,SAAS;IAUhD;;OAEG;IACH,aAAa,IAAI,OAAO;IAIxB;;OAEG;IACH,aAAa,IAAI,OAAO;IAIxB;;OAEG;IACH,QAAQ,IAAI,OAAO;IAInB;;OAEG;IACH,SAAS,IAAI,OAAO;CAGrB;AAED;;GAEG;AACH,qBAAa,cAAc;IACzB;;;OAGG;IACH,OAAO,CAAC,QAAQ,CAAC,cAAc,CAG3B;IACJ,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAU;IAE1C;;OAEG;gBACS,eAAe,EAAE,OAAO;IAKpC;;;;OAIG;IACH,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,GAAG,IAAI;IAShD;;;;;OAKG;IACH,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAI5B;;;;OAIG;IACH,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,EAAE,GAAG,SAAS;IAOvD;;;;;OAKG;IACH,kBAAkB,IAAI,OAAO;CAG9B"}

View File

@@ -0,0 +1,27 @@
/**
* A cache of resources identified by a string.
*
* It ensures that a resource is never load twice.
*/
export declare class ResourceCache<T> {
private _cachedValues;
/**
* Several calls can happen before the resource is loaded.
* This allows to stack them.
*/
private _callbacks;
constructor();
/**
* Return a resource through a call back.
* @param key the resource identifier.
* @param load load the resource in case of cache default.
* Note that the load callback is used by `getOrLoad` and not by the caller.
* @param callback called when the resource is ready.
*/
getOrLoad(
key: string,
load: (callback: (value: T | null) => void) => void,
callback: (value: T | null) => void
): void;
}
//# sourceMappingURL=ResourceCache.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ResourceCache.d.ts","sourceRoot":"","sources":["../../src/render/ResourceCache.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,qBAAa,aAAa,CAAC,CAAC;IAC1B,OAAO,CAAC,aAAa,CAAiB;IAEtC;;;OAGG;IACH,OAAO,CAAC,UAAU,CAAgD;;IAOlE;;;;;;OAMG;IACH,SAAS,CACP,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,CAAC,GAAG,IAAI,KAAK,IAAI,KAAK,IAAI,EACnD,QAAQ,EAAE,CAAC,KAAK,EAAE,CAAC,GAAG,IAAI,KAAK,IAAI,GAClC,IAAI;CA+BR"}

View File

@@ -0,0 +1,61 @@
import { TiledMap } from '../tiled/TiledFormat';
import { EditableTileMap } from '../model/TileMapModel';
import { TileTextureCache } from './TileTextureCache';
import PIXI = GlobalPIXIModule.PIXI;
/**
* A holder to share tile maps across the 2 extension objects.
*
* Every instance with the same files path in properties will
* share the same {@link EditableTileMap} and {@link TileTextureCache}.
*
* @see {@link TileMapRuntimeManager}
*/
export declare class TileMapManager {
private _tileMapCache;
private _textureCacheCaches;
constructor();
/**
* @param instanceHolder Where to set the manager instance.
* @returns The shared manager.
*/
static getManager(instanceHolder: Object): TileMapManager;
/**
* @param loadTiledMap The method that loads the Tiled JSON file in memory.
* @param tileMapJsonResourceName The resource name of the tile map.
* @param tileSetJsonResourceName The resource name of the tile set.
* @param pako The zlib library.
* @param callback A function called when the tile map is parsed.
*/
getOrLoadTileMap(
loadTiledMap: (
tileMapJsonResourceName: string,
tileSetJsonResourceName: string,
callback: (tiledMap: TiledMap | null) => void
) => void,
tileMapJsonResourceName: string,
tileSetJsonResourceName: string,
pako: any,
callback: (tileMap: EditableTileMap | null) => void
): void;
/**
* @param loadTiledMap The method that loads the Tiled JSON file in memory.
* @param getTexture The method that loads the atlas image file in memory.
* @param atlasImageResourceName The resource name of the atlas image.
* @param tileMapJsonResourceName The resource name of the tile map.
* @param tileSetJsonResourceName The resource name of the tile set.
* @param callback A function called when the tiles textures are split.
*/
getOrLoadTextureCache(
loadTiledMap: (
tileMapJsonResourceName: string,
tileSetJsonResourceName: string,
callback: (tiledMap: TiledMap | null) => void
) => void,
getTexture: (textureName: string) => PIXI.BaseTexture<PIXI.Resource>,
atlasImageResourceName: string,
tileMapJsonResourceName: string,
tileSetJsonResourceName: string,
callback: (textureCache: TileTextureCache | null) => void
): void;
}
//# sourceMappingURL=TileMapManager.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"TileMapManager.d.ts","sourceRoot":"","sources":["../../src/render/TileMapManager.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAEhD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAGtD,OAAO,IAAI,GAAG,gBAAgB,CAAC,IAAI,CAAC;AAEpC;;;;;;;GAOG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,aAAa,CAAiC;IACtD,OAAO,CAAC,mBAAmB,CAAkC;;IAO7D;;;OAGG;IACH,MAAM,CAAC,UAAU,CAAC,cAAc,EAAE,MAAM,GAAG,cAAc;IAWzD;;;;;;OAMG;IACH,gBAAgB,CACd,YAAY,EAAE,CACZ,uBAAuB,EAAE,MAAM,EAC/B,uBAAuB,EAAE,MAAM,EAC/B,QAAQ,EAAE,CAAC,QAAQ,EAAE,QAAQ,GAAG,IAAI,KAAK,IAAI,KAC1C,IAAI,EACT,uBAAuB,EAAE,MAAM,EAC/B,uBAAuB,EAAE,MAAM,EAC/B,IAAI,EAAE,GAAG,EACT,QAAQ,EAAE,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,KAAK,IAAI,GAClD,IAAI;IAwBP;;;;;;;OAOG;IACH,qBAAqB,CACnB,YAAY,EAAE,CACZ,uBAAuB,EAAE,MAAM,EAC/B,uBAAuB,EAAE,MAAM,EAC/B,QAAQ,EAAE,CAAC,QAAQ,EAAE,QAAQ,GAAG,IAAI,KAAK,IAAI,KAC1C,IAAI,EACT,UAAU,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,EACpE,sBAAsB,EAAE,MAAM,EAC9B,uBAAuB,EAAE,MAAM,EAC/B,uBAAuB,EAAE,MAAM,EAC/B,QAAQ,EAAE,CAAC,YAAY,EAAE,gBAAgB,GAAG,IAAI,KAAK,IAAI,GACxD,IAAI;CAoCR"}

View File

@@ -0,0 +1,54 @@
import { integer, float } from '../model/CommonTypes';
import { TiledMap } from '../tiled/TiledFormat';
import { EditableTileMap } from '../model/TileMapModel';
import { TileTextureCache } from './TileTextureCache';
import PIXI = GlobalPIXIModule.PIXI;
export declare class PixiTileMapHelper {
/**
* Split an atlas image into Pixi textures.
*
* @param tiledMap A tile map exported from Tiled.
* @param atlasTexture The texture containing the whole tile set.
* @param getTexture A getter to load a texture. Used if atlasTexture is not specified.
* @returns A textures cache.
*/
static parseAtlas(
tiledMap: TiledMap,
atlasTexture: PIXI.BaseTexture<PIXI.Resource> | null,
getTexture: (textureName: string) => PIXI.BaseTexture<PIXI.Resource>
): TileTextureCache | null;
/**
* Re-renders the tile map whenever its rendering settings have been changed
*
* @param pixiTileMap the tile map renderer
* @param tileMap the tile map model
* @param textureCache the tile set textures
* @param displayMode What to display:
* - only a single layer (`index`)
* - only visible layers (`visible`)
* - everything (`all`).
* @param layerIndex If `displayMode` is set to `index`, the layer index to be
* displayed.
*/
static updatePixiTileMap(
untypedPixiTileMap: any,
tileMap: EditableTileMap,
textureCache: TileTextureCache,
displayMode: 'index' | 'visible' | 'all',
layerIndex: number
): void;
/**
* Re-renders the collision mask
*/
static updatePixiCollisionMask(
pixiGraphics: PIXI.Graphics,
tileMap: EditableTileMap,
typeFilter: string,
outlineSize: integer,
outlineColor: integer,
outlineOpacity: float,
fillColor: integer,
fillOpacity: float
): void;
}
//# sourceMappingURL=TileMapPixiHelper.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"TileMapPixiHelper.d.ts","sourceRoot":"","sources":["../../src/render/TileMapPixiHelper.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAChD,OAAO,EAEL,eAAe,EAEhB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAEtD,OAAO,IAAI,GAAG,gBAAgB,CAAC,IAAI,CAAC;AAGpC,qBAAa,iBAAiB;IAC5B;;;;;;;OAOG;IACH,MAAM,CAAC,UAAU,CACf,QAAQ,EAAE,QAAQ,EAClB,YAAY,EAAE,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,IAAI,EACpD,UAAU,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,GACnE,gBAAgB,GAAG,IAAI;IA+E1B;;;;;;;;;;;;OAYG;IACH,MAAM,CAAC,iBAAiB,CACtB,kBAAkB,EAAE,GAAG,EACvB,OAAO,EAAE,eAAe,EACxB,YAAY,EAAE,gBAAgB,EAC9B,WAAW,EAAE,OAAO,GAAG,SAAS,GAAG,KAAK,EACxC,UAAU,EAAE,MAAM,GACjB,IAAI;IA4EP;;OAEG;IACH,MAAM,CAAC,uBAAuB,CAC5B,YAAY,EAAE,IAAI,CAAC,QAAQ,EAC3B,OAAO,EAAE,eAAe,EACxB,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,OAAO,EACpB,YAAY,EAAE,OAAO,EACrB,cAAc,EAAE,KAAK,EACrB,SAAS,EAAE,OAAO,EAClB,WAAW,EAAE,KAAK,GACjB,IAAI;CA6DR"}

View File

@@ -0,0 +1,44 @@
import { integer } from '../model/CommonTypes';
import PIXI = GlobalPIXIModule.PIXI;
/**
* A cache to access the tile images.
*
* It's created by {@link PixiTileMapHelper.parseAtlas}
* and used by {@link PixiTileMapHelper.updatePixiTileMap}.
*/
export declare class TileTextureCache {
private static readonly flippedHorizontallyFlag;
private static readonly flippedVerticallyFlag;
private static readonly flippedDiagonallyFlag;
private readonly _textures;
constructor();
setTexture(
tileId: integer,
flippedHorizontally: boolean,
flippedVertically: boolean,
flippedDiagonally: boolean,
texture: PIXI.Texture
): void;
/**
* Return the texture to use for the tile with the specified uid, which can contains
* information about rotation in bits 32, 31 and 30
* (see https://doc.mapeditor.org/en/stable/reference/tmx-map-format/).
*
* @param tileId The tile identifier
* @param flippedHorizontally true if the tile is flipped horizontally.
* @param flippedVertically true if the tile is flipped vertically.
* @param flippedDiagonally true if the tile is flipped diagonally.
* @returns The texture for the given tile identifier and orientation.
*/
findTileTexture(
tileId: integer,
flippedHorizontally: boolean,
flippedVertically: boolean,
flippedDiagonally: boolean
): PIXI.Texture | undefined;
/**
* @return the Tiled tile global uniq identifier.
*/
private _getGlobalId;
}
//# sourceMappingURL=TileTextureCache.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"TileTextureCache.d.ts","sourceRoot":"","sources":["../../src/render/TileTextureCache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAE/C,OAAO,IAAI,GAAG,gBAAgB,CAAC,IAAI,CAAC;AAEpC;;;;;GAKG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,uBAAuB,CAAc;IAC7D,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,qBAAqB,CAAc;IAC3D,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,qBAAqB,CAAc;IAE3D,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA6B;;IAMvD,UAAU,CACR,MAAM,EAAE,OAAO,EACf,mBAAmB,EAAE,OAAO,EAC5B,iBAAiB,EAAE,OAAO,EAC1B,iBAAiB,EAAE,OAAO,EAC1B,OAAO,EAAE,IAAI,CAAC,OAAO,GACpB,IAAI;IAUP;;;;;;;;;;OAUG;IACH,eAAe,CACb,MAAM,EAAE,OAAO,EACf,mBAAmB,EAAE,OAAO,EAC5B,iBAAiB,EAAE,OAAO,EAC1B,iBAAiB,EAAE,OAAO,GACzB,IAAI,CAAC,OAAO,GAAG,SAAS;IA8D3B;;OAEG;IACH,OAAO,CAAC,YAAY;CAkBrB"}

View File

@@ -0,0 +1,339 @@
import { float, integer } from '../model/CommonTypes';
/**
* Tiled JSON format.
*/
export declare type TiledMap = {
/** Hex-formatted color (#RRGGBB or #AARRGGBB) (optional) */
backgroundcolor?: string;
/** The compression level to use for tile layer data (defaults to -1, which means to use the algorithm default) */
compressionlevel: integer;
/** Number of tile rows */
height: integer;
/** Length of the side of a hex tile in pixels (hexagonal maps only) */
hexsidelength?: integer;
/** Whether the map has infinite dimensions */
infinite: boolean;
/** Array of {@link TiledLayer} */
layers: Array<TiledLayer>;
/** Auto-increments for each layer */
nextlayerid: integer;
/** Auto-increments for each placed object */
nextobjectid: integer;
/** `orthogonal`, `isometric`, `staggered` or `hexagonal` */
orientation: string;
/** Array of {@link TiledProperty} */
properties?: Array<TiledProperty>;
/** `right-down` (the default), `right-up`, `left-down` or `left-up` (currently only supported for orthogonal maps) */
renderorder: string;
/** `x` or `y` (staggered / hexagonal maps only) */
staggeraxis?: string;
/** `odd` or `even` (staggered / hexagonal maps only) */
staggerindex?: string;
/** The Tiled version used to save the file */
tiledversion: string;
/** Map grid height */
tileheight: integer;
/** Array of {@link TiledTileset} */
tilesets: Array<TiledTileset>;
/** Map grid width */
tilewidth: integer;
/** `map` (since 1.0) */
type: string;
/** The JSON format version (previously a number, saved as string since 1.6) */
version: string;
/** Number of tile columns */
width: integer;
};
export declare type TiledLayer = {
/** Array of {@link TiledChunk} (optional). `tilelayer` only. */
chunks?: Array<TiledChunk>;
/** `zlib`, `gzip`, `zstd` (since Tiled 1.3) or empty (default). `tilelayer` only. */
compression?: string;
/** Array of `unsigned`, `integer` (GIDs) or base64-encoded data. `tilelayer` only.*/
data?: Array<integer> | string;
/** `topdown` (default) or `index`. `objectgroup` only. */
draworder?: string;
/** `csv` (default) or `base64`. `tilelayer` only. */
encoding?: string;
/** Row count. Same as map height for fixed-size maps. */
height?: integer;
/** Incremental ID - unique across all layers */
id?: integer;
/** Image used by this layer. `imagelayer` only. */
image?: string;
/** Array of {@link TiledLayer}. `group` only. */
layers?: Array<TiledLayer>;
/** Name assigned to this layer */
name: string;
/** Array of {@link TiledObject}. `objectgroup` only. */
objects?: Array<TiledObject>;
/** Horizontal layer offset in pixels (default: 0) */
offsetx?: float;
/** Vertical layer offset in pixels (default: 0) */
offsety?: float;
/** Value between 0 and 1 */
opacity: float;
/** Horizontal {@link parallax factor} for this layer (default: 1). (since Tiled 1.5) */
parallaxx?: float;
/** Vertical {@link parallax factor} for this layer (default: 1). (since Tiled 1.5) */
parallaxy?: float;
/** Array of {@link TiledProperty} */
properties?: Array<TiledProperty>;
/** X coordinate where layer content starts (for infinite maps) */
startx?: integer;
/** Y coordinate where layer content starts (for infinite maps) */
starty?: integer;
/** Hex-formatted {@link tint color} (#RRGGBB or #AARRGGBB) that is multiplied with any graphics drawn by this layer or any child layers (optional). */
tintcolor?: string;
/** Hex-formatted color (#RRGGBB) (optional). `imagelayer` only. */
transparentcolor?: string;
/** `tilelayer`, `objectgroup`, `imagelayer` or `group` */
type: string;
/** Whether layer is shown or hidden in editor */
visible: boolean;
/** Column count. Same as map width for fixed-size maps. */
width?: integer;
/** Horizontal layer offset in tiles. Always 0. */
x: integer;
/** Vertical layer offset in tiles. Always 0. */
y: integer;
};
export declare type TiledChunk = {
/** Array of `unsigned` `integer` (GIDs) or base64-encoded data */
data: Array<integer> | string;
/** Height in tiles */
height: integer;
/** Width in tiles */
width: integer;
/** X coordinate in tiles */
x: integer;
/** Y coordinate in tiles */
y: integer;
};
export declare type TiledObject = {
/** Used to mark an object as an ellipse */
ellipse?: boolean;
/** Global tile ID, only if object represents a tile */
gid?: integer;
/** Height in pixels. */
height: float;
/** Incremental ID, unique across all objects */
id: integer;
/** String assigned to name field in editor */
name: string;
/** Used to mark an object as a point */
point?: boolean;
/** Array of {@link TiledPoint}, in case the object is a polygon */
polygon?: Array<TiledPoint>;
/** Array of {@link TiledPoint}, in case the object is a polyline */
polyline?: Array<TiledPoint>;
/** Array of {@link TiledProperty} */
properties?: Array<TiledProperty>;
/** Angle in degrees clockwise */
rotation: float;
/** Reference to a template file, in case object is a {@link template instance} */
template?: string;
/** Only used for text objects */
text?: Text;
/** String assigned to type Tiledfield in editor */
type: string;
/** Whether object is shown in editor. */
visible: boolean;
/** Width in pixels. */
width: float;
/** X coordinate in pixels */
x: float;
/** Y coordinate in pixels */
y: float;
};
export declare type TiledText = {
/** Whether to use a bold font (default: `false`) */
bold: boolean;
/** Hex-formatted color (#RRGGBB or #AARRGGBB) (default: `#000000`) */
color: string;
/** Font family (default: `sans-serif`) */
fontfamily: string;
/** Horizontal alignment (`center`, `right`, `justify` or `left` (default)) */
halign: string;
/** Whether to use an italic font (default: `false`) */
italic: boolean;
/** Whether to use kerning when placing characters (default: `true`) */
kerning: boolean;
/** Pixel size of font (default: 16) */
pixelsize: integer;
/** Whether to strike out the text (default: `false`) */
strikeout: boolean;
/** Text */
text: string;
/** Whether to underline the text (default: `false`) */
underline: boolean;
/** Vertical alignment (`center`, `bottom` or `top` (default)) */
valign: string;
/** Whether the text is wrapped within the object bounds (default: `false`) */
wrap: boolean;
};
export declare type TiledTileset = {
/** Hex-formatted color (#RRGGBB or #AARRGGBB) (optional) */
backgroundcolor?: string;
/** The number of tile columns in the tileset */
columns: integer;
/** GID corresponding to the first tile in the set */
firstgid: integer;
/** (optional) */
grid?: TiledGrid;
/** Image used for tiles in this set */
image: string;
/** Height of source image in pixels */
imageheight: integer;
/** Width of source image in pixels */
imagewidth: integer;
/** Buffer between image edge and first tile (pixels) */
margin: integer;
/** Name given to this tileset */
name: string;
/** Alignment to use for tile objects (`unspecified` (default), `topleft`, `top`, `topright`, `left`, `center`, `right`, `bottomleft`, `bottom` or `bottomright`) (since 1.4) */
objectalignment?: string;
/** Array of {@link TiledProperty} */
properties?: Array<TiledProperty>;
/** The external file that contains this tilesets data */
source?: string;
/** Spacing between adjacent tiles in image (pixels) */
spacing: integer;
/** Array of {@link TiledTerrain} (optional) */
terrains?: Array<TiledTerrain>;
/** The number of tiles in this tileset */
tilecount: integer;
/** The Tiled version used to save the file */
tiledversion: string;
/** Maximum height of tiles in this set */
tileheight: integer;
/** (optional) */
tileoffset?: TileOffset;
/** Array of {@link TiledTileDefinition} (optional) */
tiles?: Array<TiledTileDefinition>;
/** Maximum width of tiles in this set */
tilewidth: integer;
/** Allowed transformations (optional) */
transformations?: TiledTransformations;
/** Hex-formatted color (#RRGGBB) (optional) */
transparentcolor?: string;
/** `tileset` (for tileset files, since 1.0) */
type: string;
/** The JSON format version (previously a number, saved as string since 1.6) */
version: string;
/** Array of {@link TiledWangSet} (since 1.1.5) */
wangsets?: Array<TiledWangSet>;
};
export declare type TiledGrid = {
/** Cell height of tile grid */
height: integer;
/** `orthogonal` (default) or `isometric` */
orientation: string;
/** Cell width of tile grid */
width: integer;
};
export declare type TileOffset = {
/** Horizontal offset in pixels */
x: integer;
/** Vertical offset in pixels (positive is down) */
y: integer;
};
export declare type TiledTransformations = {
/** Tiles can be flipped horizontally */
hflip: boolean;
/** Tiles can be flipped vertically */
vflip: boolean;
/** Tiles can be rotated in 90-degree increments */
rotate: boolean;
/** Whether untransformed tiles remain preferred, otherwise transformed tiles are used to produce more variations */
preferuntransformed: boolean;
};
export declare type TiledTileDefinition = {
/** Array of {@link TiledTiles} */
animation?: Array<TiledTileDefinition>;
/** Local ID of the tile */
id: integer;
/** Image representing this tile (optional) */
image?: string;
/** Height of the tile image in pixels */
imageheight?: integer;
/** Width of the tile image in pixels */
imagewidth?: integer;
/** Layer with type Tiled`objectgroup`, when collision shapes are specified (optional) */
objectgroup?: TiledLayer;
/** Percentage chance this tile is chosen when competing with others in the editor (optional) */
probability?: float;
/** Array of {@link TiledProperty} */
properties?: Array<TiledProperty>;
/** Index of terrain for each corner of tile (optional) */
terrain?: Array<integer>;
/** The type of the tile (optional) */
type?: string;
};
export declare type TiledFrame = {
/** Frame duration in milliseconds */
duration: integer;
/** Local tile ID representing this frame */
tileid: integer;
};
export declare type TiledTerrain = {
/** Name of terrain */
name: string;
/** Array of {@link TiledProperty} */
properties: Array<TiledProperty>;
/** Local ID of tile representing terrain */
tile: integer;
};
export declare type TiledWangSet = {
/** Array of {@link TiledWangColor} */
colors: Array<TiledWangColor>;
/** Name of the Wang set */
name: string;
/** Array of {@link TiledProperty} */
properties: Array<TiledProperty>;
/** Local ID of tile representing the Wang set */
tile: integer;
/** Array of {@link TiledWangTile} */
wangtiles: Array<TiledWangTile>;
};
export declare type TiledWangColor = {
/** Hex-formatted color (#RRGGBB or #AARRGGBB) */
color: string;
/** Name of the Wang color */
name: string;
/** Probability used when randomizing */
probability: float;
/** Array of {@link TiledProperty} */
properties: Array<TiledProperty>;
/** Local ID of tile representing the Wang color */
tile: integer;
};
export declare type TiledWangTile = {
/** Local ID of tile */
tileid: integer;
/** Array of Wang color indexes (`uchar[8]`) */
wangid: Array<integer>;
};
export declare type TiledObjectTemplate = {
/** `template` */
type: string;
/** External tileset used by the template (optional) */
tileset?: TiledTileset;
/** The object instantiated by this template */
object: Object;
};
export declare type TiledProperty = {
/** Name of the property */
name: string;
/** type of the property (`string` (default), `integer`, `float`, `boolean`, `color` or `file` (since 0.16, with `color` and `file` added in 0.17)) */
type: string;
/** Value of the property */
value: string | number;
};
export declare type TiledPoint = {
/** X coordinate in pixels */
x: float;
/** Y coordinate in pixels */
y: float;
};
//# sourceMappingURL=Tiled.d.ts.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,339 @@
import { float, integer } from '../model/CommonTypes';
/**
* Tiled JSON format (https://www.mapeditor.org/).
*/
export declare type TiledMap = {
/** Hex-formatted color (#RRGGBB or #AARRGGBB) (optional) */
backgroundcolor?: string;
/** The compression level to use for tile layer data (defaults to -1, which means to use the algorithm default) */
compressionlevel: integer;
/** Number of tile rows */
height: integer;
/** Length of the side of a hex tile in pixels (hexagonal maps only) */
hexsidelength?: integer;
/** Whether the map has infinite dimensions */
infinite: boolean;
/** Array of {@link TiledLayer} */
layers: Array<TiledLayer>;
/** Auto-increments for each layer */
nextlayerid: integer;
/** Auto-increments for each placed object */
nextobjectid: integer;
/** `orthogonal`, `isometric`, `staggered` or `hexagonal` */
orientation: string;
/** Array of {@link TiledProperty} */
properties?: Array<TiledProperty>;
/** `right-down` (the default), `right-up`, `left-down` or `left-up` (currently only supported for orthogonal maps) */
renderorder: string;
/** `x` or `y` (staggered / hexagonal maps only) */
staggeraxis?: string;
/** `odd` or `even` (staggered / hexagonal maps only) */
staggerindex?: string;
/** The Tiled version used to save the file */
tiledversion: string;
/** Map grid height */
tileheight: integer;
/** Array of {@link TiledTileset} */
tilesets: Array<TiledTileset>;
/** Map grid width */
tilewidth: integer;
/** `map` (since 1.0) */
type: string;
/** The JSON format version (previously a number, saved as string since 1.6) */
version: string;
/** Number of tile columns */
width: integer;
};
export declare type TiledLayer = {
/** Array of {@link TiledChunk} (optional). `tilelayer` only. */
chunks?: Array<TiledChunk>;
/** `zlib`, `gzip`, `zstd` (since Tiled 1.3) or empty (default). `tilelayer` only. */
compression?: string;
/** Array of `unsigned`, `integer` (GIDs) or base64-encoded data. `tilelayer` only.*/
data?: Array<integer> | string;
/** `topdown` (default) or `index`. `objectgroup` only. */
draworder?: string;
/** `csv` (default) or `base64`. `tilelayer` only. */
encoding?: string;
/** Row count. Same as map height for fixed-size maps. */
height?: integer;
/** Incremental ID - unique across all layers */
id?: integer;
/** Image used by this layer. `imagelayer` only. */
image?: string;
/** Array of {@link TiledLayer}. `group` only. */
layers?: Array<TiledLayer>;
/** Name assigned to this layer */
name: string;
/** Array of {@link TiledObject}. `objectgroup` only. */
objects?: Array<TiledObject>;
/** Horizontal layer offset in pixels (default: 0) */
offsetx?: float;
/** Vertical layer offset in pixels (default: 0) */
offsety?: float;
/** Value between 0 and 1 */
opacity: float;
/** Horizontal {@link parallax factor} for this layer (default: 1). (since Tiled 1.5) */
parallaxx?: float;
/** Vertical {@link parallax factor} for this layer (default: 1). (since Tiled 1.5) */
parallaxy?: float;
/** Array of {@link TiledProperty} */
properties?: Array<TiledProperty>;
/** X coordinate where layer content starts (for infinite maps) */
startx?: integer;
/** Y coordinate where layer content starts (for infinite maps) */
starty?: integer;
/** Hex-formatted {@link tint color} (#RRGGBB or #AARRGGBB) that is multiplied with any graphics drawn by this layer or any child layers (optional). */
tintcolor?: string;
/** Hex-formatted color (#RRGGBB) (optional). `imagelayer` only. */
transparentcolor?: string;
/** `tilelayer`, `objectgroup`, `imagelayer` or `group` */
type: string;
/** Whether layer is shown or hidden in editor */
visible: boolean;
/** Column count. Same as map width for fixed-size maps. */
width?: integer;
/** Horizontal layer offset in tiles. Always 0. */
x: integer;
/** Vertical layer offset in tiles. Always 0. */
y: integer;
};
export declare type TiledChunk = {
/** Array of `unsigned` `integer` (GIDs) or base64-encoded data */
data: Array<integer> | string;
/** Height in tiles */
height: integer;
/** Width in tiles */
width: integer;
/** X coordinate in tiles */
x: integer;
/** Y coordinate in tiles */
y: integer;
};
export declare type TiledObject = {
/** The class of the object (renamed from type since 1.9, optional) */
class?: string;
/** Used to mark an object as an ellipse */
ellipse?: boolean;
/** Global tile ID, only if object represents a tile */
gid?: integer;
/** Height in pixels. */
height: float;
/** Incremental ID, unique across all objects */
id: integer;
/** String assigned to name field in editor */
name: string;
/** Used to mark an object as a point */
point?: boolean;
/** Array of {@link TiledPoint}, in case the object is a polygon */
polygon?: Array<TiledPoint>;
/** Array of {@link TiledPoint}, in case the object is a polyline */
polyline?: Array<TiledPoint>;
/** Array of {@link TiledProperty} */
properties?: Array<TiledProperty>;
/** Angle in degrees clockwise */
rotation: float;
/** Reference to a template file, in case object is a {@link template instance} */
template?: string;
/** Only used for text objects */
text?: Text;
/** Whether object is shown in editor. */
visible: boolean;
/** Width in pixels. */
width: float;
/** X coordinate in pixels */
x: float;
/** Y coordinate in pixels */
y: float;
};
export declare type TiledText = {
/** Whether to use a bold font (default: `false`) */
bold: boolean;
/** Hex-formatted color (#RRGGBB or #AARRGGBB) (default: `#000000`) */
color: string;
/** Font family (default: `sans-serif`) */
fontfamily: string;
/** Horizontal alignment (`center`, `right`, `justify` or `left` (default)) */
halign: string;
/** Whether to use an italic font (default: `false`) */
italic: boolean;
/** Whether to use kerning when placing characters (default: `true`) */
kerning: boolean;
/** Pixel size of font (default: 16) */
pixelsize: integer;
/** Whether to strike out the text (default: `false`) */
strikeout: boolean;
/** Text */
text: string;
/** Whether to underline the text (default: `false`) */
underline: boolean;
/** Vertical alignment (`center`, `bottom` or `top` (default)) */
valign: string;
/** Whether the text is wrapped within the object bounds (default: `false`) */
wrap: boolean;
};
export declare type TiledTileset = {
/** Hex-formatted color (#RRGGBB or #AARRGGBB) (optional) */
backgroundcolor?: string;
/** The number of tile columns in the tileset */
columns: integer;
/** GID corresponding to the first tile in the set */
firstgid: integer;
/** (optional) */
grid?: TiledGrid;
/** Image used for tiles in this set */
image: string;
/** Height of source image in pixels */
imageheight: integer;
/** Width of source image in pixels */
imagewidth: integer;
/** Buffer between image edge and first tile (pixels) */
margin: integer;
/** Name given to this tileset */
name: string;
/** Alignment to use for tile objects (`unspecified` (default), `topleft`, `top`, `topright`, `left`, `center`, `right`, `bottomleft`, `bottom` or `bottomright`) (since 1.4) */
objectalignment?: string;
/** Array of {@link TiledProperty} */
properties?: Array<TiledProperty>;
/** The external file that contains this tilesets data */
source?: string;
/** Spacing between adjacent tiles in image (pixels) */
spacing: integer;
/** Array of {@link TiledTerrain} (optional) */
terrains?: Array<TiledTerrain>;
/** The number of tiles in this tileset */
tilecount: integer;
/** The Tiled version used to save the file */
tiledversion: string;
/** Maximum height of tiles in this set */
tileheight: integer;
/** (optional) */
tileoffset?: TileOffset;
/** Array of {@link TiledTileDefinition} (optional) */
tiles?: Array<TiledTileDefinition>;
/** Maximum width of tiles in this set */
tilewidth: integer;
/** Allowed transformations (optional) */
transformations?: TiledTransformations;
/** Hex-formatted color (#RRGGBB) (optional) */
transparentcolor?: string;
/** `tileset` (for tileset files, since 1.0) */
type: string;
/** The JSON format version (previously a number, saved as string since 1.6) */
version: string;
/** Array of {@link TiledWangSet} (since 1.1.5) */
wangsets?: Array<TiledWangSet>;
};
export declare type TiledGrid = {
/** Cell height of tile grid */
height: integer;
/** `orthogonal` (default) or `isometric` */
orientation: string;
/** Cell width of tile grid */
width: integer;
};
export declare type TileOffset = {
/** Horizontal offset in pixels */
x: integer;
/** Vertical offset in pixels (positive is down) */
y: integer;
};
export declare type TiledTransformations = {
/** Tiles can be flipped horizontally */
hflip: boolean;
/** Tiles can be flipped vertically */
vflip: boolean;
/** Tiles can be rotated in 90-degree increments */
rotate: boolean;
/** Whether untransformed tiles remain preferred, otherwise transformed tiles are used to produce more variations */
preferuntransformed: boolean;
};
export declare type TiledTileDefinition = {
/** Array of {@link TiledTiles} */
animation?: Array<TiledTileDefinition>;
/** The class of the tile (renamed from type since 1.9, optional) */
class?: string;
/** Local ID of the tile */
id: integer;
/** Image representing this tile (optional) */
image?: string;
/** Height of the tile image in pixels */
imageheight?: integer;
/** Width of the tile image in pixels */
imagewidth?: integer;
/** Layer with type Tiled`objectgroup`, when collision shapes are specified (optional) */
objectgroup?: TiledLayer;
/** Percentage chance this tile is chosen when competing with others in the editor (optional) */
probability?: float;
/** Array of {@link TiledProperty} */
properties?: Array<TiledProperty>;
/** Index of terrain for each corner of tile (optional) */
terrain?: Array<integer>;
};
export declare type TiledFrame = {
/** Frame duration in milliseconds */
duration: integer;
/** Local tile ID representing this frame */
tileid: integer;
};
export declare type TiledTerrain = {
/** Name of terrain */
name: string;
/** Array of {@link TiledProperty} */
properties: Array<TiledProperty>;
/** Local ID of tile representing terrain */
tile: integer;
};
export declare type TiledWangSet = {
/** Array of {@link TiledWangColor} */
colors: Array<TiledWangColor>;
/** Name of the Wang set */
name: string;
/** Array of {@link TiledProperty} */
properties: Array<TiledProperty>;
/** Local ID of tile representing the Wang set */
tile: integer;
/** Array of {@link TiledWangTile} */
wangtiles: Array<TiledWangTile>;
};
export declare type TiledWangColor = {
/** Hex-formatted color (#RRGGBB or #AARRGGBB) */
color: string;
/** Name of the Wang color */
name: string;
/** Probability used when randomizing */
probability: float;
/** Array of {@link TiledProperty} */
properties: Array<TiledProperty>;
/** Local ID of tile representing the Wang color */
tile: integer;
};
export declare type TiledWangTile = {
/** Local ID of tile */
tileid: integer;
/** Array of Wang color indexes (`uchar[8]`) */
wangid: Array<integer>;
};
export declare type TiledObjectTemplate = {
/** `template` */
type: string;
/** External tileset used by the template (optional) */
tileset?: TiledTileset;
/** The object instantiated by this template */
object: Object;
};
export declare type TiledProperty = {
/** Name of the property */
name: string;
/** type of the property (`string` (default), `integer`, `float`, `boolean`, `color` or `file` (since 0.16, with `color` and `file` added in 0.17)) */
type: string;
/** Value of the property */
value: string | number;
};
export declare type TiledPoint = {
/** X coordinate in pixels */
x: float;
/** Y coordinate in pixels */
y: float;
};
//# sourceMappingURL=TiledFormat.d.ts.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,37 @@
import { integer } from '../model/CommonTypes';
import { TiledLayer } from './TiledFormat';
/**
* Decodes a layer data, which can sometimes be store as a compressed base64 string
* by Tiled.
* See https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#data.
* @param pako The zlib library.
* @param layer The layer data from a Tiled JSON.
* @returns The decoded layer data.
*/
export declare const decodeBase64LayerData: (
pako: any,
layer: TiledLayer
) => number[];
export declare type TiledGID = {
id: integer;
flippedHorizontally: boolean;
flippedVertically: boolean;
flippedDiagonally: boolean;
};
/**
* Extract information about the rotation of a tile from the tile id.
* @param globalTileUid The Tiled tile global uniq identifier.
* @returns The tile identifier and orientation.
*/
export declare const extractTileUidFlippedStates: (
globalTileUid: integer
) => TiledGID;
/**
* Tiled use 0 as null, we do too but it's black boxed.
* This is why the id needs to be decremented.
* @return the tile identifier used in {@link TilMapModel}.
*/
export declare const getTileIdFromTiledGUI: (
tiledGUI: number | undefined
) => number | undefined;
//# sourceMappingURL=TiledLoaderHelper.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"TiledLoaderHelper.d.ts","sourceRoot":"","sources":["../../src/tiled/TiledLoaderHelper.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAC/C,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAE3C;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB,SAAU,GAAG,SAAS,UAAU,aAgDjE,CAAC;AAEF,oBAAY,QAAQ,GAAG;IACrB,EAAE,EAAE,OAAO,CAAC;IACZ,mBAAmB,EAAE,OAAO,CAAC;IAC7B,iBAAiB,EAAE,OAAO,CAAC;IAC3B,iBAAiB,EAAE,OAAO,CAAC;CAC5B,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,2BAA2B,kBACvB,OAAO,KACrB,QAuBF,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,aACtB,MAAM,GAAG,SAAS,KAC3B,MAAM,GAAG,SAAwD,CAAC"}

View File

@@ -0,0 +1,9 @@
import { EditableTileMap } from '../model/TileMapModel';
import { TiledMap } from './TiledFormat';
/**
* It creates a {@link EditableTileMap} from a Tiled JSON.
*/
export declare class TiledTileMapLoader {
static load(pako: any, tiledMap: TiledMap): EditableTileMap | null;
}
//# sourceMappingURL=TiledTileMapLoader.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"TiledTileMapLoader.d.ts","sourceRoot":"","sources":["../../src/tiled/TiledTileMapLoader.ts"],"names":[],"mappings":"AACA,OAAO,EACL,eAAe,EAGhB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAOzC;;GAEG;AACH,qBAAa,kBAAkB;IAC7B,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,EAAE,QAAQ,EAAE,QAAQ,GAAG,eAAe,GAAG,IAAI;CA2KnE"}

View File

@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=TiledTileMapLoader.spec.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"TiledTileMapLoader.spec.d.ts","sourceRoot":"","sources":["../../src/tiled/TiledTileMapLoader.spec.ts"],"names":[],"mappings":""}

166
Extensions/TileMap/pako/dist/pako.d.ts vendored Normal file
View File

@@ -0,0 +1,166 @@
// Type definitions for pako 1.0
// Project: https://github.com/nodeca/pako
// Definitions by: Denis Cappellin <https://github.com/cappellin>
// Caleb Eggensperger <https://github.com/calebegg>
// Muhammet Öztürk <https://github.com/hlthi>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
export = Pako;
export as namespace pako;
declare namespace Pako {
enum FlushValues {
Z_NO_FLUSH = 0,
Z_PARTIAL_FLUSH = 1,
Z_SYNC_FLUSH = 2,
Z_FULL_FLUSH = 3,
Z_FINISH = 4,
Z_BLOCK = 5,
Z_TREES = 6,
}
enum StrategyValues {
Z_FILTERED = 1,
Z_HUFFMAN_ONLY = 2,
Z_RLE = 3,
Z_FIXED = 4,
Z_DEFAULT_STRATEGY = 0,
}
enum ReturnCodes {
Z_OK = 0,
Z_STREAM_END = 1,
Z_NEED_DICT = 2,
Z_ERRNO = -1,
Z_STREAM_ERROR = -2,
Z_DATA_ERROR = -3,
Z_BUF_ERROR = -5,
}
interface DeflateOptions {
level?: -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | undefined;
windowBits?: number | undefined;
memLevel?: number | undefined;
strategy?: StrategyValues | undefined;
dictionary?: any;
raw?: boolean | undefined;
to?: 'string' | undefined;
chunkSize?: number | undefined;
gzip?: boolean | undefined;
header?: Header | undefined;
}
interface DeflateFunctionOptions {
level?: -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | undefined;
windowBits?: number | undefined;
memLevel?: number | undefined;
strategy?: StrategyValues | undefined;
dictionary?: any;
raw?: boolean | undefined;
to?: 'string' | undefined;
}
interface InflateOptions {
windowBits?: number | undefined;
dictionary?: any;
raw?: boolean | undefined;
to?: 'string' | undefined;
chunkSize?: number | undefined;
}
interface InflateFunctionOptions {
windowBits?: number | undefined;
raw?: boolean | undefined;
to?: 'string' | undefined;
}
interface Header {
text?: boolean | undefined;
time?: number | undefined;
os?: number | undefined;
extra?: number[] | undefined;
name?: string | undefined;
comment?: string | undefined;
hcrc?: boolean | undefined;
}
type Data = Uint8Array | number[] | string;
/**
* Compress data with deflate algorithm and options.
*/
function deflate(
data: Data,
options: DeflateFunctionOptions & { to: 'string' }
): string;
function deflate(data: Data, options?: DeflateFunctionOptions): Uint8Array;
/**
* The same as deflate, but creates raw data, without wrapper (header and adler32 crc).
*/
function deflateRaw(
data: Data,
options: DeflateFunctionOptions & { to: 'string' }
): string;
function deflateRaw(data: Data, options?: DeflateFunctionOptions): Uint8Array;
/**
* The same as deflate, but create gzip wrapper instead of deflate one.
*/
function gzip(
data: Data,
options: DeflateFunctionOptions & { to: 'string' }
): string;
function gzip(data: Data, options?: DeflateFunctionOptions): Uint8Array;
/**
* Decompress data with inflate/ungzip and options. Autodetect format via wrapper header
* by default. That's why we don't provide separate ungzip method.
*/
function inflate(
data: Data,
options: InflateFunctionOptions & { to: 'string' }
): string;
function inflate(data: Data, options?: InflateFunctionOptions): Uint8Array;
/**
* The same as inflate, but creates raw data, without wrapper (header and adler32 crc).
*/
function inflateRaw(
data: Data,
options: InflateFunctionOptions & { to: 'string' }
): string;
function inflateRaw(data: Data, options?: InflateFunctionOptions): Uint8Array;
/**
* Just shortcut to inflate, because it autodetects format by header.content. Done for convenience.
*/
function ungzip(
data: Data,
options: InflateFunctionOptions & { to: 'string' }
): string;
function ungzip(data: Data, options?: InflateFunctionOptions): Uint8Array;
// https://github.com/nodeca/pako/blob/893381abcafa10fa2081ce60dae7d4d8e873a658/lib/deflate.js
class Deflate {
constructor(options?: DeflateOptions);
err: ReturnCodes;
msg: string;
result: Uint8Array | number[];
onData(chunk: Data): void;
onEnd(status: number): void;
push(data: Data | ArrayBuffer, mode?: FlushValues | boolean): boolean;
}
// https://github.com/nodeca/pako/blob/893381abcafa10fa2081ce60dae7d4d8e873a658/lib/inflate.js
class Inflate {
constructor(options?: InflateOptions);
header?: Header | undefined;
err: ReturnCodes;
msg: string;
result: Data;
onData(chunk: Data): void;
onEnd(status: number): void;
push(data: Data | ArrayBuffer, mode?: FlushValues | boolean): boolean;
}
}

View File

@@ -1,470 +0,0 @@
// @ts-check
(function (root, factory) {
// @ts-ignore
if (typeof exports === 'object' && typeof exports.nodeName !== 'string') {
// CommonJS
factory(exports);
} else {
// Browser globals
// @ts-ignore
factory((root.PixiTileMapHelper = {}));
}
})(typeof self !== 'undefined' ? self : this, function (exports) {
/** @typedef {GlobalPIXIModule.PIXI.Texture} PIXI.Texture */
/** @typedef {GlobalPIXIModule.PIXI.BaseTexture} PIXI.BaseTexture */
/** @typedef {GlobalPIXIModule.PIXI.Rectangle} PIXI.Rectangle */
const PIXI = GlobalPIXIModule.PIXI;
/**
* Information about one or more tiles. Loosely based on
* https://doc.mapeditor.org/en/stable/reference/json-map-format/#tile-definition.
*
* @typedef {{
"id": number,
"terrain"?: Array<number>,
"animation"?: Array<{duration: number, tileid: number}>
}} TiledDataTile
*/
/**
* Information about a layer. Loosely based on
* https://doc.mapeditor.org/en/stable/reference/json-map-format/#layer.
*
* @typedef {{
"compression"?:"zlib" | "gzip" | "zstd",
"data":Array<number> | string,
"encoding"?:"base64",
"height":number,
"id":number,
"name": string,
"opacity": number,
"type": string,
"visible":boolean,
"width":number,
"objects": Array<{ gid: number, x: number, y: number, visible: boolean }>
}} TiledDataLayer
*/
/**
* Data to render a tile map. Loosely based on the merge of a Tiled
* map and tileset.
*
* @typedef {{
width: number,
height: number,
tileWidth: number,
tileHeight: number,
atlasTexture: PIXI.BaseTexture,
textureCache: Object<number, PIXI.Texture | null>,
layers: Array<TiledDataLayer>,
tiles: Array<TiledDataTile>,
}} GenericPixiTileMapData
*/
/**
* The Tilesets that are ready to be used
* with Pixi Tilemap, indexed by their id.
* @type {Object<string, GenericPixiTileMapData>}
*/
const loadedGenericPixiTileMapData = {};
/**
* Parse a Tiled map JSON file,
* exported from Tiled (https://www.mapeditor.org/)
* into a generic tile map data (`GenericPixiTileMapData`).
*
* @param {Object} tiledData A JS object representing a map exported from Tiled.
* @param {?PIXI.BaseTexture} atlasTexture
* @param {(textureName: string) => PIXI.BaseTexture} getTexture A getter to load a texture. Used if atlasTexture is not specified.
* @returns {?GenericPixiTileMapData}
*/
const parseTiledData = (tiledData, atlasTexture, getTexture) => {
if (!tiledData.tiledversion) {
console.warn(
"The loaded Tiled map does not contain a 'tiledversion' key. Are you sure this file has been exported from Tiled (mapeditor.org)?"
);
return null;
}
// We only handle tileset embedded in the tilemap. Warn if it's not the case.
if (!tiledData.tilesets.length || 'source' in tiledData.tilesets[0]) {
console.warn(
"The loaded Tiled map seems not to contain any tileset data (nothing in 'tilesets' key)."
);
return null;
}
const {
tilewidth,
tileheight,
tilecount,
tiles,
image,
columns,
spacing,
margin,
} = tiledData.tilesets[0];
if (!atlasTexture) atlasTexture = getTexture(image);
// We try to detect what size Tiled is expecting.
const rows = tilecount / columns;
const expectedAtlasWidth =
tilewidth * columns + spacing * (columns - 1) + margin * 2;
const expectedAtlasHeight =
tileheight * rows + spacing * (rows - 1) + margin * 2;
if (
(atlasTexture.width !== 1 && expectedAtlasWidth !== atlasTexture.width) ||
(atlasTexture.height !== 1 && expectedAtlasHeight !== atlasTexture.height)
) {
const expectedSize = expectedAtlasWidth + 'x' + expectedAtlasHeight;
const actualSize = atlasTexture.width + 'x' + atlasTexture.height;
console.warn(
'It seems the atlas file was resized, which is not supported. It should be ' +
expectedSize +
"px, but it's " +
actualSize +
' px.'
);
return null;
}
// Prepare the textures pointing to the base "Atlas" Texture for each tile.
// Note that this cache can be augmented later with rotated/flipped
// versions of the tile textures.
/** @type {Object<number, PIXI.Texture | null>} */
const textureCache = {};
for (let frame = 0; frame <= tilecount; frame++) {
const columnMultiplier = Math.floor((frame - 1) % columns);
const rowMultiplier = Math.floor((frame - 1) / columns);
const x = margin + columnMultiplier * (tilewidth + spacing);
const y = margin + rowMultiplier * (tileheight + spacing);
try {
const rect = new PIXI.Rectangle(x, y, tilewidth, tileheight);
// @ts-ignore - atlasTexture is never null here.
const texture = new PIXI.Texture(atlasTexture, rect);
textureCache[frame] = texture;
} catch (error) {
console.error(
'An error occurred while creating a PIXI.Texture to be used in a TileMap:',
error
);
textureCache[frame] = null;
}
}
/** @type {GenericPixiTileMapData} */
const tileMapData = {
width: atlasTexture.width,
height: atlasTexture.height,
tileWidth: tilewidth,
tileHeight: tileheight,
atlasTexture: atlasTexture,
textureCache: textureCache,
layers: tiledData.layers,
tiles: tiles,
};
return tileMapData;
};
/**
* Decodes a layer data, which can sometimes be store as a compressed base64 string
* by Tiled.
* See https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#data.
*/
const decodeBase64LayerData = (layer, pako) => {
const { data, compression } = layer;
let index = 4;
const decodedData = [];
let step1 = atob(data)
.split('')
.map(function (x) {
return x.charCodeAt(0);
});
try {
const decodeString = (str, index) =>
(str.charCodeAt(index) +
(str.charCodeAt(index + 1) << 8) +
(str.charCodeAt(index + 2) << 16) +
(str.charCodeAt(index + 3) << 24)) >>>
0;
const decodeArray = (arr, index) =>
(arr[index] +
(arr[index + 1] << 8) +
(arr[index + 2] << 16) +
(arr[index + 3] << 24)) >>>
0;
if (compression === 'zlib') {
const binData = new Uint8Array(step1);
step1 = pako.inflate(binData);
while (index <= step1.length) {
decodedData.push(decodeArray(step1, index - 4));
index += 4;
}
} else if (compression === 'zstd') {
console.error(
'Zstandard compression is not supported for layers in a Tilemap. Use instead zlib compression or no compression.'
);
return null;
} else {
while (index <= step1.length) {
decodedData.push(decodeString(step1, index - 4));
index += 4;
}
}
return decodedData;
} catch (error) {
console.error(
'Failed to decompress and unzip base64 layer.data string',
error
);
return null;
}
};
/**
* Extract information about the rotation of a tile from the tile id.
* @param {number} globalTileUid
* @returns {[number, boolean, boolean, boolean]}
*/
const extractTileUidFlippedStates = (globalTileUid) => {
const FLIPPED_HORIZONTALLY_FLAG = 0x80000000;
const FLIPPED_VERTICALLY_FLAG = 0x40000000;
const FLIPPED_DIAGONALLY_FLAG = 0x20000000;
const flippedHorizontally = globalTileUid & FLIPPED_HORIZONTALLY_FLAG;
const flippedVertically = globalTileUid & FLIPPED_VERTICALLY_FLAG;
const flippedDiagonally = globalTileUid & FLIPPED_DIAGONALLY_FLAG;
const tileUid =
globalTileUid &
~(
FLIPPED_HORIZONTALLY_FLAG |
FLIPPED_VERTICALLY_FLAG |
FLIPPED_DIAGONALLY_FLAG
);
return [tileUid, !!flippedHorizontally, !!flippedVertically, !!flippedDiagonally];
};
/**
* Return the texture to use for the tile with the specified uid, which can contains
* information about rotation in bits 32, 31 and 30
* (see https://doc.mapeditor.org/en/stable/reference/tmx-map-format/).
*
* @param {Object<number, PIXI.Texture | null>} textureCache
* @param {number} globalTileUid
* @returns {?PIXI.Texture}
*/
const findTileTextureInCache = (textureCache, globalTileUid) => {
if (globalTileUid === 0) return null;
if (textureCache[globalTileUid]) {
return textureCache[globalTileUid];
}
// If the texture is not in the cache, it's potentially because its ID
// is a flipped/rotated version of another ID.
const flippedStates = extractTileUidFlippedStates(globalTileUid);
const tileUid = flippedStates[0];
const flippedHorizontally = flippedStates[1];
const flippedVertically = flippedStates[2];
const flippedDiagonally = flippedStates[3];
if (tileUid === 0) return null;
// If the tile still can't be found in the cache, it means the ID we got
// is invalid.
const unflippedTexture = textureCache[tileUid];
if (!unflippedTexture) return null;
// Clone the unflipped texture and save it in the cache
const frame = unflippedTexture.frame.clone();
const orig = unflippedTexture.orig.clone();
if (flippedDiagonally) {
const width = orig.width;
orig.width = orig.height;
orig.height = width;
}
const trim = orig.clone();
// Get the rotation "D8" number.
// See https://pixijs.io/examples/#/textures/texture-rotate.js
let rotate = 0;
if (flippedDiagonally) {
rotate = 10;
if (!flippedHorizontally && flippedVertically) {
rotate = 2;
} else if (flippedHorizontally && !flippedVertically) {
rotate = 6;
} else if (flippedHorizontally && flippedVertically) {
rotate = 14;
}
} else {
rotate = 0;
if (!flippedHorizontally && flippedVertically) {
rotate = 8;
} else if (flippedHorizontally && !flippedVertically) {
rotate = 12;
} else if (flippedHorizontally && flippedVertically) {
rotate = 4;
}
}
const flippedTexture = new PIXI.Texture(
unflippedTexture.baseTexture,
frame,
orig,
trim,
rotate
);
return (textureCache[globalTileUid] = flippedTexture);
};
/**
* Re-renders the tilemap whenever its rendering settings have been changed
*
* @param {any} pixiTileMap
* @param {GenericPixiTileMapData} genericTileMapData
* @param {'index' | 'visible' | 'all'} displayMode What to display: only a single layer (`index`), only visible layers (`visible`) or everyhing (`all`).
* @param {number} layerIndex If `displayMode` is set to `index`, the layer index to be displayed.
* @param {any} pako The Pako library object, to decompress the layer data.
*/
exports.updatePixiTileMap = (
pixiTileMap,
genericTileMapData,
displayMode,
layerIndex,
pako
) => {
if (!pixiTileMap || !genericTileMapData) return;
pixiTileMap.clear();
genericTileMapData.layers.forEach(function (layer, index) {
if (displayMode === 'index' && layerIndex !== index) return;
else if (displayMode === 'visible' && !layer.visible) return;
if (layer.type === 'objectgroup') {
layer.objects.forEach(function (object) {
const { gid, x, y, visible } = object;
if (displayMode === 'visible' && !visible) return;
if (genericTileMapData.textureCache[gid]) {
pixiTileMap.addFrame(
genericTileMapData.textureCache[gid],
x,
y - genericTileMapData.tileHeight
);
}
});
} else if (layer.type === 'tilelayer') {
let tileSlotIndex = 0;
let layerData = layer.data;
if (layer.encoding === 'base64') {
// @ts-ignore
layerData = decodeBase64LayerData(layer, pako);
if (!layerData) {
console.warn('Failed to uncompress layer.data');
return;
}
}
for (let i = 0; i < layer.height; i++) {
for (let j = 0; j < layer.width; j++) {
const xPos = genericTileMapData.tileWidth * j;
const yPos = genericTileMapData.tileHeight * i;
// The "globalTileUid" is the tile UID with encoded
// bits about the flipping/rotation of the tile.
/** @type {number} */
// @ts-ignore
const globalTileUid = layerData[tileSlotIndex];
// Extract the tile UID and the texture.
const tileUid = extractTileUidFlippedStates(globalTileUid)[0];
const tileTexture = findTileTextureInCache(
genericTileMapData.textureCache,
globalTileUid
);
if (tileTexture) {
const tileData =
genericTileMapData.tiles &&
genericTileMapData.tiles.find(function (tile) {
return tile.id === tileUid - 1;
});
const pixiTilemapFrame = pixiTileMap.addFrame(
tileTexture,
xPos,
yPos
);
// Animated tiles have a limitation:
// they are only able to use frames arranged horizontally one next
// to each other on the atlas.
if (tileData && tileData.animation) {
pixiTilemapFrame.tileAnimX(
genericTileMapData.tileWidth,
tileData.animation.length
);
}
}
tileSlotIndex += 1;
}
}
}
});
};
/**
* Load the given data, exported from Tiled, into a generic tilemap data (`GenericPixiTileMapData`),
* which can then be used to update a PIXI.Tilemap (see `updatePixiTileMap`).
*
* Later on, this could potentially be refactored to support other data structures
* (LDtk, for example: https://github.com/deepnight/ldtk).
*
* @param {(textureName: string) => PIXI.BaseTexture} getTexture A getter to load a texture. Used if atlasTexture is not specified.
* @param {Object} tiledData A JS object representing a map exported from Tiled.
* @param {string} atlasImageResourceName The name of the resource to pass to `getTexture` to load the atlas.
* @param {string} tilemapResourceName The name of the tilemap resource - used to index internally the loaded tilemap data.
* @param {string} tilesetResourceName The name of the tileset resource - used to index internally the loaded tilemap data.
* @returns {?GenericPixiTileMapData}
*/
exports.loadPixiTileMapData = (
getTexture,
tiledData,
atlasImageResourceName,
tilemapResourceName,
tilesetResourceName
) => {
const requestedTileMapDataId =
tilemapResourceName +
'@' +
tilesetResourceName +
'@' +
atlasImageResourceName;
// If the tilemap data is already in the cache, use it directly.
if (loadedGenericPixiTileMapData[requestedTileMapDataId]) {
return loadedGenericPixiTileMapData[requestedTileMapDataId];
}
const atlasTexture = atlasImageResourceName
? getTexture(atlasImageResourceName)
: null;
const genericPixiTileMapData = parseTiledData(
tiledData,
atlasTexture,
getTexture
);
if (genericPixiTileMapData)
loadedGenericPixiTileMapData[
requestedTileMapDataId
] = genericPixiTileMapData;
return genericPixiTileMapData;
};
});

View File

@@ -0,0 +1,29 @@
import {
CanvasTileRenderer,
CompositeRectTileLayer,
GraphicsLayer,
IMultiTextureOptions,
MultiTextureResource,
RectTileGeom,
RectTileLayer,
RectTileShader,
TileRenderer,
ZLayer,
} from './pixi-tilemap';
declare global {
namespace PIXI {
export namespace tilemap {
export { CanvasTileRenderer };
export { CompositeRectTileLayer };
export { GraphicsLayer };
export { IMultiTextureOptions };
export { MultiTextureResource };
export { RectTileGeom };
export { RectTileLayer };
export { RectTileShader };
export { TileRenderer };
export { ZLayer };
}
}
}

View File

@@ -0,0 +1,262 @@
import PIXI = GlobalPIXIModule.PIXI;
export declare class CanvasTileRenderer {
renderer: PIXI.Renderer;
tileAnim: number[];
dontUseTransform: boolean;
constructor(renderer: PIXI.Renderer);
}
export declare class CompositeRectTileLayer extends PIXI.Container {
constructor(
zIndex?: number,
bitmaps?: Array<PIXI.Texture>,
texPerChild?: number
);
z: number;
// @ts-ignore Maybe it's a compatibility issue with the PIXI version we are using
zIndex: number;
modificationMarker: number;
shadowColor: Float32Array;
_globalMat: PIXI.Matrix;
_lastLayer: RectTileLayer;
texPerChild: number;
tileAnim: number[];
initialize(
zIndex?: number,
bitmaps?: Array<PIXI.Texture>,
texPerChild?: number
): void;
setBitmaps(bitmaps: Array<PIXI.Texture>): void;
clear(): void;
addRect(
textureIndex: number,
u: number,
v: number,
x: number,
y: number,
tileWidth: number,
tileHeight: number,
animX?: number,
animY?: number,
rotate?: number,
animWidth?: number,
animHeight?: number
): this;
tileRotate(rotate: number): this;
tileAnimX(offset: number, count: number): this;
tileAnimY(offset: number, count: number): this;
addFrame(
texture_: PIXI.Texture | String | number,
x: number,
y: number,
animX?: number,
animY?: number,
animWidth?: number,
animHeight?: number
): this;
renderCanvas(renderer: any): void;
render(renderer: PIXI.Renderer): void;
isModified(anim: boolean): boolean;
clearModify(): void;
}
export declare const Constant: {
maxTextures: number;
bufferSize: number;
boundSize: number;
boundCountPerBuffer: number;
use32bitIndex: boolean;
SCALE_MODE: PIXI.SCALE_MODES;
DO_CLEAR: boolean;
};
export declare function fillSamplers(
shader: TilemapShader,
maxTextures: number
): void;
export declare function generateFragmentSrc(
maxTextures: number,
fragmentSrc: string
): string;
export declare function generateSampleSrc(maxTextures: number): string;
export declare class GraphicsLayer extends PIXI.Graphics {
constructor(zIndex: number);
renderCanvas(renderer: any): void;
isModified(anim: boolean): boolean;
clearModify(): void;
}
export declare interface IMultiTextureOptions {
boundCountPerBuffer: number;
boundSize: number;
bufferSize: number;
DO_CLEAR?: boolean;
}
export declare class MultiTextureResource extends PIXI.Resource {
constructor(options: IMultiTextureOptions);
DO_CLEAR: boolean;
boundSize: number;
_clearBuffer: Uint8Array;
bind(baseTexture: PIXI.BaseTexture): void;
baseTex: PIXI.BaseTexture;
boundSprites: Array<PIXI.Sprite>;
dirties: Array<number>;
setTexture(ind: number, texture: PIXI.Texture): void;
upload(
renderer: PIXI.Renderer,
texture: PIXI.BaseTexture,
glTexture: PIXI.GLTexture
): boolean;
}
export declare const pixi_tilemap: {
CanvasTileRenderer: typeof CanvasTileRenderer;
CompositeRectTileLayer: typeof CompositeRectTileLayer;
Constant: {
maxTextures: number;
bufferSize: number;
boundSize: number;
boundCountPerBuffer: number;
use32bitIndex: boolean;
SCALE_MODE: PIXI.SCALE_MODES;
DO_CLEAR: boolean;
};
GraphicsLayer: typeof GraphicsLayer;
MultiTextureResource: typeof MultiTextureResource;
RectTileLayer: typeof RectTileLayer;
TilemapShader: typeof TilemapShader;
RectTileShader: typeof RectTileShader;
RectTileGeom: typeof RectTileGeom;
TileRenderer: typeof TileRenderer;
ZLayer: typeof ZLayer;
};
export declare const POINT_STRUCT_SIZE = 12;
export declare class RectTileGeom extends PIXI.Geometry {
vertSize: number;
vertPerQuad: number;
stride: number;
lastTimeAccess: number;
constructor();
buf: PIXI.Buffer;
}
export declare class RectTileLayer extends PIXI.Container {
constructor(zIndex: number, texture: PIXI.Texture | Array<PIXI.Texture>);
// @ts-ignore Maybe it's a compatibility issue with the PIXI version we are using
zIndex: number;
modificationMarker: number;
_$_localBounds: PIXI.Bounds;
shadowColor: Float32Array;
_globalMat: PIXI.Matrix;
pointsBuf: Array<number>;
hasAnim: boolean;
textures: Array<PIXI.Texture>;
offsetX: number;
offsetY: number;
compositeParent: boolean;
initialize(
zIndex: number,
textures: PIXI.Texture | Array<PIXI.Texture>
): void;
clear(): void;
addFrame(
texture_: PIXI.Texture | String | number,
x: number,
y: number,
animX: number,
animY: number
): boolean;
addRect(
textureIndex: number,
u: number,
v: number,
x: number,
y: number,
tileWidth: number,
tileHeight: number,
animX?: number,
animY?: number,
rotate?: number,
animCountX?: number,
animCountY?: number
): this;
tileRotate(rotate: number): void;
tileAnimX(offset: number, count: number): void;
tileAnimY(offset: number, count: number): void;
renderCanvas(renderer: any): void;
renderCanvasCore(renderer: any): void;
vbId: number;
vb: RectTileGeom;
vbBuffer: ArrayBuffer;
vbArray: Float32Array;
vbInts: Uint32Array;
destroyVb(): void;
render(renderer: PIXI.Renderer): void;
renderWebGLCore(renderer: PIXI.Renderer, plugin: TileRenderer): void;
isModified(anim: boolean): boolean;
clearModify(): void;
protected _calculateBounds(): void;
getLocalBounds(rect?: PIXI.Rectangle): PIXI.Rectangle;
destroy(options?: any): void;
}
export declare class RectTileShader extends TilemapShader {
constructor(maxTextures: number);
}
export declare abstract class TilemapShader extends PIXI.Shader {
maxTextures: number;
constructor(maxTextures: number, shaderVert: string, shaderFrag: string);
}
export declare class TileRenderer extends PIXI.ObjectRenderer {
renderer: PIXI.Renderer;
gl: WebGLRenderingContext;
sn: number;
indexBuffer: PIXI.Buffer;
ibLen: number;
tileAnim: number[];
texLoc: Array<number>;
rectShader: RectTileShader;
texResources: Array<MultiTextureResource>;
constructor(renderer: PIXI.Renderer);
initBounds(): void;
bindTexturesWithoutRT(
renderer: PIXI.Renderer,
shader: TilemapShader,
textures: Array<PIXI.Texture>
): void;
bindTextures(
renderer: PIXI.Renderer,
shader: TilemapShader,
textures: Array<PIXI.Texture>
): void;
start(): void;
createVb(): RectTileGeom;
checkIndexBuffer(size: number, vb?: RectTileGeom): void;
getShader(): TilemapShader;
destroy(): void;
}
export declare class ZLayer extends PIXI.Container {
constructor(tilemap: PIXI.Container, zIndex: number);
tilemap: any;
z: number;
// @ts-ignore Maybe it's a compatibility issue with the PIXI version we are using
zIndex: number;
_previousLayers: number;
canvasBuffer: HTMLCanvasElement;
_tempRender: any;
_lastAnimationFrame: number;
layerTransform: PIXI.Matrix;
clear(): void;
cacheIfDirty(): void;
renderCanvas(renderer: any): void;
}

View File

@@ -0,0 +1,238 @@
// @ts-check
describe('gdjs.TileMapCollisionMaskRuntimeObject', function () {
const createScene = (framePerSecond = 60) => {
const runtimeGame = new gdjs.RuntimeGame({
variables: [],
// @ts-ignore - missing properties.
properties: { windowWidth: 800, windowHeight: 600 },
resources: {
resources: [
{
file: 'base/tests-utils/simple-tiled-map/SmallTiledMap.json',
kind: 'json',
metadata: '',
name: 'SmallTiledMap.json',
userAdded: true,
alwaysLoaded: true,
},
{
file: 'base/tests-utils/simple-tiled-map/MiniTiledSet.json',
kind: 'json',
metadata: '',
name: 'MiniTiledSet.json',
userAdded: true,
alwaysLoaded: true,
},
],
},
});
const runtimeScene = new gdjs.RuntimeScene(runtimeGame);
runtimeScene.loadFromScene({
layers: [
{
name: '',
visibility: true,
effects: [],
cameras: [],
ambientLightColorR: 0,
ambientLightColorG: 0,
ambientLightColorB: 0,
isLightingLayer: false,
followBaseLayerCamera: true,
},
],
variables: [],
r: 0,
v: 0,
b: 0,
mangledName: 'Scene1',
name: 'Scene1',
stopSoundsOnStartup: false,
title: '',
behaviorsSharedData: [],
objects: [],
instances: [],
});
setFramesPerSecond(runtimeScene, framePerSecond);
return runtimeScene;
};
const setFramesPerSecond = (runtimeScene, framePerSecond) => {
runtimeScene._timeManager.getElapsedTime = function () {
return 1000 / framePerSecond;
};
};
const addTileMapCollisionMask = (runtimeScene) => {
const tileMap = new gdjs.TileMapCollisionMaskRuntimeObject(runtimeScene, {
name: 'tilemap',
type: 'TileMap::CollisionMask',
behaviors: [],
effects: [],
content: {
tilemapJsonFile: 'SmallTiledMap.json',
tilesetJsonFile: 'MiniTiledSet.json',
layerIndex: 0,
collisionMaskTag: 'obstacle',
debugMode: false,
fillColor: '#ffffff',
outlineColor: '#ffffff',
fillOpacity: 1,
outlineOpacity: 1,
outlineSize: 1,
},
});
runtimeScene.addObject(tileMap);
return tileMap;
};
const addObject = (runtimeScene) => {
const object = new gdjs.TestRuntimeObject(runtimeScene, {
name: 'obj1',
type: '',
behaviors: [],
effects: [],
variables: [],
});
object.setCustomWidthAndHeight(8, 8);
runtimeScene.addObject(object);
return object;
};
const delay = (ms) => new Promise((res) => setTimeout(res, ms));
let runtimeScene;
/**
* @type {gdjs.TileMapCollisionMaskRuntimeObject}
*/
let tileMap;
beforeEach(async function () {
runtimeScene = createScene();
tileMap = addTileMapCollisionMask(runtimeScene);
// TODO find a clean way to wait for the json to be read.
for (
let index = 0;
index < 200 && tileMap._collisionTileMap.getDimensionX() === 0;
index++
) {
await delay(5);
}
if (tileMap._collisionTileMap.getDimensionX() === 0) {
throw new Error('Timeout reading the tile map JSON file.');
}
});
it('can be measured', function () {
tileMap.setPosition(100, 200);
expect(tileMap.getWidth()).to.be(32);
expect(tileMap.getHeight()).to.be(16);
expect(tileMap.getCenterX()).to.be(16);
expect(tileMap.getCenterY()).to.be(8);
});
/**
* insideObject usually use the AABB of the object.
* But, in case of a tile map, it makes more sense to look each tile individually.
* It returns true when there is an hitbox in the tile.
*/
it('can detect a point inside the collision mask', function () {
tileMap.setPosition(100, 200);
// The point is in the black square with an hitbox.
expect(tileMap.insideObject(104, 204)).to.be(true);
expect(tileMap.isCollidingWithPoint(104, 204)).to.be(true);
// The point is in wite square without any hitbox.
expect(tileMap.insideObject(112, 212)).to.be(false);
expect(tileMap.isCollidingWithPoint(112, 212)).to.be(false);
// The point is in black triangle part of the square that has an hitbox.
expect(tileMap.insideObject(102, 210)).to.be(true);
expect(tileMap.isCollidingWithPoint(102, 210)).to.be(true);
// The point is in white triangle part of the square that has no hitbox.
expect(tileMap.insideObject(106, 214)).to.be(true);
expect(tileMap.isCollidingWithPoint(106, 214)).to.be(false);
});
it('can detect collisions with an object', function () {
tileMap.setPosition(100, 200);
const object = addObject(runtimeScene);
object.setPosition(96, 196);
expect(gdjs.RuntimeObject.collisionTest(object, tileMap, true)).to.be(true);
object.setPosition(90, 190);
expect(gdjs.RuntimeObject.collisionTest(object, tileMap, true)).to.be(
false
);
object.setPosition(115, 207);
expect(gdjs.RuntimeObject.collisionTest(object, tileMap, true)).to.be(true);
object.setPosition(116, 208);
expect(gdjs.RuntimeObject.collisionTest(object, tileMap, true)).to.be(
false
);
});
it('can check collisions with an object on empty tiles without any issue', function () {
tileMap.setPosition(100, 200);
const object = addObject(runtimeScene);
object.setPosition(116, 208);
expect(gdjs.RuntimeObject.collisionTest(object, tileMap, true)).to.be(
false
);
});
it('can detect collisions with an object on flipped tiles', function () {
tileMap.setPosition(100, 200);
const object = addObject(runtimeScene);
// The object is over the black triangle.
object.setPosition(118, 214);
expect(gdjs.RuntimeObject.collisionTest(object, tileMap, true)).to.be(true);
// The object is over the red triangle without touching a black polygon.
object.setPosition(130, 204);
expect(gdjs.RuntimeObject.collisionTest(object, tileMap, true)).to.be(
false
);
});
it("can detect collisions with an object when it's rotated", function () {
tileMap.setPosition(100, 200);
tileMap.setAngle(90);
const object = addObject(runtimeScene);
object.setPosition(123, 185);
expect(gdjs.RuntimeObject.collisionTest(object, tileMap, true)).to.be(true);
object.setPosition(124, 184);
expect(gdjs.RuntimeObject.collisionTest(object, tileMap, true)).to.be(
false
);
});
it('can detect collisions with an object when it has a custom size', function () {
tileMap.setPosition(100, 200);
tileMap.setWidth(2 * tileMap.getWidth());
tileMap.setHeight(2 * tileMap.getHeight());
const object = addObject(runtimeScene);
object.setPosition(163, 231);
expect(gdjs.RuntimeObject.collisionTest(object, tileMap, true)).to.be(true);
object.setPosition(164, 232);
expect(gdjs.RuntimeObject.collisionTest(object, tileMap, true)).to.be(
false
);
});
});

View File

@@ -0,0 +1,472 @@
/// <reference path="helper/TileMapHelper.d.ts" />
namespace gdjs {
const logger = new gdjs.Logger('Tilemap object');
/**
* An object that handle hitboxes for a tile map.
* @extends gdjs.RuntimeObject
*/
export class TileMapCollisionMaskRuntimeObject extends gdjs.RuntimeObject {
private _tilemapJsonFile: string;
private _tilesetJsonFile: string;
private _renderer: gdjs.TileMap.TileMapCollisionMaskRenderer;
_collisionTileMap: gdjs.TileMap.TransformedCollisionTileMap;
/**
* The tiles are filtered according to this tag.
*
* This allows have multiple objects with different usage
* for the same tile map.
* For instance, platforms, jumpthru, ladder, spike, water...
*/
private _collisionMaskTag: string;
private _tileMapManager: gdjs.TileMap.TileMapRuntimeManager;
/**
* When set to true, the hitboxes will be shown.
*/
_debugMode: boolean;
_fillColor: integer;
_outlineColor: integer;
_fillOpacity: float;
_outlineOpacity: float;
_outlineSize: float;
/**
* If the owner moves, the hitboxes vertices
* will have to be transformed again.
*/
private _transformationIsUpToDate: boolean = false;
constructor(runtimeScene: gdjs.RuntimeScene, objectData) {
super(runtimeScene, objectData);
this._tilemapJsonFile = objectData.content.tilemapJsonFile;
this._tilesetJsonFile = objectData.content.tilesetJsonFile;
this._collisionMaskTag = objectData.content.collisionMaskTag;
this._debugMode = objectData.content.debugMode;
this._fillColor = gdjs.rgbOrHexStringToNumber(
objectData.content.fillColor
);
this._outlineColor = gdjs.rgbOrHexStringToNumber(
objectData.content.outlineColor
);
this._fillOpacity = objectData.content.fillOpacity;
this._outlineOpacity = objectData.content.outlineOpacity;
this._outlineSize = objectData.content.outlineSize;
this._tileMapManager = gdjs.TileMap.TileMapRuntimeManager.getManager(
runtimeScene
);
const editableTileMap = new TileMapHelper.EditableTileMap(
1,
1,
0,
0,
new Map()
);
this._collisionTileMap = new gdjs.TileMap.TransformedCollisionTileMap(
editableTileMap,
this._collisionMaskTag
);
this._renderer = new gdjs.TileMap.TileMapCollisionMaskRenderer(
this,
runtimeScene
);
this._updateTileMap();
// *ALWAYS* call `this.onCreated()` at the very end of your object constructor.
this.onCreated();
}
getRendererObject() {
return this._renderer.getRendererObject();
}
getVisibilityAABB() {
return null;
}
updateFromObjectData(oldObjectData: any, newObjectData: any): boolean {
if (
oldObjectData.content.tilemapJsonFile !==
newObjectData.content.tilemapJsonFile
) {
this.setTilemapJsonFile(newObjectData.content.tilemapJsonFile);
}
if (
oldObjectData.content.tilesetJsonFile !==
newObjectData.content.tilesetJsonFile
) {
this.setTilesetJsonFile(newObjectData.content.tilesetJsonFile);
}
if (oldObjectData.content.debugMode !== newObjectData.content.debugMode) {
this.setDebugMode(newObjectData.content.debugMode);
}
if (oldObjectData.content.fillColor !== newObjectData.content.fillColor) {
this.setFillColor(
gdjs.rgbOrHexStringToNumber(newObjectData.content.fillColor)
);
}
if (
oldObjectData.content.outlineColor !==
newObjectData.content.outlineColor
) {
this.setOutlineColor(
gdjs.rgbOrHexStringToNumber(newObjectData.content.outlineColor)
);
}
if (oldObjectData.fillOpacity !== newObjectData.fillOpacity) {
this.setFillOpacity(newObjectData.fillOpacity);
}
if (oldObjectData.outlineOpacity !== newObjectData.outlineOpacity) {
this.setOutlineOpacity(newObjectData.outlineOpacity);
}
if (oldObjectData.outlineSize !== newObjectData.outlineSize) {
this.setOutlineSize(newObjectData.outlineSize);
}
return true;
}
extraInitializationFromInitialInstance(initialInstanceData): void {
if (initialInstanceData.customSize) {
this.setWidth(initialInstanceData.width);
this.setHeight(initialInstanceData.height);
}
}
private _updateTileMap(): void {
this._tileMapManager.getOrLoadTileMap(
this._tilemapJsonFile,
this._tilesetJsonFile,
(tileMap: TileMapHelper.EditableTileMap | null) => {
if (!tileMap) {
// getOrLoadTileMap already log errors.
return;
}
this._collisionTileMap = new gdjs.TileMap.TransformedCollisionTileMap(
tileMap,
this._collisionMaskTag
);
// The tile map polygons always keep the same references.
// It works because the tilemap is never modified.
this.hitBoxes = Array.from(
this._collisionTileMap.getAllHitboxes(this._collisionMaskTag)
);
this._renderer.redrawCollisionMask();
}
);
}
updateHitBoxes(): void {
this.updateTransformation();
// Update the RuntimeObject hitboxes attribute.
for (const hitboxes of this._collisionTileMap.getAllHitboxes(
this._collisionMaskTag
)) {
// RuntimeObject.hitBoxes contains the same polygons instances as the
// hitboxes from the tiles.
//
// When hitboxes for a tile is asked to the model, they are updated
// according to the new object location if needed.
// Iterating over all the tiles forces them to update their hitboxes.
//
// The hitboxes array is built by _updateTileMap().
}
this.hitBoxesDirty = false;
this._renderer.redrawCollisionMask();
this.updateAABB();
}
/**
* Update the affine transformation according to the object position, size
* and angle.
*/
updateTransformation(): void {
if (this._transformationIsUpToDate) {
return;
}
const transformation = this._collisionTileMap.getTransformation();
const absScaleX = Math.abs(this._renderer.getScaleX());
const absScaleY = Math.abs(this._renderer.getScaleY());
transformation.setToIdentity();
// Translation
transformation.translate(this.x, this.y);
// Rotation
const angleInRadians = (this.angle * Math.PI) / 180;
transformation.rotateAround(
angleInRadians,
this.getCenterX() * absScaleX,
this.getCenterY() * absScaleY
);
// Scale
transformation.scale(absScaleX, absScaleY);
this._collisionTileMap.setTransformation(transformation);
this._transformationIsUpToDate = true;
}
/**
* This method is expensive and should not be called.
* Prefer using {@link getHitBoxesAround} rather than getHitBoxes.
*/
getHitBoxes(): gdjs.Polygon[] {
if (this.hitBoxesDirty) {
this.updateHitBoxes();
this.updateAABB();
this.hitBoxesDirty = false;
}
return this.hitBoxes;
}
getHitBoxesAround(left: float, top: float, right: float, bottom: float) {
// This implementation doesn't call updateHitBoxes.
// It's important for good performances because there is no need to
// update the whole collision mask where only a few hitboxes must be
// checked.
this.updateTransformation();
return this._collisionTileMap.getHitboxesAround(
this._collisionMaskTag,
left,
top,
right,
bottom
);
}
/**
* insideObject usually use the AABB of the object.
* But, in case of a tile map, it makes more sense to look each tile individually.
* It returns true when there is an hitbox in the tile.
*/
insideObject(x: float, y: float): boolean {
this.updateTransformation();
// This is more precise than the default implementation.
return this._collisionTileMap.pointIsInsideTile(
x,
y,
this._collisionMaskTag
);
}
// This implementation doesn't use updateHitBoxes.
// It's important for good performances.
updateAABB(): void {
if (this.getAngle() === 0) {
// Fast computation of AABB for non rotated object
this.aabb.min[0] = this.x;
this.aabb.min[1] = this.y;
this.aabb.max[0] = this.aabb.min[0] + this.getWidth();
this.aabb.max[1] = this.aabb.min[1] + this.getHeight();
} else {
const affineTransformation = this._collisionTileMap.getTransformation();
const left = 0;
const right = this._collisionTileMap.getWidth();
const top = 0;
const bottom = this._collisionTileMap.getHeight();
const workingPoint = this.aabb.min;
workingPoint[0] = left;
workingPoint[1] = top;
affineTransformation.transform(workingPoint, workingPoint);
const topLeftX = workingPoint[0];
const topLeftY = workingPoint[1];
workingPoint[0] = right;
workingPoint[1] = top;
affineTransformation.transform(workingPoint, workingPoint);
const topRightX = workingPoint[0];
const topRightY = workingPoint[1];
workingPoint[0] = right;
workingPoint[1] = bottom;
affineTransformation.transform(workingPoint, workingPoint);
const bottomRightX = workingPoint[0];
const bottomRightY = workingPoint[1];
workingPoint[0] = left;
workingPoint[1] = bottom;
affineTransformation.transform(workingPoint, workingPoint);
const bottomLeftX = workingPoint[0];
const bottomLeftY = workingPoint[1];
this.aabb.min[0] = Math.min(
topLeftX,
topRightX,
bottomRightX,
bottomLeftX
);
this.aabb.max[0] = Math.max(
topLeftX,
topRightX,
bottomRightX,
bottomLeftX
);
this.aabb.min[1] = Math.min(
topLeftY,
topRightY,
bottomRightY,
bottomLeftY
);
this.aabb.max[1] = Math.max(
topLeftY,
topRightY,
bottomRightY,
bottomLeftY
);
}
}
/**
* Set the Tilemap json file to display.
*/
setTilemapJsonFile(tilemapJsonFile: string): void {
this._tilemapJsonFile = tilemapJsonFile;
this._updateTileMap();
}
getTilemapJsonFile(): string {
return this._tilemapJsonFile;
}
isTilemapJsonFile(selectedTilemapJsonFile: string): boolean {
return this._tilemapJsonFile === selectedTilemapJsonFile;
}
setTilesetJsonFile(tilesetJsonFile: string) {
this._tilesetJsonFile = tilesetJsonFile;
this._updateTileMap();
}
getTilesetJsonFile(): string {
return this._tilesetJsonFile;
}
isTilesetJsonFile(selectedTilesetJsonFile: string): boolean {
return this._tilesetJsonFile === selectedTilesetJsonFile;
}
/**
* @returns true if the hitboxes are shown.
*/
getDebugMode(): boolean {
return this._debugMode;
}
/**
* @returns true if the hitboxes are shown.
*/
setDebugMode(debugMode: boolean): void {
this._debugMode = debugMode;
this._renderer.redrawCollisionMask();
}
getFillColor(): integer {
return this._fillColor;
}
getOutlineColor(): integer {
return this._outlineColor;
}
setFillColor(fillColor: integer): void {
this._fillColor = fillColor;
}
setOutlineColor(outlineColor: integer): void {
this._outlineColor = outlineColor;
}
setOutlineSize(size: float): void {
this._outlineSize = size;
}
getOutlineSize(): float {
return this._outlineSize;
}
/**
*
* @param opacity from 0 to 255
*/
setFillOpacity(opacity: float): void {
this._fillOpacity = opacity;
}
/**
*
* @returns an opacity value from 0 to 255.
*/
getFillOpacity(): float {
return this._fillOpacity;
}
/**
*
* @param opacity from 0 to 255
*/
setOutlineOpacity(opacity: float): void {
this._outlineOpacity = opacity;
}
/**
*
* @returns an opacity value from 0 to 255.
*/
getOutlineOpacity(): float {
return this._outlineOpacity;
}
setX(x: float): void {
super.setX(x);
this._transformationIsUpToDate = false;
}
setY(y: float): void {
super.setY(y);
this._transformationIsUpToDate = false;
}
setAngle(angle: float): void {
super.setAngle(angle);
this._transformationIsUpToDate = false;
}
// TODO allow size changes from events?
setWidth(width: float): void {
if (this._renderer.getWidth() === width) return;
this._renderer.setWidth(width);
this.hitBoxesDirty = true;
this._transformationIsUpToDate = false;
}
setHeight(height: float): void {
if (this._renderer.getHeight() === height) return;
this._renderer.setHeight(height);
this.hitBoxesDirty = true;
this._transformationIsUpToDate = false;
}
getWidth(): float {
return this._renderer.getWidth();
}
getHeight(): float {
return this._renderer.getHeight();
}
}
gdjs.registerObject(
'TileMap::CollisionMask',
gdjs.TileMapCollisionMaskRuntimeObject
);
TileMapCollisionMaskRuntimeObject.supportsReinitialization = false;
}

View File

@@ -1,3 +1,5 @@
/// <reference path="helper/TileMapHelper.d.ts" />
/// <reference path="pixi-tilemap/dist/pixi-tilemap.d.ts" />
namespace gdjs {
const logger = new gdjs.Logger('Tilemap object');
@@ -10,8 +12,7 @@ namespace gdjs {
_object: any;
_runtimeScene: gdjs.RuntimeScene;
// @ts-ignore - pixi-tilemap types to be added.
_pixiObject: any;
_pixiObject: PIXI.tilemap.CompositeRectTileLayer;
/**
* @param runtimeObject The object to render
@@ -25,10 +26,7 @@ namespace gdjs {
this._runtimeScene = runtimeScene;
// Load (or reset)
if (this._pixiObject === undefined) {
// @ts-ignore - pixi-tilemap types to be added.
this._pixiObject = new PIXI.tilemap.CompositeRectTileLayer(0);
}
this._pixiObject = new PIXI.tilemap.CompositeRectTileLayer(0);
this._pixiObject.tileAnim = [0, 0];
runtimeScene
@@ -37,7 +35,6 @@ namespace gdjs {
.addRendererObject(this._pixiObject, runtimeObject.getZOrder());
this.updateAngle();
this.updateOpacity();
this.updateTileMap();
this.updatePosition();
}
@@ -45,74 +42,21 @@ namespace gdjs {
return this._pixiObject;
}
incrementAnimationFrameX(runtimeScene) {
incrementAnimationFrameX(runtimeScene: gdjs.RuntimeScene) {
this._pixiObject.tileAnim[0] += 1;
}
_loadTileMapWithTileset(tileMapJsonData, tilesetJsonData) {
// @ts-ignore - TODO: Add typings for pixi-tilemap-helper.
const pixiTileMapData = PixiTileMapHelper.loadPixiTileMapData(
(textureName) =>
this._runtimeScene
.getGame()
.getImageManager()
.getPIXITexture(textureName),
tilesetJsonData
? { ...tileMapJsonData, tilesets: [tilesetJsonData] }
: tileMapJsonData,
this._object._tilemapAtlasImage,
this._object._tilemapJsonFile,
this._object._tilesetJsonFile
updatePixiTileMap(
tileMap: TileMapHelper.EditableTileMap,
textureCache: TileMapHelper.TileTextureCache
) {
TileMapHelper.PixiTileMapHelper.updatePixiTileMap(
this._pixiObject,
tileMap,
textureCache,
this._object._displayMode,
this._object._layerIndex
);
if (pixiTileMapData) {
// @ts-ignore - TODO: Add typings for pixi-tilemap-helper.
PixiTileMapHelper.updatePixiTileMap(
this._pixiObject,
pixiTileMapData,
this._object._displayMode,
this._object._layerIndex,
// @ts-ignore - TODO: Add typings for pako.
pako
);
}
}
updateTileMap(): void {
this._runtimeScene
.getGame()
.getJsonManager()
.loadJson(this._object._tilemapJsonFile, (error, tileMapJsonData) => {
if (error) {
logger.error(
'An error happened while loading a Tilemap JSON data:',
error
);
return;
}
if (this._object._tilesetJsonFile) {
this._runtimeScene
.getGame()
.getJsonManager()
.loadJson(
this._object._tilesetJsonFile,
(error, tilesetJsonData) => {
if (error) {
logger.error(
'An error happened while loading Tileset JSON data:',
error
);
return;
}
this._loadTileMapWithTileset(
tileMapJsonData,
tilesetJsonData
);
}
);
} else {
this._loadTileMapWithTileset(tileMapJsonData, null);
}
});
}
updatePosition(): void {
@@ -135,13 +79,13 @@ namespace gdjs {
this._pixiObject.alpha = this._object._opacity / 255;
}
setWidth(width): void {
setWidth(width: float): void {
this._pixiObject.width = width / this._pixiObject.scale.x;
this._pixiObject.pivot.x = width / 2;
this.updatePosition();
}
setHeight(height): void {
setHeight(height: float): void {
this._pixiObject.height = height / this._pixiObject.scale.y;
this._pixiObject.pivot.y = height / 2;
this.updatePosition();

View File

@@ -1,9 +1,10 @@
/// <reference path="helper/TileMapHelper.d.ts" />
namespace gdjs {
import PIXI = GlobalPIXIModule.PIXI;
const logger = new gdjs.Logger('Tilemap object');
/**
* Displays a Tilemap object (mapeditor.org supported).
* @memberof gdjs
* @class TileMapRuntimeObject
* @extends gdjs.RuntimeObject
*/
export class TileMapRuntimeObject extends gdjs.RuntimeObject {
_frameElapsedTime: float = 0;
@@ -15,9 +16,10 @@ namespace gdjs {
_layerIndex: integer;
_animationSpeedScale: number;
_animationFps: number;
_renderer: any;
_tileMapManager: gdjs.TileMap.TileMapRuntimeManager;
_renderer: gdjs.TileMapRuntimeObjectPixiRenderer;
constructor(runtimeScene, objectData) {
constructor(runtimeScene: gdjs.RuntimeScene, objectData) {
super(runtimeScene, objectData);
this._opacity = objectData.content.opacity;
this._tilemapJsonFile = objectData.content.tilemapJsonFile;
@@ -27,18 +29,14 @@ namespace gdjs {
this._layerIndex = objectData.content.layerIndex;
this._animationSpeedScale = objectData.content.animationSpeedScale;
this._animationFps = objectData.content.animationFps;
if (this._renderer) {
gdjs.TileMapRuntimeObjectRenderer.call(
this._renderer,
this,
runtimeScene
);
} else {
this._renderer = new gdjs.TileMapRuntimeObjectRenderer(
this,
runtimeScene
);
}
this._tileMapManager = gdjs.TileMap.TileMapRuntimeManager.getManager(
runtimeScene
);
this._renderer = new gdjs.TileMapRuntimeObjectRenderer(
this,
runtimeScene
);
this._updateTileMap();
// *ALWAYS* call `this.onCreated()` at the very end of your object constructor.
this.onCreated();
@@ -48,14 +46,14 @@ namespace gdjs {
return this._renderer.getRendererObject();
}
update(runtimeScene): void {
update(runtimeScene: gdjs.RuntimeScene): void {
if (this._animationSpeedScale <= 0 || this._animationFps === 0) {
return;
}
const elapsedTime = this.getElapsedTime(runtimeScene) / 1000;
this._frameElapsedTime += elapsedTime * this._animationSpeedScale;
while (this._frameElapsedTime > 1 / this._animationFps) {
this._renderer.incrementAnimationFrameX();
this._renderer.incrementAnimationFrameX(runtimeScene);
this._frameElapsedTime -= 1 / this._animationFps;
}
}
@@ -108,67 +106,101 @@ namespace gdjs {
return true;
}
/**
* Initialize the extra parameters that could be set for an instance.
*/
extraInitializationFromInitialInstance(initialInstanceData) {
extraInitializationFromInitialInstance(initialInstanceData): void {
if (initialInstanceData.customSize) {
this.setWidth(initialInstanceData.width);
this.setHeight(initialInstanceData.height);
}
}
private _updateTileMap(): void {
this._tileMapManager.getOrLoadTileMap(
this._tilemapJsonFile,
this._tilesetJsonFile,
(tileMap: TileMapHelper.EditableTileMap | null) => {
if (!tileMap) {
// getOrLoadTileMap already warn.
return;
}
this._tileMapManager.getOrLoadTextureCache(
(textureName) =>
(this._runtimeScene
.getGame()
.getImageManager()
.getPIXITexture(textureName) as unknown) as PIXI.BaseTexture<
PIXI.Resource
>,
this._tilemapAtlasImage,
this._tilemapJsonFile,
this._tilesetJsonFile,
(textureCache: TileMapHelper.TileTextureCache | null) => {
if (!textureCache) {
// getOrLoadTextureCache already log warns and errors.
return;
}
this._renderer.updatePixiTileMap(tileMap, textureCache);
}
);
}
);
}
/**
* Set the Tilemap json file to display.
*/
setTilemapJsonFile(tilemapJsonFile): void {
setTilemapJsonFile(tilemapJsonFile: string): void {
this._tilemapJsonFile = tilemapJsonFile;
this._renderer.updateTileMap();
this._updateTileMap();
}
getTilemapJsonFile() {
getTilemapJsonFile(): string {
return this._tilemapJsonFile;
}
isTilemapJsonFile(selectedTilemapJsonFile): boolean {
isTilemapJsonFile(selectedTilemapJsonFile: string): boolean {
return this._tilemapJsonFile === selectedTilemapJsonFile;
}
setTilesetJsonFile(tilesetJsonFile) {
setTilesetJsonFile(tilesetJsonFile: string): void {
this._tilesetJsonFile = tilesetJsonFile;
this._renderer.updateTileMap();
this._updateTileMap();
}
getTilesetJsonFile() {
getTilesetJsonFile(): string {
return this._tilesetJsonFile;
}
setAnimationFps(animationFps) {
setAnimationFps(animationFps: float) {
this._animationFps = animationFps;
}
getAnimationFps() {
getAnimationFps(): float {
return this._animationFps;
}
isTilesetJsonFile(selectedTilesetJsonFile) {
isTilesetJsonFile(selectedTilesetJsonFile: string): boolean {
return this._tilesetJsonFile === selectedTilesetJsonFile;
}
isDisplayMode(selectedDisplayMode) {
isDisplayMode(selectedDisplayMode: string): boolean {
return this._displayMode === selectedDisplayMode;
}
setDisplayMode(displayMode): void {
setDisplayMode(displayMode: string): void {
this._displayMode = displayMode;
this._renderer.updateTileMap();
this._updateTileMap();
}
getDisplayMode() {
getDisplayMode(): string {
return this._displayMode;
}
setLayerIndex(layerIndex): void {
this._layerIndex = layerIndex;
this._renderer.updateTileMap();
this._updateTileMap();
}
getLayerIndex() {
getLayerIndex(): integer {
return this._layerIndex;
}
@@ -176,14 +208,10 @@ namespace gdjs {
this._animationSpeedScale = animationSpeedScale;
}
getAnimationSpeedScale() {
getAnimationSpeedScale(): float {
return this._animationSpeedScale;
}
/**
* Set the width of the object.
* @param width The new width.
*/
setWidth(width: float): void {
if (this._renderer.getWidth() === width) return;
@@ -191,10 +219,6 @@ namespace gdjs {
this.hitBoxesDirty = true;
}
/**
* Set the height of the object.
* @param height The new height.
*/
setHeight(height: float): void {
if (this._renderer.getHeight() === height) return;
@@ -202,28 +226,16 @@ namespace gdjs {
this.hitBoxesDirty = true;
}
/**
* Set object position on X axis.
* @param x The new position X of the object.
*/
setX(x: float): void {
super.setX(x);
this._renderer.updatePosition();
}
/**
* Set object position on Y axis.
* @param y The new position Y of the object.
*/
setY(y: float): void {
super.setY(y);
this._renderer.updatePosition();
}
/**
* Set the angle of the object.
* @param angle The new angle of the object.
*/
setAngle(angle: float): void {
super.setAngle(angle);
this._renderer.updateAngle();
@@ -241,20 +253,14 @@ namespace gdjs {
/**
* Get object opacity.
*/
getOpacity() {
getOpacity(): float {
return this._opacity;
}
/**
* Get the width of the object.
*/
getWidth(): float {
return this._renderer.getWidth();
}
/**
* Get the height of the object.
*/
getHeight(): float {
return this._renderer.getHeight();
}

View File

@@ -553,6 +553,7 @@ void ExporterHelper::AddLibsInclude(bool pixiRenderers,
InsertUnique(includesFiles, "oncetriggers.js");
InsertUnique(includesFiles, "runtimebehavior.js");
InsertUnique(includesFiles, "spriteruntimeobject.js");
InsertUnique(includesFiles, "affinetransformation.js");
// Common includes for events only.
InsertUnique(includesFiles, "events-tools/commontools.js");

View File

@@ -0,0 +1,493 @@
namespace gdjs {
/**
* An affine transformation that can transform points.
*/
export class AffineTransformation {
private matrix: Float32Array;
/**
* Initialize to the identity.
*/
constructor() {
// | 1 0 0 |
// | 0 1 0 |
// | 0 0 1 |
this.matrix = new Float32Array([1, 0, 0, 1, 0, 0]);
}
/**
* Reset to the identity.
*/
setToIdentity() {
const matrix = this.matrix;
// | 1 0 0 |
// | 0 1 0 |
// | 0 0 1 |
matrix[0] = 1;
matrix[1] = 0;
matrix[2] = 0;
matrix[3] = 1;
matrix[4] = 0;
matrix[5] = 0;
}
/**
* Check if this transformation is the identity.
*/
isIdentity(): boolean {
const matrix = this.matrix;
return (
matrix[0] === 1 &&
matrix[1] === 0 &&
matrix[2] === 0 &&
matrix[3] === 1 &&
matrix[4] === 0 &&
matrix[5] === 0
);
}
/**
* Check if this is equals to another transformation.
* @param other The transformation to check.
*/
equals(other: AffineTransformation): boolean {
const matrix = this.matrix;
const otherMatrix = other.matrix;
return (
this === other ||
(matrix[0] === otherMatrix[0] &&
matrix[1] === otherMatrix[1] &&
matrix[2] === otherMatrix[2] &&
matrix[3] === otherMatrix[3] &&
matrix[4] === otherMatrix[4] &&
matrix[5] === otherMatrix[5])
);
}
/**
* Check if this is almost equals to another transformation.
* @param other The transformation to check.
* @param epsilon The relative margin error.
*/
nearlyEquals(other: AffineTransformation, epsilon: float): boolean {
const matrix = this.matrix;
const otherMatrix = other.matrix;
return (
this === other ||
(gdjs.nearlyEqual(matrix[0], otherMatrix[0], epsilon) &&
gdjs.nearlyEqual(matrix[1], otherMatrix[1], epsilon) &&
gdjs.nearlyEqual(matrix[2], otherMatrix[2], epsilon) &&
gdjs.nearlyEqual(matrix[3], otherMatrix[3], epsilon) &&
gdjs.nearlyEqual(matrix[4], otherMatrix[4], epsilon) &&
gdjs.nearlyEqual(matrix[5], otherMatrix[5], epsilon))
);
}
/**
* Copy a transformation.
* @param other The transformation to copy.
*/
copyFrom(other: AffineTransformation) {
const matrix = this.matrix;
const otherMatrix = other.matrix;
matrix[0] = otherMatrix[0];
matrix[1] = otherMatrix[1];
matrix[2] = otherMatrix[2];
matrix[3] = otherMatrix[3];
matrix[4] = otherMatrix[4];
matrix[5] = otherMatrix[5];
return this;
}
/**
* Reset to a translation.
*
* @param x The horizontal translation value.
* @param y The vertical translation value.
*/
setToTranslation(tx: float, ty: float) {
const matrix = this.matrix;
// | m0 m2 m4 | | 1 0 tx |
// | m1 m3 m5 | = | 0 1 ty |
// | 0 0 1 | | 0 0 1 |
matrix[0] = 1;
matrix[1] = 0;
matrix[2] = 0;
matrix[3] = 1;
matrix[4] = tx;
matrix[5] = ty;
}
/**
* Concatenate a translation.
*
* @param tx The horizontal translation value.
* @param ty The vertical translation value.
*/
translate(tx: float, ty: float) {
var matrix = this.matrix;
// 1 0 tx
// 0 1 ty
// 0 0 1
// m0 m2 m4
// m1 m3 m5
// 0 0 1
matrix[4] = matrix[0] * tx + matrix[2] * ty + matrix[4];
matrix[5] = matrix[1] * tx + matrix[3] * ty + matrix[5];
}
/**
* Reset to a scale.
*
* @param sx The horizontal scale value.
* @param sy The vertical scale value.
*/
setToScale(sx: float, sy: float) {
const matrix = this.matrix;
// | m0 m2 m4 | | sx 0 0 |
// | m1 m3 m5 | = | 0 sy 0 |
// | 0 0 1 | | 0 0 1 |
matrix[0] = sx;
matrix[1] = 0;
matrix[2] = 0;
matrix[3] = sy;
matrix[4] = 0;
matrix[5] = 0;
}
/**
* Concatenate a scale.
*
* @param sx The horizontal scale value.
* @param sy The vertical scale value.
*/
scale(sx: float, sy: float) {
const matrix = this.matrix;
// sx 0 0
// 0 sy 0
// 0 0 1
// m0 m2 m4
// m1 m3 m5
// 0 0 1
matrix[0] *= sx;
matrix[1] *= sx;
matrix[2] *= sy;
matrix[3] *= sy;
}
/**
* Reset to a rotation.
*
* @param angle The angle of rotation in radians.
*/
setToRotation(theta: float) {
const matrix = this.matrix;
let cost = Math.cos(theta);
let sint = Math.sin(theta);
// Avoid rounding errors around 0.
if (cost === -1 || cost === 1) {
sint = 0;
}
if (sint === -1 || sint === 1) {
cost = 0;
}
// | m0 m2 m4 | | cost -sint 0 |
// | m1 m3 m5 | = | sint cost 0 |
// | 0 0 1 | | 0 0 1 |
matrix[0] = cost;
matrix[1] = sint;
matrix[2] = -sint;
matrix[3] = cost;
matrix[4] = 0;
matrix[5] = 0;
}
/**
* Concatenate a rotation.
*
* @param angle The angle of rotation in radians.
*/
rotate(angle: float) {
const matrix = this.matrix;
let cost = Math.cos(angle);
let sint = Math.sin(angle);
// Avoid rounding errors around 0.
if (cost === -1 || cost === 1) {
sint = 0;
}
if (sint === -1 || sint === 1) {
cost = 0;
}
// cost -sint 0
// sint cost 0
// 0 0 1
// m0 m2 m4
// m1 m3 m5
// 0 0 1
const m0 = matrix[0];
const m1 = matrix[1];
const m2 = matrix[2];
const m3 = matrix[3];
matrix[0] = m0 * cost + m2 * sint;
matrix[1] = m1 * cost + m3 * sint;
matrix[2] = m0 * -sint + m2 * cost;
matrix[3] = m1 * -sint + m3 * cost;
}
/**
* Reset to a rotation.
*
* @param angle The angle of rotation in radians.
* @param anchorX The rotation anchor point X.
* @param anchorY The rotation anchor point Y.
*/
setToRotationAround(angle: float, anchorX: float, anchorY: float) {
const matrix = this.matrix;
let cost = Math.cos(angle);
let sint = Math.sin(angle);
// Avoid rounding errors around 0.
if (cost === -1 || cost === 1) {
sint = 0;
}
if (sint === -1 || sint === 1) {
cost = 0;
}
// | m0 m2 m4 | | cost -sint x-x*cost+y*sint |
// | m1 m3 m5 | = | sint cost y-x*sint-y*cost |
// | 0 0 1 | | 0 0 1 |
matrix[0] = cost;
matrix[1] = sint;
matrix[2] = -sint;
matrix[3] = cost;
matrix[4] = anchorX - anchorX * cost + anchorY * sint;
matrix[5] = anchorY - anchorX * sint + anchorY * cost;
}
/**
* Concatenate a rotation.
*
* @param angle The angle of rotation in radians.
* @param anchorX The rotation anchor point X.
* @param anchorY The rotation anchor point Y.
*/
rotateAround(angle: float, anchorX: float, anchorY: float) {
this.translate(anchorX, anchorY);
this.rotate(angle);
// First: translate anchor to origin
this.translate(-anchorX, -anchorY);
}
/**
* Reset to an horizontal flip.
*
* @param anchorX The flip anchor point X.
*/
setToFlipX(anchorX: float) {
const matrix = this.matrix;
// | m0 m2 m4 | | -1 0 2x |
// | m1 m3 m5 | = | 0 1 0 |
// | 0 0 1 | | 0 0 1 |
matrix[0] = -1;
matrix[1] = 0;
matrix[2] = 0;
matrix[3] = 1;
matrix[4] = 2 * anchorX;
matrix[5] = 0;
}
/**
* Concatenate an horizontal flip.
*
* @param anchorX The flip anchor point X.
*/
flipX(anchorX: float) {
this.translate(anchorX, 0);
this.scale(-1, 1);
// First: translate anchor to origin
this.translate(-anchorX, 0);
}
/**
* Reset to an vertical flip.
*
* @param anchorY The flip anchor point Y.
*/
setToFlipY(anchorY: float) {
const matrix = this.matrix;
// | m0 m2 m4 | | 1 0 0 |
// | m1 m3 m5 | = | 0 -1 2x |
// | 0 0 1 | | 0 0 1 |
matrix[0] = -1;
matrix[1] = 0;
matrix[2] = 0;
matrix[3] = 1;
matrix[4] = 0;
matrix[5] = 2 * anchorY;
}
/**
* Concatenate an vertical flip.
*
* @param anchorY The flip anchor point Y.
*/
flipY(anchorY: float) {
this.translate(0, anchorY);
this.scale(1, -1);
// First: translate anchor to origin
this.translate(0, -anchorY);
}
/**
* Concatenate a flip between X and Y.
*/
flipDiagonally() {
const matrix = this.matrix;
const m0 = matrix[0];
const m1 = matrix[1];
const m2 = matrix[2];
const m3 = matrix[3];
const m4 = matrix[4];
const m5 = matrix[5];
matrix[0] = m1;
matrix[1] = m0;
matrix[2] = m3;
matrix[3] = m2;
matrix[4] = m5;
matrix[5] = m4;
}
/**
* Concatenate a transformation after this one.
* @param other The transformation to concatenate.
*/
concatenate(other: AffineTransformation) {
const matrix = this.matrix;
const otherMatrix = other.matrix;
const m0 = matrix[0];
const m1 = matrix[1];
const m2 = matrix[2];
const m3 = matrix[3];
const m4 = matrix[4];
const m5 = matrix[5];
const o0 = otherMatrix[0];
const o1 = otherMatrix[1];
const o2 = otherMatrix[2];
const o3 = otherMatrix[3];
const o4 = otherMatrix[4];
const o5 = otherMatrix[5];
// o0 o2 o4
// o1 o3 o5
// 0 0 1
// m0 m2 m4
// m1 m3 m5
// 0 0 1
matrix[0] = o0 * m0 + o1 * m2;
matrix[1] = o0 * m1 + o1 * m3;
matrix[2] = o2 * m0 + o3 * m2;
matrix[3] = o2 * m1 + o3 * m3;
matrix[4] = o4 * m0 + o5 * m2 + m4;
matrix[5] = o4 * m1 + o5 * m3 + m5;
}
/**
* Concatenate a transformation before this one.
* @param other The transformation to concatenate.
*/
preConcatenate(other: AffineTransformation) {
const matrix = this.matrix;
const otherMatrix = other.matrix;
const m0 = matrix[0];
const m1 = matrix[1];
const m2 = matrix[2];
const m3 = matrix[3];
const m4 = matrix[4];
const m5 = matrix[5];
const o0 = otherMatrix[0];
const o1 = otherMatrix[1];
const o2 = otherMatrix[2];
const o3 = otherMatrix[3];
const o4 = otherMatrix[4];
const o5 = otherMatrix[5];
// m0 m2 m4
// m1 m3 m5
// 0 0 1
// o0 o2 o4
// o1 o3 o5
// 0 0 1
matrix[0] = m0 * o0 + m1 * o2;
matrix[1] = m0 * o1 + m1 * o3;
matrix[2] = m2 * o0 + m3 * o2;
matrix[3] = m2 * o1 + m3 * o3;
matrix[4] = m4 * o0 + m5 * o2 + o4;
matrix[5] = m4 * o1 + m5 * o3 + o5;
}
/**
* Transform a point.
*
* @param source The point to transform.
* @param destination The Point to store the transformed coordinates.
*/
transform(source: FloatPoint, destination: FloatPoint) {
const matrix = this.matrix;
// x
// y
// 1
// m0 m2 m4
// m1 m3 m5
// 0 0 1
const x = matrix[0] * source[0] + matrix[2] * source[1] + matrix[4];
const y = matrix[1] * source[0] + matrix[3] * source[1] + matrix[5];
destination[0] = x;
destination[1] = y;
}
/**
* Invert the matrix.
*/
invert() {
const matrix = this.matrix;
const m0 = matrix[0];
const m1 = matrix[1];
const m2 = matrix[2];
const m3 = matrix[3];
const m4 = matrix[4];
const m5 = matrix[5];
const n = m0 * m3 - m1 * m2;
matrix[0] = m3 / n;
matrix[1] = -m1 / n;
matrix[2] = -m2 / n;
matrix[3] = m0 / n;
matrix[4] = (m2 * m5 - m3 * m4) / n;
matrix[5] = -(m0 * m5 - m1 * m4) / n;
return this;
}
toString() {
const matrix = this.matrix;
return `[[${matrix[0]} ${matrix[1]}] [${matrix[2]} ${matrix[3]}] [${matrix[4]} ${matrix[5]}]]`;
}
}
}

View File

@@ -95,6 +95,15 @@ namespace gdjs {
return hexToRGBColor(value);
};
/**
* Convert a RGB string ("rrr;ggg;bbb") or a Hex string ("#rrggbb") to a RGB color number.
* @param rgbOrHexString The color as a RGB string or Hex string
*/
export const rgbOrHexStringToNumber = (rgbOrHexString: string): integer => {
const components = gdjs.rgbOrHexToRGBColor(rgbOrHexString);
return gdjs.rgbToHexNumber(components[0], components[1], components[2]);
};
/**
* Convert a RGB object to a Hex number.
* @param r Red
@@ -524,6 +533,31 @@ namespace gdjs {
hex[r[15]]
);
};
/**
* See https://floating-point-gui.de/errors/comparison/
* @param a
* @param b
* @param epsilon the relative margin error
* @returns true when a and b are within a relative margin error.
*/
export const nearlyEqual = (a: float, b: float, epsilon: float): boolean => {
const absA = Math.abs(a);
const absB = Math.abs(b);
const diff = Math.abs(a - b);
if (a === b) {
// shortcut, handles infinities
return true;
} else if (a == 0 || b == 0 || absA + absB < Number.EPSILON) {
// a or b is zero or both are extremely close to it
// relative error is less meaningful here
return diff < epsilon * Number.EPSILON;
} else {
// use relative error
return diff / Math.min(absA + absB, Number.MAX_VALUE) < epsilon;
}
};
}
//Make sure console.warn and console.error are available.

View File

@@ -66,6 +66,7 @@ module.exports = function (config) {
'../../newIDE/app/resources/GDJS/Runtime/events-tools/stringtools.js',
'../../newIDE/app/resources/GDJS/Runtime/events-tools/windowtools.js',
'../../newIDE/app/resources/GDJS/Runtime/debugger-client/hot-reloader.js',
'../../newIDE/app/resources/GDJS/Runtime/affinetransformation.js',
//Extensions:
'../../newIDE/app/resources/GDJS/Runtime/Extensions/DraggableBehavior/draggableruntimebehavior.js',
@@ -93,7 +94,15 @@ module.exports = function (config) {
'../../newIDE/app/resources/GDJS/Runtime/Extensions/Firebase/B_firebasetools/*.js',
'../../newIDE/app/resources/GDJS/Runtime/Extensions/Effects/kawase-blur-pixi-filter.js',
'../../newIDE/app/resources/GDJS/Runtime/Extensions/Effects/pixi-filters/filter-kawase-blur.js',
'../../newIDE/app/resources/GDJS/Runtime/Extensions/TileMap/tilemapcollisionmaskruntimeobject.js',
'../../newIDE/app/resources/GDJS/Runtime/Extensions/TileMap/TileMapRuntimeManager.js',
'../../newIDE/app/resources/GDJS/Runtime/Extensions/TileMap/tilemapruntimeobject-pixi-renderer.js',
'../../newIDE/app/resources/GDJS/Runtime/Extensions/TileMap/tilemapruntimeobject.js',
'../../newIDE/app/resources/GDJS/Runtime/Extensions/TileMap/collision/TileMapCollisionMaskRenderer.js',
'../../newIDE/app/resources/GDJS/Runtime/Extensions/TileMap/collision/TransformedTileMap.js',
'../../newIDE/app/resources/GDJS/Runtime/Extensions/TileMap/helper/TileMapHelper.js',
'../../newIDE/app/resources/GDJS/Runtime/Extensions/TileMap/pako/dist/pako.min.js',
// Test extensions:
'./tests/Extensions/**.js',
@@ -112,6 +121,13 @@ module.exports = function (config) {
served: true,
nocache: false,
},
{
pattern: './tests-utils/simple-tiled-map/*.json',
watched: false,
included: false,
served: true,
nocache: false,
},
...testFiles,
...(config.enableBenchmarks ? benchmarkFiles : []),

View File

@@ -0,0 +1,234 @@
{ "columns":2,
"image":"MiniTiledSet.png",
"imageheight":24,
"imagewidth":16,
"margin":0,
"name":"new tileset",
"spacing":0,
"tilecount":6,
"tiledversion":"1.7.2",
"tileheight":8,
"tiles":[
{
"id":0,
"objectgroup":
{
"draworder":"index",
"name":"",
"objects":[
{
"height":8,
"id":1,
"name":"",
"rotation":0,
"class":"",
"visible":true,
"width":8,
"x":0,
"y":0
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
},
"class":"obstacle"
},
{
"id":2,
"objectgroup":
{
"draworder":"index",
"name":"",
"objects":[
{
"height":0,
"id":1,
"name":"",
"polygon":[
{
"x":0,
"y":0
},
{
"x":8,
"y":-8
},
{
"x":0,
"y":-8
}],
"rotation":0,
"class":"",
"visible":true,
"width":0,
"x":0,
"y":8
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
},
"class":"obstacle"
},
{
"id":3,
"objectgroup":
{
"draworder":"index",
"id":2,
"name":"",
"objects":[
{
"height":0,
"id":1,
"name":"",
"polygon":[
{
"x":0,
"y":0
},
{
"x":4,
"y":4
},
{
"x":8,
"y":0
}],
"rotation":0,
"class":"",
"visible":true,
"width":0,
"x":0,
"y":0
},
{
"height":0,
"id":3,
"name":"",
"polygon":[
{
"x":0,
"y":0
},
{
"x":4,
"y":4
},
{
"x":8,
"y":0
}],
"rotation":180,
"class":"",
"visible":true,
"width":0,
"x":8,
"y":8
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
},
"class":"obstacle"
},
{
"id":4,
"objectgroup":
{
"draworder":"index",
"id":2,
"name":"",
"objects":[
{
"height":0,
"id":1,
"name":"",
"polygon":[
{
"x":0,
"y":0
},
{
"x":0,
"y":8
},
{
"x":8,
"y":0
}],
"rotation":0,
"class":"obstacle",
"visible":true,
"width":0,
"x":0,
"y":0
},
{
"height":0,
"id":2,
"name":"",
"polygon":[
{
"x":0,
"y":0
},
{
"x":0,
"y":8
},
{
"x":8,
"y":0
}],
"rotation":180,
"class":"lava",
"visible":true,
"width":0,
"x":8,
"y":8
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
}
},
{
"id":5,
"objectgroup":
{
"draworder":"index",
"id":2,
"name":"",
"objects":[
{
"height":8,
"id":1,
"name":"",
"rotation":0,
"class":"",
"visible":true,
"width":8,
"x":0,
"y":0
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
},
"class":"lava"
}],
"tilewidth":8,
"type":"tileset",
"version":"1.6"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 B

View File

@@ -0,0 +1,32 @@
{ "compressionlevel":-1,
"height":2,
"infinite":false,
"layers":[
{
"data":[1, 3, 4, 5, 3, 2, 0, 1073741829],
"height":2,
"id":1,
"name":"Tile Layer 1",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":4,
"x":0,
"y":0
}],
"nextlayerid":2,
"nextobjectid":1,
"orientation":"orthogonal",
"renderorder":"right-down",
"tiledversion":"1.7.2",
"tileheight":8,
"tilesets":[
{
"firstgid":1,
"source":"MiniTiledSet.json"
}],
"tilewidth":8,
"type":"map",
"version":"1.6",
"width":4
}

View File

@@ -0,0 +1,58 @@
describe('gdjs.AffineTransformation', function () {
const epsilon = 1 / (2 << 16);
it('can conserve identity through identity', function () {
const identityA = new gdjs.AffineTransformation();
const identityB = new gdjs.AffineTransformation();
expect(identityA.equals(identityB)).to.be(true);
identityB.setToIdentity();
expect(identityA.equals(identityB)).to.be(true);
identityB.concatenate(identityA);
expect(identityA.equals(identityB)).to.be(true);
});
it('can compose translations', function () {
const translationA = new gdjs.AffineTransformation();
translationA.setToTranslation(12 + 45, 67 + 89);
const translationB = new gdjs.AffineTransformation();
translationB.setToTranslation(12, 67);
translationB.translate(45, 89);
expect(translationA.equals(translationB)).to.be(true);
});
it('can compose rotations', function () {
const rotationA = new gdjs.AffineTransformation();
rotationA.setToRotation(Math.PI / 3 + Math.PI / 2);
const rotationB = new gdjs.AffineTransformation();
rotationB.setToRotation(Math.PI / 3);
rotationB.rotate(Math.PI / 2);
expect(rotationA.nearlyEquals(rotationB, epsilon)).to.be(true);
});
it('can do exact 90° rotation transformations', function () {
const rotationA = new gdjs.AffineTransformation();
rotationA.setToRotation(90 * Math.PI / 180);
const result = [10, 5];
rotationA.transform(result, result);
expect(result).to.eql([-5, 10]);
});
it('can compose scales', function () {
const scaleA = new gdjs.AffineTransformation();
scaleA.setToScale(2 * 4, 3 * 5);
const scaleB = new gdjs.AffineTransformation();
scaleB.setToScale(2, 3);
scaleB.scale(4, 5);
expect(scaleA.equals(scaleB)).to.be(true);
});
});

4
SharedLibs/TileMapHelper/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
/dist/
/coverage/
/node_modules/
/package-lock.json

View File

@@ -0,0 +1,3 @@
This library sources is used by the [tile map extension](../../Extensions/TileMap/).
The `npm run build` command copy the bundled library at the right place for the extension to use it.

View File

@@ -0,0 +1,42 @@
module.exports = function (config) {
config.set({
frameworks: ['mocha', 'karma-typescript'],
browserNoActivityTimeout: 400000,
client: {
mocha: {
reporter: 'html',
timeout: 10000,
},
},
files: [
{ pattern: "node_modules/expect.js/index.js" },
{ pattern: "./src/tiled/**/*.ts" },
{ pattern: "./src/model/**/*.ts" }
],
preprocessors: {
"**/*.ts": 'karma-typescript'
},
reporters: ['dots', 'karma-typescript'],
singleRun: true,
karmaTypescriptConfig: {
compilerOptions: {
module: "commonjs",
noImplicitAny: true,
outDir: "tmp",
target: "ES5",
sourceMap: true,
types : [
"mocha",
"expect.js",
"offscreencanvas"
],
lib: ["DOM", "ES5", "ES6"],
"esModuleInterop": false,
"downlevelIteration": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
},
exclude: ["node_modules"]
}
});
};

View File

@@ -0,0 +1,59 @@
{
"name": "tilemap",
"version": "0.0.0",
"license": "MIT",
"description": "",
"main": "dist/TileMapHelper.js",
"types": "dist/index.d.ts",
"files": [
"src",
"dist"
],
"scripts": {
"test": "karma start --browsers ChromeHeadless --single-run",
"test:watch": "karma start --browsers ChromeHeadless",
"test:firefox": "karma start --browsers Firefox --single-run",
"test:firefox:watch": "karma start --browsers Firefox",
"tsc": "tsc",
"build": "rollup -c",
"dev": "rollup -c -w",
"prepublishOnly": "npm run build",
"format": "prettier --write \"src/**/*.ts\"",
"check-format": "prettier --list-different \"src/**/*.ts\""
},
"dependencies": {
"@types/offscreencanvas": "^2019.6.4",
"pako": "^2.0.4",
"pixi.js": "^6.1.2"
},
"devDependencies": {
"@rollup/plugin-node-resolve": "^13.1.3",
"@rollup/plugin-typescript": "^8.3.3",
"@types/expect.js": "^0.3.29",
"@types/mocha": "^5.2.7",
"expect.js": "^0.3.1",
"karma": "^6.1.0",
"karma-chrome-launcher": "^3.1.0",
"karma-firefox-launcher": "^2.0.0",
"karma-mocha": "^1.3.0",
"karma-typescript": "latest",
"mocha": "^6.2.0",
"prettier": "2.1.2",
"rollup": "^2.66.1",
"rollup-plugin-terser": "^7.0.2",
"ts-loader": "^9.2.3",
"tslib": "^2.3.1",
"typescript": "latest"
},
"repository": {
"type": "git",
"url": ""
},
"keywords": [],
"author": "",
"contributors": [],
"bugs": {
"url": ""
},
"homepage": ""
}

View File

@@ -0,0 +1,30 @@
//import pkg from "./package.json";
import typescript from "@rollup/plugin-typescript";
import resolve from '@rollup/plugin-node-resolve';
import { terser } from 'rollup-plugin-terser';
export default [
{
input: './src/index.ts',
output: [
{
name: 'TileMapHelper',
format: 'umd',
file: '../../Extensions/TileMap/helper/TileMapHelper.js',
sourcemap: true,
plugins: [terser({
format: {
comments: false
},
})]
},
],
external: ['pixi.js'],
plugins: [
resolve({
extensions: ['.js'],
}),
typescript({ tsconfig: './tsconfig.json' }),
],
},
];

View File

@@ -0,0 +1,28 @@
/**
* @packageDocumentation
* @module TileMapHelper
*/
import { TiledMap, TiledTileset } from "./tiled/TiledFormat";
import {
EditableTileMap,
EditableTileMapLayer,
TileDefinition,
} from "./model/TileMapModel";
import { TileMapManager } from "./render/TileMapManager";
import { TileTextureCache } from "./render/TileTextureCache";
import { PixiTileMapHelper } from "./render/TileMapPixiHelper";
export * from "./model/CommonTypes";
export { EditableTileMap };
export { EditableTileMapLayer };
export { TileDefinition };
export { TiledMap };
export { TiledTileset };
export { TileMapManager };
export { TileTextureCache };
export { PixiTileMapHelper };

View File

@@ -0,0 +1,5 @@
export declare type integer = number;
export declare type float = number;
export type FloatPoint = [float, float];
export type PolygonVertices = FloatPoint[];

View File

@@ -0,0 +1,608 @@
import { PolygonVertices, integer, float } from "./CommonTypes";
/**
* A tile map model.
*
* Tile map files are parsed into this model by {@link TiledTileMapLoader}.
* This model is used for rending ({@link TileMapRuntimeObjectPixiRenderer})
* and hitboxes handling ({@link TransformedCollisionTileMap}).
* This allows to support new file format with only a new parser.
*/
export class EditableTileMap {
private _tileSet: Map<integer, TileDefinition>;
private _layers: Array<AbstractEditableLayer>;
/**
* The width of a tile.
*/
private readonly tileWidth: integer;
/**
* The height of a tile.
*/
private readonly tileHeight: integer;
/**
* The number of tile columns in the map.
*/
private readonly dimX: integer;
/**
* The number of tile rows in the map.
*/
private readonly dimY: integer;
/**
* @param tileWidth The width of a tile.
* @param tileHeight The height of a tile.
* @param dimX The number of tile columns in the map.
* @param dimY The number of tile rows in the map.
* @param tileSet The tile set.
*/
constructor(
tileWidth: integer,
tileHeight: integer,
dimX: integer,
dimY: integer,
// TODO should the tile set be built internally?
// It's not meant to change and it avoid to do a copy.
tileSet: Map<integer, TileDefinition>
) {
this.tileWidth = tileWidth;
this.tileHeight = tileHeight;
this.dimX = dimX;
this.dimY = dimY;
this._tileSet = tileSet;
this._layers = [];
}
/**
* @returns The tile map width in pixels.
*/
getWidth(): integer {
return this.tileWidth * this.dimX;
}
/**
* @returns The tile map height in pixels.
*/
getHeight(): integer {
return this.tileHeight * this.dimY;
}
/**
* @returns The tile width in pixels.
*/
getTileHeight(): integer {
return this.tileWidth;
}
/**
* @returns The tile height in pixels.
*/
getTileWidth(): integer {
return this.tileHeight;
}
/**
* @returns The number of tile columns in the map.
*/
getDimensionX(): integer {
return this.dimX;
}
/**
* @returns The number of tile rows in the map.
*/
getDimensionY(): integer {
return this.dimY;
}
/**
* @param tileId The tile identifier
* @returns The tile definition form the tile set.
*/
getTileDefinition(tileId: integer): TileDefinition | undefined {
return this._tileSet.get(tileId);
}
/**
* @returns All the tile definitions form the tile set.
*/
getTileDefinitions(): Iterable<TileDefinition> {
return this._tileSet.values();
}
/**
* @param id The identifier of the new layer.
* @returns The new layer.
*/
addTileLayer(id: integer): EditableTileMapLayer {
const layer = new EditableTileMapLayer(this, id);
this._layers.push(layer);
return layer;
}
/**
* @param id The identifier of the new layer.
* @returns The new layer.
*/
addObjectLayer(id: integer): EditableObjectLayer {
const layer = new EditableObjectLayer(this, id);
this._layers.push(layer);
return layer;
}
/**
* @returns All the layers of the tile map.
*/
getLayers(): Iterable<AbstractEditableLayer> {
return this._layers;
}
/**
* Check if a point is inside a tile with a given tag.
*
* It doesn't use the tile hitboxes.
* It only check the point is inside the tile square.
*
* @param x The X coordinate of the point to check.
* @param y The Y coordinate of the point to check.
* @param tag The tile tag
* @returns true when the point is inside a tile with a given tag.
*/
pointIsInsideTile(x: float, y: float, tag: string): boolean {
const indexX = Math.floor(x / this.tileWidth);
const indexY = Math.floor(y / this.tileHeight);
for (const layer of this._layers) {
const tileLayer = layer as EditableTileMapLayer;
if (!tileLayer) {
continue;
}
const tileId = tileLayer.get(indexX, indexY);
if (tileId === undefined) {
return false;
}
const tileDefinition = this._tileSet.get(tileId);
if (tileDefinition!.hasTag(tag)) {
return true;
}
}
return false;
}
}
/**
* A tile map layer.
*/
abstract class AbstractEditableLayer {
/**
* The layer tile map.
*/
readonly tileMap: EditableTileMap;
/**
* The layer identifier.
*/
readonly id: integer;
private visible: boolean = true;
/**
* @param tileMap The layer tile map.
* @param id The layer identifier.
*/
constructor(tileMap: EditableTileMap, id: integer) {
this.tileMap = tileMap;
this.id = id;
}
setVisible(visible: boolean): void {
this.visible = visible;
}
/**
* @returns true if the layer is visible.
*/
isVisible(): boolean {
return this.visible;
}
}
/**
* A layer where tiles are placed with pixel coordinates.
*/
export class EditableObjectLayer extends AbstractEditableLayer {
readonly objects: TileObject[];
/**
* @param tileMap The layer tile map.
* @param id The layer identifier.
*/
constructor(tileMap: EditableTileMap, id: integer) {
super(tileMap, id);
this.objects = [];
}
add(object: TileObject): void {
this.objects.push(object);
}
}
/**
* A tile that is placed with pixel coordinates.
*/
export class TileObject {
/**
* The tile identifier in the tile set.
*/
private tileId: integer;
/**
* The coordinate of the tile left side.
*/
readonly x: float;
/**
* The coordinate of the tile top side.
*/
readonly y: float;
/**
* @param x The coordinate of the tile left side.
* @param y The coordinate of the tile top side.
* @param tileId The tile identifier in the tile set.
*/
constructor(x: float, y: float, tileId: integer) {
this.tileId = tileId;
this.x = x;
this.y = y;
}
/**
* @return The tile identifier in the tile set.
*/
getTileId(): integer {
return FlippingHelper.getTileId(this.tileId);
}
setFlippedHorizontally(flippedHorizontally: boolean): void {
this.tileId = FlippingHelper.setFlippedHorizontally(
this.tileId,
flippedHorizontally
);
}
setFlippedVertically(flippedVertically: boolean): void {
this.tileId = FlippingHelper.setFlippedVertically(
this.tileId,
flippedVertically
);
}
setFlippedDiagonally(flippedDiagonally: boolean): void {
this.tileId = FlippingHelper.setFlippedDiagonally(
this.tileId,
flippedDiagonally
);
}
/**
* @returns true if the tile is flipped horizontally.
*/
isFlippedHorizontally(): boolean {
return FlippingHelper.isFlippedHorizontally(this.tileId);
}
/**
* @returns true if the tile is flipped vertically.
*/
isFlippedVertically(): boolean {
return FlippingHelper.isFlippedVertically(this.tileId);
}
/**
* @returns true if the tile is flipped diagonally.
*/
isFlippedDiagonally(): boolean {
return FlippingHelper.isFlippedDiagonally(this.tileId);
}
}
/**
* Tile identifiers making to access flipping flags.
*/
class FlippingHelper {
static readonly flippedHorizontallyFlag = 0x80000000;
static readonly flippedVerticallyFlag = 0x40000000;
static readonly flippedDiagonallyFlag = 0x20000000;
static readonly tileIdMask = ~(
FlippingHelper.flippedHorizontallyFlag |
FlippingHelper.flippedVerticallyFlag |
FlippingHelper.flippedDiagonallyFlag
);
static getTileId(tileId: integer): integer {
return tileId & FlippingHelper.tileIdMask;
}
static setFlippedHorizontally(
tileId: integer,
flippedHorizontally: boolean
): integer {
tileId &= ~FlippingHelper.flippedHorizontallyFlag;
if (flippedHorizontally) {
tileId |= FlippingHelper.flippedHorizontallyFlag;
}
return tileId;
}
static setFlippedVertically(
tileId: integer,
flippedVertically: boolean
): integer {
tileId &= ~FlippingHelper.flippedVerticallyFlag;
if (flippedVertically) {
tileId |= FlippingHelper.flippedVerticallyFlag;
}
return tileId;
}
static setFlippedDiagonally(
tileId: integer,
flippedDiagonally: boolean
): integer {
tileId &= ~FlippingHelper.flippedDiagonallyFlag;
if (flippedDiagonally) {
tileId |= FlippingHelper.flippedDiagonallyFlag;
}
return tileId;
}
static isFlippedHorizontally(tileId: integer): boolean {
return (tileId & FlippingHelper.flippedHorizontallyFlag) !== 0;
}
static isFlippedVertically(tileId: integer): boolean {
return (tileId & FlippingHelper.flippedVerticallyFlag) !== 0;
}
static isFlippedDiagonally(tileId: integer): boolean {
return (tileId & FlippingHelper.flippedDiagonallyFlag) !== 0;
}
}
/**
* A tile map layer with tile organized in grid.
*/
export class EditableTileMapLayer extends AbstractEditableLayer {
private readonly _tiles: Array<Int32Array>;
/**
* @param tileMap The layer tile map.
* @param id The layer identifier.
*/
constructor(tileMap: EditableTileMap, id: integer) {
super(tileMap, id);
this._tiles = [];
this._tiles.length = this.tileMap.getDimensionY();
for (let index = 0; index < this._tiles.length; index++) {
this._tiles[index] = new Int32Array(this.tileMap.getDimensionX());
}
}
/**
* @param x The layer column.
* @param y The layer row.
* @param tileId The tile identifier in the tile set.
*/
setTile(x: integer, y: integer, tileId: integer): void {
const definition = this.tileMap.getTileDefinition(tileId);
if (!definition) {
console.error(`Invalid tile definition index: ${tileId}`);
return;
}
// +1 because 0 mean null
this._tiles[y][x] = tileId + 1;
}
/**
* @param x The layer column.
* @param y The layer row.
*/
removeTile(x: integer, y: integer): void {
// 0 mean null
this._tiles[y][x] = 0;
}
/**
* @param x The layer column.
* @param y The layer row.
* @param flippedHorizontally true if the tile is flipped horizontally.
*/
setFlippedHorizontally(
x: integer,
y: integer,
flippedHorizontally: boolean
): void {
const tileId = this._tiles[y][x];
if (tileId === 0) {
return;
}
this._tiles[y][x] = FlippingHelper.setFlippedHorizontally(
tileId,
flippedHorizontally
);
}
/**
* @param x The layer column.
* @param y The layer row.
* @param flippedVertically true if the tile is flipped vertically.
*/
setFlippedVertically(
x: integer,
y: integer,
flippedVertically: boolean
): void {
const tileId = this._tiles[y][x];
if (tileId === 0) {
return;
}
this._tiles[y][x] = FlippingHelper.setFlippedVertically(
tileId,
flippedVertically
);
}
/**
* @param x The layer column.
* @param y The layer row.
* @param flippedDiagonally true if the tile is flipped diagonally.
*/
setFlippedDiagonally(
x: integer,
y: integer,
flippedDiagonally: boolean
): void {
const tileId = this._tiles[y][x];
if (tileId === 0) {
return;
}
this._tiles[y][x] = FlippingHelper.setFlippedDiagonally(
tileId,
flippedDiagonally
);
}
/**
* @param x The layer column.
* @param y The layer row.
* @returns true if the tile is flipped horizontally.
*/
isFlippedHorizontally(x: integer, y: integer): boolean {
return FlippingHelper.isFlippedHorizontally(this._tiles[y][x]);
}
/**
* @param x The layer column.
* @param y The layer row.
* @returns true if the tile is flipped vertically.
*/
isFlippedVertically(x: integer, y: integer): boolean {
return FlippingHelper.isFlippedVertically(this._tiles[y][x]);
}
/**
* @param x The layer column.
* @param y The layer row.
* @returns true if the tile is flipped diagonally.
*/
isFlippedDiagonally(x: integer, y: integer): boolean {
return FlippingHelper.isFlippedDiagonally(this._tiles[y][x]);
}
/**
* @param x The layer column.
* @param y The layer row.
* @returns The tile identifier from the tile set.
*/
get(x: integer, y: integer): integer | undefined {
const row = this._tiles[y];
if (!row || row[x] === 0) {
return undefined;
}
// -1 because 0 is keep for null.
const tileId = FlippingHelper.getTileId(row[x] - 1);
return tileId;
}
/**
* The number of tile columns in the layer.
*/
getDimensionX(): integer {
return this._tiles.length === 0 ? 0 : this._tiles[0].length;
}
/**
* The number of tile rows in the layer.
*/
getDimensionY(): integer {
return this._tiles.length;
}
/**
* @returns The layer width in pixels.
*/
getWidth(): integer {
return this.tileMap.getWidth();
}
/**
* @returns The layer height in pixels.
*/
getHeight(): integer {
return this.tileMap.getHeight();
}
}
/**
* A tile definition from the tile set.
*/
export class TileDefinition {
/**
* There will probably be at most 4 tags on a tile.
* An array lookup should take less time than using a Map.
*/
private readonly taggedHitBoxes: {
tag: string;
polygons: PolygonVertices[];
}[];
private readonly animationLength: integer;
/**
* @param animationLength The number of frame in the tile animation.
*/
constructor(animationLength: integer) {
this.taggedHitBoxes = [];
this.animationLength = animationLength;
}
/**
* Add a polygon for the collision layer
* @param tag The tag to allow collision layer filtering.
* @param polygon The polygon to use for collisions.
*/
add(tag: string, polygon: PolygonVertices): void {
let taggedHitBox = this.taggedHitBoxes.find((hitbox) => hitbox.tag === tag);
if (!taggedHitBox) {
taggedHitBox = { tag, polygons: [] };
this.taggedHitBoxes.push(taggedHitBox);
}
taggedHitBox.polygons.push(polygon);
}
/**
* This property is used by {@link TransformedCollisionTileMap}
* to make collision classes.
* @param tag The tag to allow collision layer filtering.
* @returns true if this tile contains any polygon with the given tag.
*/
hasTag(tag: string): boolean {
return this.taggedHitBoxes.some((hitbox) => hitbox.tag === tag);
}
/**
* The hitboxes positioning is done by {@link TransformedCollisionTileMap}.
* @param tag The tag to allow collision layer filtering.
* @returns The hit boxes for this tile.
*/
getHitBoxes(tag: string): PolygonVertices[] | undefined {
const taggedHitBox = this.taggedHitBoxes.find(
(hitbox) => hitbox.tag === tag
);
return taggedHitBox && taggedHitBox.polygons;
}
/**
* Animated tiles have a limitation:
* they are only able to use frames arranged horizontally one next
* to each other on the atlas.
* @returns The number of frame in the tile animation.
*/
getAnimationLength(): integer {
return this.animationLength;
}
}

View File

@@ -0,0 +1,62 @@
/**
* A cache of resources identified by a string.
*
* It ensures that a resource is never load twice.
*/
export class ResourceCache<T> {
private _cachedValues: Map<string, T>;
/**
* Several calls can happen before the resource is loaded.
* This allows to stack them.
*/
private _callbacks: Map<string, Array<(value: T | null) => void>>;
constructor() {
this._cachedValues = new Map<string, T>();
this._callbacks = new Map<string, Array<(value: T | null) => void>>();
}
/**
* Return a resource through a call back.
* @param key the resource identifier.
* @param load load the resource in case of cache default.
* Note that the load callback is used by `getOrLoad` and not by the caller.
* @param callback called when the resource is ready.
*/
getOrLoad(
key: string,
load: (callback: (value: T | null) => void) => void,
callback: (value: T | null) => void
): void {
// Check if the value is in the cache.
{
const value = this._cachedValues.get(key);
if (value) {
callback(value);
return;
}
}
// Check if the value is being loading.
{
const callbacks = this._callbacks.get(key);
if (callbacks) {
callbacks.push(callback);
return;
} else {
this._callbacks.set(key, [callback]);
}
}
load((value) => {
if (value) {
this._cachedValues.set(key, value);
}
const callbacks = this._callbacks.get(key)!;
this._callbacks.delete(key);
for (const callback of callbacks) {
callback(value);
}
});
}
}

View File

@@ -0,0 +1,138 @@
import { ResourceCache } from "./ResourceCache";
import { TiledMap } from "../tiled/TiledFormat";
import { TiledTileMapLoader } from "../tiled/TiledTileMapLoader";
import { EditableTileMap } from "../model/TileMapModel";
import { TileTextureCache } from "./TileTextureCache";
import { PixiTileMapHelper } from "./TileMapPixiHelper";
import PIXI = GlobalPIXIModule.PIXI;
/**
* A holder to share tile maps across the 2 extension objects.
*
* Every instance with the same files path in properties will
* share the same {@link EditableTileMap} and {@link TileTextureCache}.
*
* @see {@link TileMapRuntimeManager}
*/
export class TileMapManager {
private _tileMapCache: ResourceCache<EditableTileMap>;
private _textureCacheCaches: ResourceCache<TileTextureCache>;
constructor() {
this._tileMapCache = new ResourceCache<EditableTileMap>();
this._textureCacheCaches = new ResourceCache<TileTextureCache>();
}
/**
* @param instanceHolder Where to set the manager instance.
* @returns The shared manager.
*/
static getManager(instanceHolder: Object): TileMapManager {
// @ts-ignore
if (!instanceHolder.tileMapCollisionMaskManager) {
//Create the shared manager if necessary.
// @ts-ignore
instanceHolder.tileMapCollisionMaskManager = new TileMapManager();
}
// @ts-ignore
return instanceHolder.tileMapCollisionMaskManager;
}
/**
* @param loadTiledMap The method that loads the Tiled JSON file in memory.
* @param tileMapJsonResourceName The resource name of the tile map.
* @param tileSetJsonResourceName The resource name of the tile set.
* @param pako The zlib library.
* @param callback A function called when the tile map is parsed.
*/
getOrLoadTileMap(
loadTiledMap: (
tileMapJsonResourceName: string,
tileSetJsonResourceName: string,
callback: (tiledMap: TiledMap | null) => void
) => void,
tileMapJsonResourceName: string,
tileSetJsonResourceName: string,
pako: any,
callback: (tileMap: EditableTileMap | null) => void
): void {
const key = tileMapJsonResourceName + "|" + tileSetJsonResourceName;
this._tileMapCache.getOrLoad(
key,
(callback) => {
loadTiledMap(
tileMapJsonResourceName,
tileSetJsonResourceName,
(tiledMap: TiledMap | null) => {
if (!tiledMap) {
callback(null);
return;
}
const collisionTileMap = TiledTileMapLoader.load(pako, tiledMap);
callback(collisionTileMap);
}
);
},
callback
);
}
/**
* @param loadTiledMap The method that loads the Tiled JSON file in memory.
* @param getTexture The method that loads the atlas image file in memory.
* @param atlasImageResourceName The resource name of the atlas image.
* @param tileMapJsonResourceName The resource name of the tile map.
* @param tileSetJsonResourceName The resource name of the tile set.
* @param callback A function called when the tiles textures are split.
*/
getOrLoadTextureCache(
loadTiledMap: (
tileMapJsonResourceName: string,
tileSetJsonResourceName: string,
callback: (tiledMap: TiledMap | null) => void
) => void,
getTexture: (textureName: string) => PIXI.BaseTexture<PIXI.Resource>,
atlasImageResourceName: string,
tileMapJsonResourceName: string,
tileSetJsonResourceName: string,
callback: (textureCache: TileTextureCache | null) => void
): void {
const key =
tileMapJsonResourceName +
"|" +
tileSetJsonResourceName +
"|" +
atlasImageResourceName;
this._textureCacheCaches.getOrLoad(
key,
(callback) => {
loadTiledMap(
tileMapJsonResourceName,
tileSetJsonResourceName,
(tiledMap: TiledMap | null) => {
if (!tiledMap) {
// loadTiledMap already log errors.
callback(null);
return;
}
const atlasTexture = atlasImageResourceName
? getTexture(atlasImageResourceName)
: null;
const textureCache = PixiTileMapHelper.parseAtlas(
tiledMap,
atlasTexture,
getTexture
);
callback(textureCache);
}
);
},
callback
);
}
}

View File

@@ -0,0 +1,273 @@
import { integer, float } from "../model/CommonTypes";
import { TiledMap } from "../tiled/TiledFormat";
import {
EditableObjectLayer,
EditableTileMap,
EditableTileMapLayer,
} from "../model/TileMapModel";
import { TileTextureCache } from "./TileTextureCache";
import PIXI = GlobalPIXIModule.PIXI;
import { getTileIdFromTiledGUI } from "../tiled/TiledLoaderHelper";
export class PixiTileMapHelper {
/**
* Split an atlas image into Pixi textures.
*
* @param tiledMap A tile map exported from Tiled.
* @param atlasTexture The texture containing the whole tile set.
* @param getTexture A getter to load a texture. Used if atlasTexture is not specified.
* @returns A textures cache.
*/
static parseAtlas(
tiledMap: TiledMap,
atlasTexture: PIXI.BaseTexture<PIXI.Resource> | null,
getTexture: (textureName: string) => PIXI.BaseTexture<PIXI.Resource>
): TileTextureCache | null {
if (!tiledMap.tiledversion) {
console.warn(
"The loaded Tiled map does not contain a 'tiledversion' key. Are you sure this file has been exported from Tiled (mapeditor.org)?"
);
return null;
}
// We only handle tileset embedded in the tilemap. Warn if it's not the case.
if (!tiledMap.tilesets.length || "source" in tiledMap.tilesets[0]) {
console.warn(
"The loaded Tiled map seems not to contain any tileset data (nothing in 'tilesets' key)."
);
return null;
}
const tiledSet = tiledMap.tilesets[0];
const {
tilewidth,
tileheight,
tilecount,
image,
columns,
spacing,
margin,
} = tiledSet;
const firstGid = tiledSet.firstgid === undefined ? 1 : tiledSet.firstgid;
if (!atlasTexture) atlasTexture = getTexture(image);
// We try to detect what size Tiled is expecting.
const rows = tilecount / columns;
const expectedAtlasWidth =
tilewidth * columns + spacing * (columns - 1) + margin * 2;
const expectedAtlasHeight =
tileheight * rows + spacing * (rows - 1) + margin * 2;
if (
(atlasTexture.width !== 1 && expectedAtlasWidth !== atlasTexture.width) ||
(atlasTexture.height !== 1 && expectedAtlasHeight !== atlasTexture.height)
) {
const expectedSize = expectedAtlasWidth + "x" + expectedAtlasHeight;
const actualSize = atlasTexture.width + "x" + atlasTexture.height;
console.warn(
"It seems the atlas file was resized, which is not supported. It should be " +
expectedSize +
"px, but it's " +
actualSize +
" px."
);
return null;
}
// Prepare the textures pointing to the base "Atlas" Texture for each tile.
// Note that this cache can be augmented later with rotated/flipped
// versions of the tile textures.
const textureCache = new TileTextureCache();
for (let tileSetIndex = 0; tileSetIndex < tilecount; tileSetIndex++) {
const columnMultiplier = Math.floor(tileSetIndex % columns);
const rowMultiplier = Math.floor(tileSetIndex / columns);
const x = margin + columnMultiplier * (tilewidth + spacing);
const y = margin + rowMultiplier * (tileheight + spacing);
const tileId = getTileIdFromTiledGUI(firstGid + tileSetIndex);
try {
const rect = new PIXI.Rectangle(x, y, tilewidth, tileheight);
const texture = new PIXI.Texture(atlasTexture!, rect);
textureCache.setTexture(tileId, false, false, false, texture);
} catch (error) {
console.error(
"An error occurred while creating a PIXI.Texture to be used in a TileMap:",
error
);
}
}
return textureCache;
}
/**
* Re-renders the tile map whenever its rendering settings have been changed
*
* @param pixiTileMap the tile map renderer
* @param tileMap the tile map model
* @param textureCache the tile set textures
* @param displayMode What to display:
* - only a single layer (`index`)
* - only visible layers (`visible`)
* - everything (`all`).
* @param layerIndex If `displayMode` is set to `index`, the layer index to be
* displayed.
*/
static updatePixiTileMap(
untypedPixiTileMap: any,
tileMap: EditableTileMap,
textureCache: TileTextureCache,
displayMode: "index" | "visible" | "all",
layerIndex: number
): void {
// The extension doesn't handle the Pixi sub-namespace very well.
const pixiTileMap = untypedPixiTileMap as PIXI.tilemap.CompositeRectTileLayer;
if (!pixiTileMap) return;
pixiTileMap.clear();
for (const layer of tileMap.getLayers()) {
if (
(displayMode === "index" && layerIndex !== layer.id) ||
(displayMode === "visible" && !layer.isVisible())
) {
return;
}
if (layer instanceof EditableObjectLayer) {
const objectLayer = layer as EditableObjectLayer;
for (const object of objectLayer.objects) {
const texture = textureCache.findTileTexture(
object.getTileId(),
object.isFlippedHorizontally(),
object.isFlippedVertically(),
object.isFlippedDiagonally()
);
if (texture) {
pixiTileMap.addFrame(
texture,
object.x,
object.y - objectLayer.tileMap.getTileHeight()
);
}
}
} else if (layer instanceof EditableTileMapLayer) {
const tileLayer = layer as EditableTileMapLayer;
for (let y = 0; y < tileLayer.tileMap.getDimensionY(); y++) {
for (let x = 0; x < tileLayer.tileMap.getDimensionX(); x++) {
const tileWidth = tileLayer.tileMap.getTileWidth();
const xPos = tileWidth * x;
const yPos = tileLayer.tileMap.getTileHeight() * y;
const tileId = tileLayer.get(x, y);
if (tileId === undefined) {
continue;
}
const tileTexture = textureCache.findTileTexture(
tileId,
tileLayer.isFlippedHorizontally(x, y),
tileLayer.isFlippedVertically(x, y),
tileLayer.isFlippedDiagonally(x, y)
);
if (!tileTexture) {
continue;
}
const pixiTilemapFrame = pixiTileMap.addFrame(
tileTexture,
xPos,
yPos
);
const tileDefinition = tileLayer.tileMap.getTileDefinition(tileId);
// Animated tiles have a limitation:
// they are only able to use frames arranged horizontally one next
// to each other on the atlas.
if (tileDefinition && tileDefinition.getAnimationLength() > 0) {
pixiTilemapFrame.tileAnimX(
tileWidth,
tileDefinition.getAnimationLength()
);
}
}
}
}
}
}
/**
* Re-renders the collision mask
*/
static updatePixiCollisionMask(
pixiGraphics: PIXI.Graphics,
tileMap: EditableTileMap,
typeFilter: string,
outlineSize: integer,
outlineColor: integer,
outlineOpacity: float,
fillColor: integer,
fillOpacity: float
): void {
if (!pixiGraphics) return;
pixiGraphics.clear();
for (const layer of tileMap.getLayers()) {
const tileWidth = tileMap.getTileWidth();
const tileHeight = tileMap.getTileHeight();
if (layer instanceof EditableTileMapLayer) {
const tileLayer = layer as EditableTileMapLayer;
for (let y = 0; y < tileLayer.tileMap.getDimensionY(); y++) {
for (let x = 0; x < tileLayer.tileMap.getDimensionX(); x++) {
const xPos = tileWidth * x;
const yPos = tileHeight * y;
const tileId = tileLayer.get(x, y)!;
const isFlippedHorizontally = tileLayer.isFlippedHorizontally(x, y);
const isFlippedVertically = tileLayer.isFlippedVertically(x, y);
const isFlippedDiagonally = tileLayer.isFlippedDiagonally(x, y);
const tileDefinition = tileLayer.tileMap.getTileDefinition(tileId);
if (!tileDefinition) {
continue;
}
const hitboxes = tileDefinition.getHitBoxes(typeFilter);
if (!hitboxes) {
continue;
}
pixiGraphics.lineStyle(outlineSize, outlineColor, outlineOpacity);
for (const vertices of hitboxes) {
if (vertices.length === 0) continue;
pixiGraphics.beginFill(fillColor, fillOpacity);
for (let index = 0; index < vertices.length; index++) {
let vertexX = vertices[index][0];
let vertexY = vertices[index][1];
if (isFlippedHorizontally) {
vertexX = tileWidth - vertexX;
}
if (isFlippedVertically) {
vertexY = tileHeight - vertexY;
}
if (isFlippedDiagonally) {
const swap = vertexX;
vertexX = vertexY;
vertexY = swap;
}
if (index === 0) {
pixiGraphics.moveTo(xPos + vertexX, yPos + vertexY);
} else {
pixiGraphics.lineTo(xPos + vertexX, yPos + vertexY);
}
}
pixiGraphics.closePath();
pixiGraphics.endFill();
}
}
}
}
}
}
}

View File

@@ -0,0 +1,137 @@
import { integer } from "../model/CommonTypes";
import PIXI = GlobalPIXIModule.PIXI;
/**
* A cache to access the tile images.
*
* It's created by {@link PixiTileMapHelper.parseAtlas}
* and used by {@link PixiTileMapHelper.updatePixiTileMap}.
*/
export class TileTextureCache {
private static readonly flippedHorizontallyFlag = 0x80000000;
private static readonly flippedVerticallyFlag = 0x40000000;
private static readonly flippedDiagonallyFlag = 0x20000000;
private readonly _textures: Map<integer, PIXI.Texture>;
constructor() {
this._textures = new Map<integer, PIXI.Texture>();
}
setTexture(
tileId: integer,
flippedHorizontally: boolean,
flippedVertically: boolean,
flippedDiagonally: boolean,
texture: PIXI.Texture
): void {
let globalTileUid = this._getGlobalId(
tileId,
flippedHorizontally,
flippedVertically,
flippedDiagonally
);
this._textures.set(globalTileUid, texture);
}
/**
* Return the texture to use for the tile with the specified uid, which can contains
* information about rotation in bits 32, 31 and 30
* (see https://doc.mapeditor.org/en/stable/reference/tmx-map-format/).
*
* @param tileId The tile identifier
* @param flippedHorizontally true if the tile is flipped horizontally.
* @param flippedVertically true if the tile is flipped vertically.
* @param flippedDiagonally true if the tile is flipped diagonally.
* @returns The texture for the given tile identifier and orientation.
*/
findTileTexture(
tileId: integer,
flippedHorizontally: boolean,
flippedVertically: boolean,
flippedDiagonally: boolean
): PIXI.Texture | undefined {
let globalTileUid = this._getGlobalId(
tileId,
flippedHorizontally,
flippedVertically,
flippedDiagonally
);
if (this._textures.has(globalTileUid)) {
return this._textures.get(globalTileUid);
}
// If the texture is not in the cache, it's potentially because its ID
// is a flipped/rotated version of another ID.
const unflippedTexture = this._textures.get(tileId);
// If the tile still can't be found in the cache, it means the ID we got
// is invalid.
if (!unflippedTexture) return undefined;
// Clone the unflipped texture and save it in the cache
const frame = unflippedTexture.frame.clone();
const orig = unflippedTexture.orig.clone();
if (flippedDiagonally) {
const width = orig.width;
orig.width = orig.height;
orig.height = width;
}
const trim = orig.clone();
// Get the rotation "D8" number.
// See https://pixijs.io/examples/#/textures/texture-rotate.js
let rotate = 0;
if (flippedDiagonally) {
rotate = 10;
if (!flippedHorizontally && flippedVertically) {
rotate = 2;
} else if (flippedHorizontally && !flippedVertically) {
rotate = 6;
} else if (flippedHorizontally && flippedVertically) {
rotate = 14;
}
} else {
rotate = 0;
if (!flippedHorizontally && flippedVertically) {
rotate = 8;
} else if (flippedHorizontally && !flippedVertically) {
rotate = 12;
} else if (flippedHorizontally && flippedVertically) {
rotate = 4;
}
}
const flippedTexture = new PIXI.Texture(
unflippedTexture.baseTexture,
frame,
orig,
trim,
rotate
);
this._textures.set(globalTileUid, flippedTexture);
return flippedTexture;
}
/**
* @return the Tiled tile global uniq identifier.
*/
private _getGlobalId(
tileId: integer,
flippedHorizontally: boolean,
flippedVertically: boolean,
flippedDiagonally: boolean
): integer {
let globalTileUid = tileId;
if (flippedHorizontally) {
globalTileUid |= TileTextureCache.flippedHorizontallyFlag;
}
if (flippedVertically) {
globalTileUid |= TileTextureCache.flippedVerticallyFlag;
}
if (flippedDiagonally) {
globalTileUid |= TileTextureCache.flippedDiagonallyFlag;
}
return globalTileUid;
}
}

View File

@@ -0,0 +1,487 @@
import { float, integer } from "../model/CommonTypes";
/**
* Tiled JSON format (https://www.mapeditor.org/).
*/
export type TiledMap = {
/** Hex-formatted color (#RRGGBB or #AARRGGBB) (optional) */
backgroundcolor?: string;
/** The compression level to use for tile layer data (defaults to -1, which means to use the algorithm default) */
compressionlevel: integer;
/** Number of tile rows */
height: integer;
/** Length of the side of a hex tile in pixels (hexagonal maps only) */
hexsidelength?: integer;
/** Whether the map has infinite dimensions */
infinite: boolean;
/** Array of {@link TiledLayer} */
layers: Array<TiledLayer>;
/** Auto-increments for each layer */
nextlayerid: integer;
/** Auto-increments for each placed object */
nextobjectid: integer;
/** `orthogonal`, `isometric`, `staggered` or `hexagonal` */
orientation: string;
/** Array of {@link TiledProperty} */
properties?: Array<TiledProperty>;
/** `right-down` (the default), `right-up`, `left-down` or `left-up` (currently only supported for orthogonal maps) */
renderorder: string;
/** `x` or `y` (staggered / hexagonal maps only) */
staggeraxis?: string;
/** `odd` or `even` (staggered / hexagonal maps only) */
staggerindex?: string;
/** The Tiled version used to save the file */
tiledversion: string;
/** Map grid height */
tileheight: integer;
/** Array of {@link TiledTileset} */
tilesets: Array<TiledTileset>;
/** Map grid width */
tilewidth: integer;
/** `map` (since 1.0) */
type: string;
/** The JSON format version (previously a number, saved as string since 1.6) */
version: string;
/** Number of tile columns */
width: integer;
};
export type TiledLayer = {
/** Array of {@link TiledChunk} (optional). `tilelayer` only. */
chunks?: Array<TiledChunk>;
/** `zlib`, `gzip`, `zstd` (since Tiled 1.3) or empty (default). `tilelayer` only. */
compression?: string;
/** Array of `unsigned`, `integer` (GIDs) or base64-encoded data. `tilelayer` only.*/
data?: Array<integer> | string;
/** `topdown` (default) or `index`. `objectgroup` only. */
draworder?: string;
/** `csv` (default) or `base64`. `tilelayer` only. */
encoding?: string;
/** Row count. Same as map height for fixed-size maps. */
height?: integer;
/** Incremental ID - unique across all layers */
id?: integer;
/** Image used by this layer. `imagelayer` only. */
image?: string;
/** Array of {@link TiledLayer}. `group` only. */
layers?: Array<TiledLayer>;
/** Name assigned to this layer */
name: string;
/** Array of {@link TiledObject}. `objectgroup` only. */
objects?: Array<TiledObject>;
/** Horizontal layer offset in pixels (default: 0) */
offsetx?: float;
/** Vertical layer offset in pixels (default: 0) */
offsety?: float;
/** Value between 0 and 1 */
opacity: float;
/** Horizontal {@link parallax factor} for this layer (default: 1). (since Tiled 1.5) */
parallaxx?: float;
/** Vertical {@link parallax factor} for this layer (default: 1). (since Tiled 1.5) */
parallaxy?: float;
/** Array of {@link TiledProperty} */
properties?: Array<TiledProperty>;
/** X coordinate where layer content starts (for infinite maps) */
startx?: integer;
/** Y coordinate where layer content starts (for infinite maps) */
starty?: integer;
/** Hex-formatted {@link tint color} (#RRGGBB or #AARRGGBB) that is multiplied with any graphics drawn by this layer or any child layers (optional). */
tintcolor?: string;
/** Hex-formatted color (#RRGGBB) (optional). `imagelayer` only. */
transparentcolor?: string;
/** `tilelayer`, `objectgroup`, `imagelayer` or `group` */
type: string;
/** Whether layer is shown or hidden in editor */
visible: boolean;
/** Column count. Same as map width for fixed-size maps. */
width?: integer;
/** Horizontal layer offset in tiles. Always 0. */
x: integer;
/** Vertical layer offset in tiles. Always 0. */
y: integer;
};
export type TiledChunk = {
/** Array of `unsigned` `integer` (GIDs) or base64-encoded data */
data: Array<integer> | string;
/** Height in tiles */
height: integer;
/** Width in tiles */
width: integer;
/** X coordinate in tiles */
x: integer;
/** Y coordinate in tiles */
y: integer;
};
export type TiledObject = {
/** The class of the object (renamed from type since 1.9, optional) */
class?: string;
/** Used to mark an object as an ellipse */
ellipse?: boolean;
/** Global tile ID, only if object represents a tile */
gid?: integer;
/** Height in pixels. */
height: float;
/** Incremental ID, unique across all objects */
id: integer;
/** String assigned to name field in editor */
name: string;
/** Used to mark an object as a point */
point?: boolean;
/** Array of {@link TiledPoint}, in case the object is a polygon */
polygon?: Array<TiledPoint>;
/** Array of {@link TiledPoint}, in case the object is a polyline */
polyline?: Array<TiledPoint>;
/** Array of {@link TiledProperty} */
properties?: Array<TiledProperty>;
/** Angle in degrees clockwise */
rotation: float;
/** Reference to a template file, in case object is a {@link template instance} */
template?: string;
/** Only used for text objects */
text?: Text;
/** Whether object is shown in editor. */
visible: boolean;
/** Width in pixels. */
width: float;
/** X coordinate in pixels */
x: float;
/** Y coordinate in pixels */
y: float;
};
export type TiledText = {
/** Whether to use a bold font (default: `false`) */
bold: boolean;
/** Hex-formatted color (#RRGGBB or #AARRGGBB) (default: `#000000`) */
color: string;
/** Font family (default: `sans-serif`) */
fontfamily: string;
/** Horizontal alignment (`center`, `right`, `justify` or `left` (default)) */
halign: string;
/** Whether to use an italic font (default: `false`) */
italic: boolean;
/** Whether to use kerning when placing characters (default: `true`) */
kerning: boolean;
/** Pixel size of font (default: 16) */
pixelsize: integer;
/** Whether to strike out the text (default: `false`) */
strikeout: boolean;
/** Text */
text: string;
/** Whether to underline the text (default: `false`) */
underline: boolean;
/** Vertical alignment (`center`, `bottom` or `top` (default)) */
valign: string;
/** Whether the text is wrapped within the object bounds (default: `false`) */
wrap: boolean;
};
export type TiledTileset = {
/** Hex-formatted color (#RRGGBB or #AARRGGBB) (optional) */
backgroundcolor?: string;
/** The number of tile columns in the tileset */
columns: integer;
/** GID corresponding to the first tile in the set */
firstgid: integer;
/** (optional) */
grid?: TiledGrid;
/** Image used for tiles in this set */
image: string;
/** Height of source image in pixels */
imageheight: integer;
/** Width of source image in pixels */
imagewidth: integer;
/** Buffer between image edge and first tile (pixels) */
margin: integer;
/** Name given to this tileset */
name: string;
/** Alignment to use for tile objects (`unspecified` (default), `topleft`, `top`, `topright`, `left`, `center`, `right`, `bottomleft`, `bottom` or `bottomright`) (since 1.4) */
objectalignment?: string;
/** Array of {@link TiledProperty} */
properties?: Array<TiledProperty>;
/** The external file that contains this tilesets data */
source?: string;
/** Spacing between adjacent tiles in image (pixels) */
spacing: integer;
/** Array of {@link TiledTerrain} (optional) */
terrains?: Array<TiledTerrain>;
/** The number of tiles in this tileset */
tilecount: integer;
/** The Tiled version used to save the file */
tiledversion: string;
/** Maximum height of tiles in this set */
tileheight: integer;
/** (optional) */
tileoffset?: TileOffset;
/** Array of {@link TiledTileDefinition} (optional) */
tiles?: Array<TiledTileDefinition>;
/** Maximum width of tiles in this set */
tilewidth: integer;
/** Allowed transformations (optional) */
transformations?: TiledTransformations;
/** Hex-formatted color (#RRGGBB) (optional) */
transparentcolor?: string;
/** `tileset` (for tileset files, since 1.0) */
type: string;
/** The JSON format version (previously a number, saved as string since 1.6) */
version: string;
/** Array of {@link TiledWangSet} (since 1.1.5) */
wangsets?: Array<TiledWangSet>;
};
export type TiledGrid = {
/** Cell height of tile grid */
height: integer;
/** `orthogonal` (default) or `isometric` */
orientation: string;
/** Cell width of tile grid */
width: integer;
};
export type TileOffset = {
/** Horizontal offset in pixels */
x: integer;
/** Vertical offset in pixels (positive is down) */
y: integer;
};
export type TiledTransformations = {
/** Tiles can be flipped horizontally */
hflip: boolean;
/** Tiles can be flipped vertically */
vflip: boolean;
/** Tiles can be rotated in 90-degree increments */
rotate: boolean;
/** Whether untransformed tiles remain preferred, otherwise transformed tiles are used to produce more variations */
preferuntransformed: boolean;
};
export type TiledTileDefinition = {
/** Array of {@link TiledTiles} */
animation?: Array<TiledTileDefinition>;
/** The class of the tile (renamed from type since 1.9, optional) */
class?: string;
/** Local ID of the tile */
id: integer;
/** Image representing this tile (optional) */
image?: string;
/** Height of the tile image in pixels */
imageheight?: integer;
/** Width of the tile image in pixels */
imagewidth?: integer;
/** Layer with type Tiled`objectgroup`, when collision shapes are specified (optional) */
objectgroup?: TiledLayer;
/** Percentage chance this tile is chosen when competing with others in the editor (optional) */
probability?: float;
/** Array of {@link TiledProperty} */
properties?: Array<TiledProperty>;
/** Index of terrain for each corner of tile (optional) */
terrain?: Array<integer>;
};
export type TiledFrame = {
/** Frame duration in milliseconds */
duration: integer;
/** Local tile ID representing this frame */
tileid: integer;
};
export type TiledTerrain = {
/** Name of terrain */
name: string;
/** Array of {@link TiledProperty} */
properties: Array<TiledProperty>;
/** Local ID of tile representing terrain */
tile: integer;
};
export type TiledWangSet = {
/** Array of {@link TiledWangColor} */
colors: Array<TiledWangColor>;
/** Name of the Wang set */
name: string;
/** Array of {@link TiledProperty} */
properties: Array<TiledProperty>;
/** Local ID of tile representing the Wang set */
tile: integer;
/** Array of {@link TiledWangTile} */
wangtiles: Array<TiledWangTile>;
};
export type TiledWangColor = {
/** Hex-formatted color (#RRGGBB or #AARRGGBB) */
color: string;
/** Name of the Wang color */
name: string;
/** Probability used when randomizing */
probability: float;
/** Array of {@link TiledProperty} */
properties: Array<TiledProperty>;
/** Local ID of tile representing the Wang color */
tile: integer;
};
export type TiledWangTile = {
/** Local ID of tile */
tileid: integer;
/** Array of Wang color indexes (`uchar[8]`) */
wangid: Array<integer>;
};
export type TiledObjectTemplate = {
/** `template` */
type: string;
/** External tileset used by the template (optional) */
tileset?: TiledTileset;
/** The object instantiated by this template */
object: Object;
};
export type TiledProperty = {
/** Name of the property */
name: string;
/** type of the property (`string` (default), `integer`, `float`, `boolean`, `color` or `file` (since 0.16, with `color` and `file` added in 0.17)) */
type: string;
/** Value of the property */
value: string | number;
};
export type TiledPoint = {
/** X coordinate in pixels */
x: float;
/** Y coordinate in pixels */
y: float;
};

View File

@@ -0,0 +1,108 @@
import { integer } from "../model/CommonTypes";
import { TiledLayer } from "./TiledFormat";
/**
* Decodes a layer data, which can sometimes be store as a compressed base64 string
* by Tiled.
* See https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#data.
* @param pako The zlib library.
* @param layer The layer data from a Tiled JSON.
* @returns The decoded layer data.
*/
export const decodeBase64LayerData = (pako: any, layer: TiledLayer) => {
const { data, compression } = layer;
const dataBase64 = data as string;
if (!dataBase64) {
// The layer data is not encoded.
return data as number[];
}
let index = 4;
const decodedData: integer[] = [];
let step1 = atob(dataBase64)
.split("")
.map(function (x) {
return x.charCodeAt(0);
});
try {
const decodeArray = (arr: integer[] | Uint8Array, index: integer) =>
(arr[index] +
(arr[index + 1] << 8) +
(arr[index + 2] << 16) +
(arr[index + 3] << 24)) >>>
0;
if (compression === "zlib") {
const binData = new Uint8Array(step1);
const decompressedData = pako.inflate(binData);
while (index <= decompressedData.length) {
decodedData.push(decodeArray(decompressedData, index - 4));
index += 4;
}
} else if (compression === "zstd") {
console.error(
"Zstandard compression is not supported for layers in a Tilemap. Use instead zlib compression or no compression."
);
return null;
} else {
while (index <= step1.length) {
decodedData.push(decodeArray(step1, index - 4));
index += 4;
}
}
return decodedData;
} catch (error) {
console.error(
"Failed to decompress and unzip base64 layer.data string",
error
);
return null;
}
};
export type TiledGID = {
id: integer;
flippedHorizontally: boolean;
flippedVertically: boolean;
flippedDiagonally: boolean;
};
/**
* Extract information about the rotation of a tile from the tile id.
* @param globalTileUid The Tiled tile global uniq identifier.
* @returns The tile identifier and orientation.
*/
export const extractTileUidFlippedStates = (
globalTileUid: integer
): TiledGID => {
const FLIPPED_HORIZONTALLY_FLAG = 0x80000000;
const FLIPPED_VERTICALLY_FLAG = 0x40000000;
const FLIPPED_DIAGONALLY_FLAG = 0x20000000;
const flippedHorizontally = globalTileUid & FLIPPED_HORIZONTALLY_FLAG;
const flippedVertically = globalTileUid & FLIPPED_VERTICALLY_FLAG;
const flippedDiagonally = globalTileUid & FLIPPED_DIAGONALLY_FLAG;
const tileUid = getTileIdFromTiledGUI(
globalTileUid &
~(
FLIPPED_HORIZONTALLY_FLAG |
FLIPPED_VERTICALLY_FLAG |
FLIPPED_DIAGONALLY_FLAG
)
);
return {
id: tileUid,
flippedHorizontally: !!flippedHorizontally,
flippedVertically: !!flippedVertically,
flippedDiagonally: !!flippedDiagonally,
};
};
/**
* Tiled use 0 as null, we do too but it's black boxed.
* This is why the id needs to be decremented.
* @return the tile identifier used in {@link TilMapModel}.
*/
export const getTileIdFromTiledGUI = (
tiledGUI: number | undefined
): number | undefined => (tiledGUI === 0 ? undefined : tiledGUI - 1);

View File

@@ -0,0 +1,516 @@
import { EditableTileMap, EditableTileMapLayer } from "../model/TileMapModel";
import { TiledMap } from "./TiledFormat";
import { TiledTileMapLoader } from "./TiledTileMapLoader";
describe("TiledTileMapLoader", function () {
describe("without a collision mask", function () {
// Built from an actual json file exported by Tiled.
const tiledMap: TiledMap = {
compressionlevel: -1,
height: 2,
infinite: false,
layers: [
{
data: [1, 0, 2, 0, 0, 1, 0, 2],
height: 2,
id: 1,
name: "Tile Layer 1",
opacity: 1,
type: "tilelayer",
visible: true,
width: 4,
x: 0,
y: 0,
},
],
nextlayerid: 2,
nextobjectid: 1,
orientation: "orthogonal",
renderorder: "right-down",
tiledversion: "1.7.2",
tileheight: 8,
tilesets: [
{
firstgid: 1,
columns: 2,
image: "MiniTiledSet.png",
imageheight: 8,
imagewidth: 16,
margin: 0,
name: "new tileset",
spacing: 0,
tilecount: 2,
tiledversion: "1.7.2",
tileheight: 8,
tilewidth: 8,
type: "tileset",
version: "1.6",
},
],
tilewidth: 8,
type: "map",
version: "1.6",
width: 4,
};
const tileMap: EditableTileMap = TiledTileMapLoader.load(null, tiledMap);
it("can load map dimensions", function () {
expect(tileMap.getDimensionX()).to.be(4);
expect(tileMap.getDimensionY()).to.be(2);
expect(tileMap.getTileHeight()).to.be(8);
expect(tileMap.getTileWidth()).to.be(8);
expect(tileMap.getWidth()).to.be(32);
expect(tileMap.getHeight()).to.be(16);
});
it("can load a tile set", function () {
expect(tileMap.getTileDefinition(0)).to.be.ok();
expect(tileMap.getTileDefinition(1)).to.be.ok();
expect(tileMap.getTileDefinition(2)).not.to.be.ok();
});
it("can load a tile map content", function () {
const layers = new Array(...tileMap.getLayers());
expect(layers.length).to.be(1);
// TODO Change the model to avoid casts?
const layer = layers[0] as EditableTileMapLayer;
// TODO Add the layer name as it can be useful for events?
expect(layer.id).to.be(1);
expect(layer.isVisible()).to.be(true);
expect(layer.get(0, 0)).to.be(0);
expect(layer.get(1, 0)).to.be(undefined);
expect(layer.get(2, 0)).to.be(1);
expect(layer.get(3, 0)).to.be(undefined);
expect(layer.get(0, 1)).to.be(undefined);
expect(layer.get(1, 1)).to.be(0);
expect(layer.get(2, 1)).to.be(undefined);
expect(layer.get(3, 1)).to.be(1);
});
});
describe("with a collision mask", function () {
// Built from an actual json file exported by Tiled.
const tiledMap: TiledMap = {
compressionlevel: -1,
height: 2,
infinite: false,
layers: [
{
data: [1, 3, 4, 5, 3, 2, 0, 1073741829],
height: 2,
id: 1,
name: "Tile Layer 1",
opacity: 1,
type: "tilelayer",
visible: true,
width: 4,
x: 0,
y: 0,
},
],
nextlayerid: 2,
nextobjectid: 1,
orientation: "orthogonal",
renderorder: "right-down",
tiledversion: "1.7.2",
tileheight: 8,
tilesets: [
{
firstgid: 1,
columns: 2,
image: "MiniTiledSet.png",
imageheight: 16,
imagewidth: 16,
margin: 0,
name: "new tileset",
spacing: 0,
tilecount: 4,
tiledversion: "1.7.2",
tileheight: 8,
tiles: [
// Contains a rectangle
{
id: 0,
objectgroup: {
draworder: "index",
name: "",
objects: [
{
height: 8,
id: 1,
name: "",
rotation: 0,
class: "",
visible: true,
width: 8,
x: 0,
y: 0,
},
],
opacity: 1,
type: "objectgroup",
visible: true,
x: 0,
y: 0,
},
class: "obstacle",
},
// The tile with id == 1 is missing
// because it doesn't have any collision mask.
// Contains a polygon.
{
id: 2,
objectgroup: {
draworder: "index",
name: "",
objects: [
{
height: 0,
id: 1,
name: "",
polygon: [
{
x: 0,
y: 0,
},
{
x: 8,
y: -8,
},
{
x: 0,
y: -8,
},
],
rotation: 0,
class: "",
visible: true,
width: 0,
x: 0,
y: 8,
},
],
opacity: 1,
type: "objectgroup",
visible: true,
x: 0,
y: 0,
},
class: "obstacle",
},
// Contains 2 polygons, one is a rotated.
{
id: 3,
objectgroup: {
draworder: "index",
id: 2,
name: "",
objects: [
{
height: 0,
id: 1,
name: "",
polygon: [
{
x: 0,
y: 0,
},
{
x: 4,
y: 4,
},
{
x: 8,
y: 0,
},
],
rotation: 0,
class: "",
visible: true,
width: 0,
x: 0,
y: 0,
},
{
height: 0,
id: 3,
name: "",
polygon: [
{
x: 0,
y: 0,
},
{
x: 4,
y: 4,
},
{
x: 8,
y: 0,
},
],
rotation: 180,
class: "",
visible: true,
width: 0,
x: 8,
y: 8,
},
],
opacity: 1,
type: "objectgroup",
visible: true,
x: 0,
y: 0,
},
class: "obstacle",
},
// Contains hitboxes for obstacle and lava.
{
id: 4,
objectgroup: {
draworder: "index",
id: 2,
name: "",
objects: [
{
height: 0,
id: 1,
name: "",
polygon: [
{
x: 0,
y: 0,
},
{
x: 0,
y: 8,
},
{
x: 8,
y: 0,
},
],
rotation: 0,
class: "obstacle",
visible: true,
width: 0,
x: 0,
y: 0,
},
{
height: 0,
id: 2,
name: "",
polygon: [
{
x: 0,
y: 0,
},
{
x: 0,
y: 8,
},
{
x: 8,
y: 0,
},
],
rotation: 180,
class: "lava",
visible: true,
width: 0,
x: 8,
y: 8,
},
],
opacity: 1,
type: "objectgroup",
visible: true,
x: 0,
y: 0,
},
},
// Contains hitboxes for lava only
{
id: 5,
objectgroup: {
draworder: "index",
id: 2,
name: "",
objects: [
{
height: 8,
id: 1,
name: "",
rotation: 0,
class: "lava",
visible: true,
width: 8,
x: 0,
y: 0,
},
],
opacity: 1,
type: "objectgroup",
visible: true,
x: 0,
y: 0,
},
class: "lava",
},
],
tilewidth: 8,
type: "tileset",
version: "1.6",
},
],
tilewidth: 8,
type: "map",
version: "1.6",
width: 4,
};
const tileMap: EditableTileMap = TiledTileMapLoader.load(null, tiledMap);
it("can load map dimensions", function () {
expect(tileMap.getDimensionX()).to.be(4);
expect(tileMap.getDimensionY()).to.be(2);
expect(tileMap.getTileHeight()).to.be(8);
expect(tileMap.getTileWidth()).to.be(8);
expect(tileMap.getWidth()).to.be(32);
expect(tileMap.getHeight()).to.be(16);
});
it("can load a tile set with a rectangle collision mask", function () {
const tileDefinition = tileMap.getTileDefinition(0);
expect(tileDefinition).to.be.ok();
expect(tileDefinition.hasTag("obstacle")).to.be(true);
expect(tileDefinition.hasTag("lava")).to.be(false);
expect(tileDefinition.getHitBoxes("obstacle")).to.be.eql([
[
[0, 0],
[0, 8],
[8, 8],
[8, 0],
],
]);
});
it("can load a tile set with an empty collision mask", function () {
const tileDefinition = tileMap.getTileDefinition(1);
expect(tileDefinition).to.be.ok();
expect(tileDefinition.hasTag("obstacle")).to.be(false);
expect(tileDefinition.hasTag("lava")).to.be(false);
});
it("can load a tile set with a polygon collision mask", function () {
{
const tileDefinition = tileMap.getTileDefinition(2);
expect(tileDefinition).to.be.ok();
expect(tileDefinition.hasTag("obstacle")).to.be(true);
expect(tileDefinition.hasTag("lava")).to.be(false);
expect(tileDefinition.getHitBoxes("obstacle")).to.be.eql([
[
[0, 8],
[8, 0],
[0, 0],
],
]);
}
});
it("can load a tile set with a 2 polygons collision mask", function () {
const tileDefinition = tileMap.getTileDefinition(3);
expect(tileDefinition).to.be.ok();
expect(tileDefinition.hasTag("obstacle")).to.be(true);
expect(tileDefinition.hasTag("lava")).to.be(false);
expect(tileDefinition.getHitBoxes("obstacle")).to.be.eql([
[
[0, 0],
[4, 4],
[8, 0],
],
[
[8, 8],
[4, 4],
[0, 8],
],
]);
});
it("can load a tile set with several collision mask filter tags", function () {
const tileDefinition = tileMap.getTileDefinition(4);
expect(tileDefinition).to.be.ok();
expect(tileDefinition.hasTag("obstacle")).to.be(true);
expect(tileDefinition.hasTag("lava")).to.be(true);
expect(tileDefinition.getHitBoxes("obstacle")).to.be.eql([
[
[0, 0],
[0, 8],
[8, 0],
],
]);
expect(tileDefinition.getHitBoxes("lava")).to.be.eql([
[
[8, 8],
[8, 0],
[0, 8],
],
]);
});
it("can load a tile set with only the other filter tag", function () {
const tileDefinition = tileMap.getTileDefinition(5);
expect(tileDefinition).to.be.ok();
expect(tileDefinition.hasTag("obstacle")).to.be(false);
expect(tileDefinition.hasTag("lava")).to.be(true);
expect(tileDefinition.getHitBoxes("lava")).to.be.eql([
[
[0, 0],
[0, 8],
[8, 8],
[8, 0],
],
]);
});
it("can load a tile set", function () {
expect(tileMap.getTileDefinition(6)).not.to.be.ok();
});
it("can load a tile map content", function () {
const layers = new Array(...tileMap.getLayers());
expect(layers.length).to.be(1);
const layer = layers[0] as EditableTileMapLayer;
expect(layer.id).to.be(1);
expect(layer.isVisible()).to.be(true);
expect(layer.get(0, 0)).to.be(0);
expect(layer.get(1, 0)).to.be(2);
expect(layer.get(2, 0)).to.be(3);
expect(layer.get(3, 0)).to.be(4);
expect(layer.get(0, 1)).to.be(2);
expect(layer.get(1, 1)).to.be(1);
expect(layer.get(2, 1)).to.be(undefined);
expect(layer.get(3, 1)).to.be(4);
expect(layer.isFlippedVertically(3, 1)).to.be(true);
expect(layer.isFlippedHorizontally(3, 1)).to.be(false);
expect(layer.isFlippedDiagonally(3, 1)).to.be(false);
});
it("can detect that a point is in a tile that contains a mask with a given tag", function () {
// The point is in the black square with an hitbox.
expect(tileMap.pointIsInsideTile(4, 4, "obstacle")).to.be(true);
// The point is in wite square without any hitbox.
expect(tileMap.pointIsInsideTile(12, 12, "obstacle")).to.be(false);
});
});
});

View File

@@ -0,0 +1,189 @@
import { integer, PolygonVertices } from "../model/CommonTypes";
import {
EditableTileMap,
TileDefinition,
TileObject,
} from "../model/TileMapModel";
import { TiledMap } from "./TiledFormat";
import {
extractTileUidFlippedStates,
decodeBase64LayerData,
getTileIdFromTiledGUI,
} from "./TiledLoaderHelper";
/**
* It creates a {@link EditableTileMap} from a Tiled JSON.
*/
export class TiledTileMapLoader {
static load(pako: any, tiledMap: TiledMap): EditableTileMap | null {
if (!tiledMap.tiledversion) {
console.warn(
"The loaded Tiled map does not contain a 'tiledversion' key. Are you sure this file has been exported from Tiled (mapeditor.org)?"
);
return null;
}
const definitions = new Map<integer, TileDefinition>();
for (const tiledSet of tiledMap.tilesets) {
const firstGid = tiledSet.firstgid === undefined ? 1 : tiledSet.firstgid;
if (tiledSet.tiles) {
for (const tile of tiledSet.tiles) {
const tileDefinition = new TileDefinition(
tile.animation ? tile.animation.length : 0
);
if (tile.objectgroup) {
for (const object of tile.objectgroup.objects) {
const tag = object.class || tile.class;
if (!tag || tag.length === 0) {
continue;
}
let polygon: PolygonVertices | null = null;
if (object.polygon) {
const angle = (object.rotation * Math.PI) / 180;
let cos = Math.cos(angle);
let sin = Math.sin(angle);
// Avoid rounding errors around 0.
if (cos === -1 || cos === 1) {
sin = 0;
}
if (sin === -1 || sin === 1) {
cos = 0;
}
polygon = object.polygon.map((point) => [
object.x + point.x * cos - point.y * sin,
object.y + point.x * sin + point.y * cos,
]);
//TODO check that polygons are convex or split them?
}
// TODO handle ellipses by creating a polygon?
// Make an object property for the number of vertices or always create 8 ones?
// Will the user need the same vertices number for every ellipse?
else if (
object.x !== undefined &&
object.y !== undefined &&
object.width !== undefined &&
object.height !== undefined
) {
polygon = [
[object.x, object.y],
[object.x, object.y + object.height],
[object.x + object.width, object.y + object.height],
[object.x + object.width, object.y],
];
}
if (polygon) {
tileDefinition.add(tag, polygon);
}
}
} else if (tile.class && tile.class.length > 0) {
// When there is no shape, default to the whole tile.
const polygon: PolygonVertices = [
[0, 0],
[0, tiledMap.tileheight],
[tiledMap.tilewidth, tiledMap.tileheight],
[tiledMap.tilewidth, 0],
];
tileDefinition.add(tile.class, polygon);
}
definitions.set(
getTileIdFromTiledGUI(firstGid + tile.id),
tileDefinition
);
}
}
for (let tileIndex = 0; tileIndex < tiledSet.tilecount; tileIndex++) {
const tileId = getTileIdFromTiledGUI(
firstGid + tileIndex
);
if (!definitions.has(tileId)) {
definitions.set(tileId, new TileDefinition(0));
}
}
}
const collisionTileMap = new EditableTileMap(
tiledMap.tilewidth,
tiledMap.tileheight,
tiledMap.width,
tiledMap.height,
definitions
);
for (const tiledLayer of tiledMap.layers) {
if (tiledLayer.type === "objectgroup") {
const objectLayer = collisionTileMap.addObjectLayer(tiledLayer.id);
objectLayer.setVisible(tiledLayer.visible);
for (const tiledObject of tiledLayer.objects) {
if (!tiledObject.visible || !tiledObject.gid) {
// Objects layer are nice to put decorations but dynamic objects
// must be done with GDevelop objects.
// So, there is no point to load it as there won't be any action to
// make objects visible individually.
continue;
}
const tileGid = extractTileUidFlippedStates(tiledObject.gid);
const object = new TileObject(
tiledObject.x,
tiledObject.y,
tileGid.id
);
objectLayer.add(object);
object.setFlippedHorizontally(tileGid.flippedHorizontally);
object.setFlippedVertically(tileGid.flippedVertically);
object.setFlippedDiagonally(tileGid.flippedDiagonally);
}
} else if (tiledLayer.type === "tilelayer") {
let tileSlotIndex = 0;
let layerData: integer[] | null = null;
if (tiledLayer.encoding === "base64") {
layerData = decodeBase64LayerData(pako, tiledLayer);
if (!layerData) {
console.warn("Failed to uncompress layer.data");
}
} else {
layerData = tiledLayer.data as integer[];
}
if (layerData) {
const collisionTileLayer = collisionTileMap.addTileLayer(
tiledLayer.id
);
collisionTileLayer.setVisible(tiledLayer.visible);
// TODO handle layer offset
for (let y = 0; y < tiledLayer.height; y++) {
for (let x = 0; x < tiledLayer.width; x++) {
// The "globalTileUid" is the tile UID with encoded
// bits about the flipping/rotation of the tile.
const globalTileUid = layerData[tileSlotIndex];
// Extract the tile UID and the texture.
const tileUid = extractTileUidFlippedStates(globalTileUid);
if (tileUid.id !== undefined) {
collisionTileLayer.setTile(x, y, tileUid.id);
collisionTileLayer.setFlippedHorizontally(
x,
y,
tileUid.flippedHorizontally
);
collisionTileLayer.setFlippedVertically(
x,
y,
tileUid.flippedVertically
);
collisionTileLayer.setFlippedDiagonally(
x,
y,
tileUid.flippedDiagonally
);
}
tileSlotIndex += 1;
}
}
}
}
}
return collisionTileMap;
}
}

View File

@@ -0,0 +1,26 @@
{
"compileOnSave": false,
"compilerOptions": {
"module": "esnext",
"noImplicitAny": true,
"outDir": "./dist/tsc",
"target": "ES5",
"sourceMap": true,
"declaration": true,
"declarationMap": true,
"declarationDir": "./dts/",
"types" : [
"mocha",
"expect.js",
"offscreencanvas"
],
"lib": ["DOM", "ES5", "ES6"],
"esModuleInterop": false,
"downlevelIteration": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
},
"exclude": [
"node_modules"
]
}

View File

@@ -0,0 +1,29 @@
// https://github.com/pixijs/tilemap
import {
CanvasTileRenderer,
CompositeRectTileLayer,
GraphicsLayer,
IMultiTextureOptions,
MultiTextureResource,
RectTileGeom,
RectTileLayer,
RectTileShader,
TileRenderer,
ZLayer,
} from './pixi-tilemap';
declare module 'pixi.js' {
export namespace tilemap {
export { CanvasTileRenderer };
export { CompositeRectTileLayer };
export { GraphicsLayer };
export { IMultiTextureOptions };
export { MultiTextureResource };
export { RectTileGeom };
export { RectTileLayer };
export { RectTileShader };
export { TileRenderer };
export { ZLayer };
}
}

View File

@@ -0,0 +1,22 @@
import * as PixiModule from "pixi.js";
declare global {
/**
* This namespace contains the `PIXI` module, and should always be used to access to PixiJS apis.
*
* Rationale:
* PixiJS typings are sadly not available anymore as an "ambient namespace" in TypeScript.
* To expose the typings as a global object, we need to use the "export import" syntax.
* This only works in a namespace, that we call GlobalPIXIModule.
*
* This at least allows to easily find in the codebase all the dependencies on PixiJS in the GDJS
* runtime and extensions.
*
* Note that we also modified the bundled `pixi.js` file to create a global variable called
* `GlobalPIXIModule`, containing the `PIXI` object.
* Note that we could use `export as namespace`, but this crash the TypeScript compiler.
*/
namespace GlobalPIXIModule {
export import PIXI = PixiModule;
}
}

View File

@@ -0,0 +1,193 @@
import PIXI = GlobalPIXIModule.PIXI;
export declare class CanvasTileRenderer {
renderer: PIXI.Renderer;
tileAnim: number[];
dontUseTransform: boolean;
constructor(renderer: PIXI.Renderer);
}
export declare class CompositeRectTileLayer extends PIXI.Container {
constructor(zIndex?: number, bitmaps?: Array<PIXI.Texture>, texPerChild?: number);
z: number;
// @ts-ignore Maybe it's a compatibility issue with the PIXI version we are using
zIndex: number;
modificationMarker: number;
shadowColor: Float32Array;
_globalMat: PIXI.Matrix;
_lastLayer: RectTileLayer;
texPerChild: number;
tileAnim: number[];
initialize(zIndex?: number, bitmaps?: Array<PIXI.Texture>, texPerChild?: number): void;
setBitmaps(bitmaps: Array<PIXI.Texture>): void;
clear(): void;
addRect(textureIndex: number, u: number, v: number, x: number, y: number, tileWidth: number, tileHeight: number, animX?: number, animY?: number, rotate?: number, animWidth?: number, animHeight?: number): this;
tileRotate(rotate: number): this;
tileAnimX(offset: number, count: number): this;
tileAnimY(offset: number, count: number): this;
addFrame(texture_: PIXI.Texture | String | number, x: number, y: number, animX?: number, animY?: number, animWidth?: number, animHeight?: number): this;
renderCanvas(renderer: any): void;
render(renderer: PIXI.Renderer): void;
isModified(anim: boolean): boolean;
clearModify(): void;
}
export declare const Constant: {
maxTextures: number;
bufferSize: number;
boundSize: number;
boundCountPerBuffer: number;
use32bitIndex: boolean;
SCALE_MODE: PIXI.SCALE_MODES;
DO_CLEAR: boolean;
};
export declare function fillSamplers(shader: TilemapShader, maxTextures: number): void;
export declare function generateFragmentSrc(maxTextures: number, fragmentSrc: string): string;
export declare function generateSampleSrc(maxTextures: number): string;
export declare class GraphicsLayer extends PIXI.Graphics {
constructor(zIndex: number);
renderCanvas(renderer: any): void;
isModified(anim: boolean): boolean;
clearModify(): void;
}
export declare interface IMultiTextureOptions {
boundCountPerBuffer: number;
boundSize: number;
bufferSize: number;
DO_CLEAR?: boolean;
}
export declare class MultiTextureResource extends PIXI.Resource {
constructor(options: IMultiTextureOptions);
DO_CLEAR: boolean;
boundSize: number;
_clearBuffer: Uint8Array;
bind(baseTexture: PIXI.BaseTexture): void;
baseTex: PIXI.BaseTexture;
boundSprites: Array<PIXI.Sprite>;
dirties: Array<number>;
setTexture(ind: number, texture: PIXI.Texture): void;
upload(renderer: PIXI.Renderer, texture: PIXI.BaseTexture, glTexture: PIXI.GLTexture): boolean;
}
export declare const pixi_tilemap: {
CanvasTileRenderer: typeof CanvasTileRenderer;
CompositeRectTileLayer: typeof CompositeRectTileLayer;
Constant: {
maxTextures: number;
bufferSize: number;
boundSize: number;
boundCountPerBuffer: number;
use32bitIndex: boolean;
SCALE_MODE: PIXI.SCALE_MODES;
DO_CLEAR: boolean;
};
GraphicsLayer: typeof GraphicsLayer;
MultiTextureResource: typeof MultiTextureResource;
RectTileLayer: typeof RectTileLayer;
TilemapShader: typeof TilemapShader;
RectTileShader: typeof RectTileShader;
RectTileGeom: typeof RectTileGeom;
TileRenderer: typeof TileRenderer;
ZLayer: typeof ZLayer;
};
export declare const POINT_STRUCT_SIZE = 12;
export declare class RectTileGeom extends PIXI.Geometry {
vertSize: number;
vertPerQuad: number;
stride: number;
lastTimeAccess: number;
constructor();
buf: PIXI.Buffer;
}
export declare class RectTileLayer extends PIXI.Container {
constructor(zIndex: number, texture: PIXI.Texture | Array<PIXI.Texture>);
// @ts-ignore Maybe it's a compatibility issue with the PIXI version we are using
zIndex: number;
modificationMarker: number;
_$_localBounds: PIXI.Bounds;
shadowColor: Float32Array;
_globalMat: PIXI.Matrix;
pointsBuf: Array<number>;
hasAnim: boolean;
textures: Array<PIXI.Texture>;
offsetX: number;
offsetY: number;
compositeParent: boolean;
initialize(zIndex: number, textures: PIXI.Texture | Array<PIXI.Texture>): void;
clear(): void;
addFrame(texture_: PIXI.Texture | String | number, x: number, y: number, animX: number, animY: number): boolean;
addRect(textureIndex: number, u: number, v: number, x: number, y: number, tileWidth: number, tileHeight: number, animX?: number, animY?: number, rotate?: number, animCountX?: number, animCountY?: number): this;
tileRotate(rotate: number): void;
tileAnimX(offset: number, count: number): void;
tileAnimY(offset: number, count: number): void;
renderCanvas(renderer: any): void;
renderCanvasCore(renderer: any): void;
vbId: number;
vb: RectTileGeom;
vbBuffer: ArrayBuffer;
vbArray: Float32Array;
vbInts: Uint32Array;
destroyVb(): void;
render(renderer: PIXI.Renderer): void;
renderWebGLCore(renderer: PIXI.Renderer, plugin: TileRenderer): void;
isModified(anim: boolean): boolean;
clearModify(): void;
protected _calculateBounds(): void;
getLocalBounds(rect?: PIXI.Rectangle): PIXI.Rectangle;
destroy(options?: any): void;
}
export declare class RectTileShader extends TilemapShader {
constructor(maxTextures: number);
}
export declare abstract class TilemapShader extends PIXI.Shader {
maxTextures: number;
constructor(maxTextures: number, shaderVert: string, shaderFrag: string);
}
export declare class TileRenderer extends PIXI.ObjectRenderer {
renderer: PIXI.Renderer;
gl: WebGLRenderingContext;
sn: number;
indexBuffer: PIXI.Buffer;
ibLen: number;
tileAnim: number[];
texLoc: Array<number>;
rectShader: RectTileShader;
texResources: Array<MultiTextureResource>;
constructor(renderer: PIXI.Renderer);
initBounds(): void;
bindTexturesWithoutRT(renderer: PIXI.Renderer, shader: TilemapShader, textures: Array<PIXI.Texture>): void;
bindTextures(renderer: PIXI.Renderer, shader: TilemapShader, textures: Array<PIXI.Texture>): void;
start(): void;
createVb(): RectTileGeom;
checkIndexBuffer(size: number, vb?: RectTileGeom): void;
getShader(): TilemapShader;
destroy(): void;
}
export declare class ZLayer extends PIXI.Container {
constructor(tilemap: PIXI.Container, zIndex: number);
tilemap: any;
z: number;
// @ts-ignore Maybe it's a compatibility issue with the PIXI version we are using
zIndex: number;
_previousLayers: number;
canvasBuffer: HTMLCanvasElement;
_tempRender: any;
_lastAnimationFrame: number;
layerTransform: PIXI.Matrix;
clear(): void;
cacheIfDirty(): void;
renderCanvas(renderer: any): void;
}

View File

@@ -0,0 +1,21 @@
<svg width="32" height="32" version="1.1" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<g>
<g fill="#243286">
<rect x="8" y="16" width="8" height="8"/>
<rect x="-1.91e-6" y="24" width="8" height="8"/>
<rect x="16" y="24" width="8" height="8"/>
<rect x="24" y="16" width="8" height="8"/>
<rect x="16" y="8" width="8" height="8"/>
<rect x="24" y="4.6e-7" width="8" height="8"/>
</g>
<g fill="#2c7ea1">
<rect x="24" y="24" width="8" height="8"/>
<rect x="16" y="16" width="8" height="8"/>
<rect x="24" y="8" width="8" height="8"/>
<rect x="8" y="24" width="8" height="8"/>
<rect y="16" width="8" height="8"/>
<rect x="8" y="8" width="8" height="8"/>
<rect x="16" width="8" height="8"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 760 B

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" version="1.1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g>
<g transform="scale(.75)">
<g fill="#243286">
<rect x="8" y="16" width="8" height="8"/>
<rect y="24" width="8" height="8"/>
<rect x="16" y="24" width="8" height="8"/>
<rect x="24" y="16" width="8" height="8"/>
<rect x="16" y="8" width="8" height="8"/>
<rect x="24" y="5.47e-7" width="8" height="8"/>
</g>
<g fill="#2c7ea1">
<rect x="24" y="24" width="8" height="8"/>
<rect x="16" y="16" width="8" height="8"/>
<rect x="24" y="8" width="8" height="8"/>
<rect x="8" y="24" width="8" height="8"/>
<rect y="16" width="8" height="8"/>
<rect x="8" y="8" width="8" height="8"/>
<rect x="16" width="8" height="8"/>
</g>
</g>
<g fill="#2c7ea1">
<path d="m19 1v4h4v-4zm1 1h2v2h-2z"/>
<path d="m13 7v4h4v-4zm1 1h2v2h-2z"/>
<path d="m7 13v4h4v-4zm1 1h2v2h-2z"/>
<path d="m1 19v4h4v-4zm1 1h2v2h-2z"/>
</g>
<g fill="#243286" fill-rule="evenodd">
<path d="m17 1-4 4h4zm-1 2.4v0.6h-0.6z"/>
<path d="m23 7-4 4v-4zm-2.4 1h-0.6v0.6z"/>
<path d="m11 7-4 4h4zm-1 2.4v0.6h-0.6z"/>
<path d="m5 13-4 4h4zm-1 2.4v0.6h-0.6z"/>
<path d="m11 19-4 4v-4zm-2.4 1h-0.6v0.6z"/>
<path d="m17 13-4 4v-4zm-2.4 1h-0.6v0.6z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="32" height="32" version="1.1" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<g>
<g fill="#243286">
<rect x="8" y="16" width="8" height="8"/>
<rect y="24" width="8" height="8"/>
<rect x="16" y="24" width="8" height="8"/>
<rect x="24" y="16" width="8" height="8"/>
<rect x="16" y="8" width="8" height="8"/>
<rect x="24" y="5.47e-7" width="8" height="8"/>
</g>
<g fill="#2c7ea1">
<rect x="24" y="24" width="8" height="8"/>
<rect x="16" y="16" width="8" height="8"/>
<rect x="24" y="8" width="8" height="8"/>
<rect x="8" y="24" width="8" height="8"/>
<rect y="16" width="8" height="8"/>
<rect x="8" y="8" width="8" height="8"/>
<rect x="16" width="8" height="8"/>
</g>
<g fill="#2c7ea1">
<path d="m9 17v6h6v-6zm1 1h4v4h-4z"/>
<path d="m17 9v6h6v-6zm1 1h4v4h-4z"/>
<path d="m1 25v6h6v-6zm1 1h4v4h-4z"/>
<path d="m25 1v6h6v-6zm1 1h4v4h-4z"/>
</g>
<g fill="#243286" fill-rule="evenodd">
<path d="m15 9-6 6h6zm-1 2.4v2.6h-2.6z"/>
<path d="m22.6 1-6 6h6zm-1 2.4v2.6h-2.6z"/>
<path d="m7 17-6 6h6zm-1 2.4v2.6h-2.6z"/>
<path d="m31 9-6 6 1e-6 -6zm-2.4 1h-2.6v2.6z"/>
<path d="m23 17-6 6 1e-6 -6zm-2.4 1h-2.6v2.6z"/>
<path d="m15 25-6 6 1e-6 -6zm-2.4 1h-2.6v2.6z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -112,7 +112,7 @@ const jsExtensions = [
// $FlowExpectedError - this path is ignored for Flow.
'pixi-tilemap/dist/pixi-tilemap.umd': require('GDJS-for-web-app-only/Runtime/Extensions/TileMap/pixi-tilemap/dist/pixi-tilemap.umd'),
// $FlowExpectedError - this path is ignored for Flow.
'pixi-tilemap-helper': require('GDJS-for-web-app-only/Runtime/Extensions/TileMap/pixi-tilemap-helper'),
TileMapHelper: require('GDJS-for-web-app-only/Runtime/Extensions/TileMap/helper/TileMapHelper.js'),
// $FlowExpectedError - this path is ignored for Flow.
'pako/dist/pako.min': require('GDJS-for-web-app-only/Runtime/Extensions/TileMap/pako/dist/pako.min'),
},