mirror of
https://github.com/Heretek-AI/GDevelop.git
synced 2026-07-21 09:35:27 -04:00
f76a834284
- Huge tile maps are faster to create and modify at runtime
938 lines
28 KiB
TypeScript
938 lines
28 KiB
TypeScript
/// <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 _layerIndex: integer | null;
|
|
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;
|
|
/**
|
|
* A 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,
|
|
layerIndex: number | null = null
|
|
) {
|
|
this._source = source;
|
|
this.tag = tag;
|
|
this._layerIndex = layerIndex;
|
|
this._layers = new Map<integer, TransformedCollisionTileMapLayer>();
|
|
this._buildLayersFromTileMap(source, this._layers);
|
|
}
|
|
|
|
updateFromTileMap(tileMap: TileMapHelper.EditableTileMap) {
|
|
this._source = tileMap;
|
|
this._layers = new Map<integer, TransformedCollisionTileMapLayer>();
|
|
|
|
this._buildLayersFromTileMap(tileMap, this._layers);
|
|
}
|
|
|
|
_buildLayersFromTileMap(
|
|
tileMap: TileMapHelper.EditableTileMap,
|
|
layers: Map<integer, TransformedCollisionTileMapLayer>
|
|
) {
|
|
if (this._layerIndex) {
|
|
const tileLayer = tileMap.getTileLayer(this._layerIndex);
|
|
if (!tileLayer) {
|
|
return;
|
|
}
|
|
layers.set(
|
|
tileLayer.id,
|
|
new TransformedCollisionTileMapLayer(this, tileLayer)
|
|
);
|
|
} else {
|
|
for (const sourceLayer of tileMap.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;
|
|
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 tile 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;
|
|
}
|
|
|
|
invalidateTile(layerIndex: integer, x: integer, y: integer) {
|
|
const layer = this.getLayer(layerIndex);
|
|
if (layer) {
|
|
const tile = layer.get(x, y);
|
|
if (tile) {
|
|
tile.invalidate();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @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.hasTaggedHitBox(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 = -1;
|
|
|
|
/**
|
|
* 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;
|
|
// Full hit boxes are built on demand.
|
|
if (!definition.hasFullHitBox(tag)) {
|
|
const definitionHitboxes = definition.getHitBoxes(tag);
|
|
if (definitionHitboxes && definitionHitboxes.length) {
|
|
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.getTileId(this.x, this.y)!
|
|
)!;
|
|
}
|
|
|
|
private _isHitboxesUpToDate() {
|
|
return (
|
|
this.affineTransformationUpToDateCount ===
|
|
this.layer.tileMap._transformationUpToDateCount
|
|
);
|
|
}
|
|
|
|
private _setHitboxesUpToDate() {
|
|
this.affineTransformationUpToDateCount =
|
|
this.layer.tileMap._transformationUpToDateCount;
|
|
}
|
|
|
|
invalidate() {
|
|
this.affineTransformationUpToDateCount = -1;
|
|
// Also invalidate neighbors because their hit boxes may need to be
|
|
// extended differently.
|
|
let neighbor = this.layer.get(this.x - 1, this.y);
|
|
if (neighbor) {
|
|
neighbor.affineTransformationUpToDateCount = -1;
|
|
}
|
|
neighbor = this.layer.get(this.x + 1, this.y);
|
|
if (neighbor) {
|
|
neighbor.affineTransformationUpToDateCount = -1;
|
|
}
|
|
neighbor = this.layer.get(this.x, this.y - 1);
|
|
if (neighbor) {
|
|
neighbor.affineTransformationUpToDateCount = -1;
|
|
}
|
|
neighbor = this.layer.get(this.x, this.y + 1);
|
|
if (neighbor) {
|
|
neighbor.affineTransformationUpToDateCount = -1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @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();
|
|
this.hitBoxes.length = 0;
|
|
return this.hitBoxes;
|
|
}
|
|
const tag = this.layer.tileMap.tag;
|
|
const definitionHitboxes = definition.getHitBoxes(tag);
|
|
if (!definitionHitboxes) {
|
|
this._setHitboxesUpToDate();
|
|
this.hitBoxes.length = 0;
|
|
return this.hitBoxes;
|
|
}
|
|
|
|
const tileMap = this.layer.tileMap;
|
|
const width = tileMap.getTileWidth();
|
|
const height = tileMap.getTileHeight();
|
|
|
|
// Extend the hit boxes.
|
|
// It avoids small objects to be pushed side way into a wall when they
|
|
// should be pop out of the wall.
|
|
|
|
const hasFullHitBox =
|
|
definitionHitboxes.length === 1 && definition.hasFullHitBox(tag);
|
|
if (hasFullHitBox) {
|
|
const isLeftFull = this._hasNeighborFullHitBox(-1, 0);
|
|
const isRightFull = this._hasNeighborFullHitBox(1, 0);
|
|
const isTopFull = this._hasNeighborFullHitBox(0, -1);
|
|
const isBottomFull = this._hasNeighborFullHitBox(0, 1);
|
|
|
|
let hitBoxesCount = 0;
|
|
if (isLeftFull || isRightFull) {
|
|
let minX = isLeftFull ? -width : 0;
|
|
let maxX = isRightFull ? 2 * width : width;
|
|
if (hitBoxesCount >= this.hitBoxes.length) {
|
|
this.hitBoxes[hitBoxesCount] = gdjs.Polygon.createRectangle(0, 0);
|
|
}
|
|
TransformedCollisionTile.setRectangle(
|
|
this.hitBoxes[hitBoxesCount],
|
|
minX,
|
|
0,
|
|
maxX,
|
|
height
|
|
);
|
|
hitBoxesCount++;
|
|
}
|
|
if (isTopFull || isBottomFull) {
|
|
let minY = isTopFull ? -height : 0;
|
|
let maxY = isBottomFull ? 2 * height : height;
|
|
if (hitBoxesCount >= this.hitBoxes.length) {
|
|
this.hitBoxes[hitBoxesCount] = gdjs.Polygon.createRectangle(0, 0);
|
|
}
|
|
TransformedCollisionTile.setRectangle(
|
|
this.hitBoxes[hitBoxesCount],
|
|
0,
|
|
minY,
|
|
width,
|
|
maxY
|
|
);
|
|
hitBoxesCount++;
|
|
}
|
|
if (hitBoxesCount === 0) {
|
|
if (this.hitBoxes.length === 0) {
|
|
this.hitBoxes[0] = gdjs.Polygon.createRectangle(0, 0);
|
|
}
|
|
TransformedCollisionTile.setRectangle(
|
|
this.hitBoxes[0],
|
|
0,
|
|
0,
|
|
width,
|
|
height
|
|
);
|
|
hitBoxesCount++;
|
|
}
|
|
this.hitBoxes.length = hitBoxesCount;
|
|
} else {
|
|
for (
|
|
let polygonIndex = 0;
|
|
polygonIndex < definitionHitboxes.length;
|
|
polygonIndex++
|
|
) {
|
|
const defPolygon = definitionHitboxes[polygonIndex];
|
|
if (polygonIndex >= this.hitBoxes.length) {
|
|
// This can't happen in practice as only the simple tile map can be
|
|
// modify and it only contains full hit boxes.
|
|
this.hitBoxes[polygonIndex] = gdjs.Polygon.createRectangle(0, 0);
|
|
}
|
|
const polygon = this.hitBoxes[polygonIndex];
|
|
|
|
for (
|
|
let vertexIndex = 0;
|
|
vertexIndex < polygon.vertices.length;
|
|
vertexIndex++
|
|
) {
|
|
const defVertex = defPolygon[vertexIndex];
|
|
const vertex = polygon.vertices[vertexIndex];
|
|
|
|
vertex[0] = defVertex[0];
|
|
vertex[1] = defVertex[1];
|
|
}
|
|
}
|
|
}
|
|
|
|
// Transform the hit boxes.
|
|
// First compute the affine transformation relative to the tile.
|
|
// Then the transformation is applied to each polygon in the hitboxes.
|
|
// TODO: If the tile contains hitboxes that are both extended and not full,
|
|
// this code should be adapted so that the transformation considers the
|
|
// actual width and height of the hitbox (and not the tile).
|
|
// At the moment, extended hitboxes can only be full and since flipping
|
|
// transformations don't change a full hitbox, those transformations
|
|
// are not applied.
|
|
|
|
const tileTransformation =
|
|
TransformedCollisionTile.workingTransformation;
|
|
tileTransformation.setToTranslation(width * this.x, height * this.y);
|
|
if (!hasFullHitBox) {
|
|
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.flipX(width / 2);
|
|
tileTransformation.rotateAround(Math.PI / 2, width / 2, height / 2);
|
|
}
|
|
}
|
|
tileTransformation.preConcatenate(tileMap.getTransformation());
|
|
for (
|
|
let polygonIndex = 0;
|
|
polygonIndex < this.hitBoxes.length;
|
|
polygonIndex++
|
|
) {
|
|
const polygon = this.hitBoxes[polygonIndex];
|
|
|
|
for (
|
|
let vertexIndex = 0;
|
|
vertexIndex < polygon.vertices.length;
|
|
vertexIndex++
|
|
) {
|
|
const vertex = polygon.vertices[vertexIndex];
|
|
|
|
tileTransformation.transform(vertex, vertex);
|
|
}
|
|
}
|
|
this._setHitboxesUpToDate();
|
|
return this.hitBoxes;
|
|
}
|
|
|
|
private _hasNeighborFullHitBox(deltaX: integer, deltaY: integer) {
|
|
const sourceLayer = this.layer._source;
|
|
const tileId = sourceLayer.getTileId(this.x + deltaX, this.y + deltaY);
|
|
const tileDefinition =
|
|
tileId && this.layer.tileMap.getTileDefinition(tileId);
|
|
return (
|
|
tileDefinition && tileDefinition.hasFullHitBox(this.layer.tileMap.tag)
|
|
);
|
|
}
|
|
|
|
private static setRectangle(
|
|
polygon: gdjs.Polygon,
|
|
minX: float,
|
|
minY: float,
|
|
maxX: float,
|
|
maxY: float
|
|
) {
|
|
const vertices = polygon.vertices;
|
|
vertices[0][0] = minX;
|
|
vertices[0][1] = minY;
|
|
vertices[1][0] = maxX;
|
|
vertices[1][1] = minY;
|
|
vertices[2][0] = maxX;
|
|
vertices[2][1] = maxY;
|
|
vertices[3][0] = minX;
|
|
vertices[3][1] = maxY;
|
|
}
|
|
}
|
|
}
|
|
}
|