mirror of
https://github.com/Heretek-AI/GDevelop.git
synced 2026-08-25 12:23:58 -04:00
572 lines
17 KiB
TypeScript
572 lines
17 KiB
TypeScript
/// <reference path="helper/TileMapHelper.d.ts" />
|
|
namespace gdjs {
|
|
/**
|
|
* Displays a Tilemap object (LDtk and Tiled).
|
|
* @category Objects > Tile Map
|
|
*/
|
|
export abstract class AbstractTileMapRuntimeObject
|
|
extends gdjs.RuntimeObject
|
|
implements gdjs.TileMap
|
|
{
|
|
/**
|
|
* A reusable Point to avoid allocations.
|
|
*/
|
|
private static readonly workingPoint: FloatPoint = [0, 0];
|
|
|
|
private _sceneToTileMapTransformation: gdjs.AffineTransformation =
|
|
new gdjs.AffineTransformation();
|
|
private _tileMapToSceneTransformation: gdjs.AffineTransformation =
|
|
new gdjs.AffineTransformation();
|
|
private _transformationIsUpToDate: boolean = false;
|
|
|
|
constructor(
|
|
instanceContainer: gdjs.RuntimeInstanceContainer,
|
|
objectData: ObjectData,
|
|
instanceData?: InstanceData
|
|
) {
|
|
super(instanceContainer, objectData, instanceData);
|
|
}
|
|
|
|
abstract getTileMap(): TileMapHelper.EditableTileMap | null;
|
|
|
|
/**
|
|
* Return a tile map that is safe to modify.
|
|
*/
|
|
abstract getTileMapForEdition(): TileMapHelper.EditableTileMap | null;
|
|
|
|
abstract getCollisionTileMap(): gdjs.TileMap.TransformedCollisionTileMap | null;
|
|
|
|
/**
|
|
* Return the layer that is used to edit and read tiles.
|
|
*/
|
|
abstract getEditedLayerIndex(): integer;
|
|
|
|
abstract getCollisionMaskTag(): string;
|
|
|
|
abstract getScaleX(): float;
|
|
|
|
abstract getScaleY(): float;
|
|
|
|
abstract invalidateTileMap(): void;
|
|
|
|
abstract getTileSetColumnCount(): integer;
|
|
|
|
abstract getTileSetRowCount(): integer;
|
|
|
|
/**
|
|
* This method is expensive and should not be called.
|
|
* Prefer using {@link getHitBoxesAround} rather than getHitBoxes.
|
|
*/
|
|
override getHitBoxes(): gdjs.Polygon[] {
|
|
return super.getHitBoxes();
|
|
}
|
|
|
|
override updateHitBoxes(): void {
|
|
this.updateTransformation();
|
|
const collisionTileMap = this.getCollisionTileMap();
|
|
if (!collisionTileMap) return;
|
|
this.hitBoxes = Array.from(
|
|
collisionTileMap.getAllHitboxes(this.getCollisionMaskTag())
|
|
);
|
|
this.hitBoxesDirty = false;
|
|
this.updateAABB();
|
|
}
|
|
|
|
// This implementation doesn't use updateHitBoxes.
|
|
// It's important for good performances.
|
|
override getAABB(): AABB {
|
|
// It's fine to compute it every time because tile maps are rarely rotated.
|
|
// It avoids calling updateHitBoxes to rely on hitBoxesDirty to know when
|
|
// to update.
|
|
this.updateAABB();
|
|
return this.aabb;
|
|
}
|
|
|
|
// This implementation doesn't use updateHitBoxes.
|
|
// It's important for good performances.
|
|
override 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 collisionTileMap = this.getCollisionTileMap();
|
|
if (!collisionTileMap) return;
|
|
const affineTransformation = collisionTileMap.getTransformation();
|
|
|
|
const left = 0;
|
|
const right = collisionTileMap.getWidth();
|
|
const top = 0;
|
|
const bottom = 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
|
|
);
|
|
}
|
|
}
|
|
|
|
getHitBoxesAround(
|
|
left: float,
|
|
top: float,
|
|
right: float,
|
|
bottom: float
|
|
): Iterable<gdjs.Polygon> {
|
|
// 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();
|
|
const collisionTileMap = this.getCollisionTileMap();
|
|
if (!collisionTileMap) {
|
|
return [];
|
|
}
|
|
return collisionTileMap.getHitboxesAround(
|
|
this.getCollisionMaskTag(),
|
|
left,
|
|
top,
|
|
right,
|
|
bottom
|
|
);
|
|
}
|
|
|
|
override isSpatiallyIndexed(): boolean {
|
|
return true;
|
|
}
|
|
|
|
invalidateTransformation(): void {
|
|
this._transformationIsUpToDate = false;
|
|
}
|
|
|
|
updateTransformation() {
|
|
if (this._transformationIsUpToDate) {
|
|
return;
|
|
}
|
|
|
|
this._tileMapToSceneTransformation.setToIdentity();
|
|
this._tileMapToSceneTransformation.translate(this.getX(), this.getY());
|
|
this._tileMapToSceneTransformation.rotateAround(
|
|
(this.getAngle() * Math.PI) / 180,
|
|
this.getCenterX(),
|
|
this.getCenterY()
|
|
);
|
|
this._tileMapToSceneTransformation.scale(
|
|
Math.abs(this.getScaleX()),
|
|
Math.abs(this.getScaleY())
|
|
);
|
|
const collisionTileMap = this.getCollisionTileMap();
|
|
if (collisionTileMap) {
|
|
const collisionTileMapTransformation =
|
|
collisionTileMap.getTransformation();
|
|
collisionTileMapTransformation.copyFrom(
|
|
this._tileMapToSceneTransformation
|
|
);
|
|
collisionTileMap.setTransformation(collisionTileMapTransformation);
|
|
}
|
|
this._sceneToTileMapTransformation.copyFrom(
|
|
this._tileMapToSceneTransformation
|
|
);
|
|
this._sceneToTileMapTransformation.invert();
|
|
this._transformationIsUpToDate = true;
|
|
}
|
|
|
|
/**
|
|
* The returned array is alway the same.
|
|
*/
|
|
getGridCoordinatesFromSceneCoordinates(
|
|
x: float,
|
|
y: float
|
|
): [integer, integer] {
|
|
this.updateTransformation();
|
|
const result = AbstractTileMapRuntimeObject.workingPoint;
|
|
const tileMap = this.getTileMap();
|
|
if (!tileMap) {
|
|
result[0] = 0;
|
|
result[1] = 0;
|
|
return result;
|
|
}
|
|
this._sceneToTileMapTransformation.transform([x, y], result);
|
|
|
|
result[0] = Math.floor(result[0] / tileMap.getTileWidth());
|
|
result[1] = Math.floor(result[1] / tileMap.getTileHeight());
|
|
return result;
|
|
}
|
|
|
|
getGridRowCount(): integer {
|
|
const tileMap = this.getTileMap();
|
|
return tileMap ? tileMap.getDimensionY() : 0;
|
|
}
|
|
|
|
getGridColumnCount(): integer {
|
|
const tileMap = this.getTileMap();
|
|
return tileMap ? tileMap.getDimensionX() : 0;
|
|
}
|
|
|
|
/**
|
|
* @param x The layer column.
|
|
* @param y The layer row.
|
|
* @param layerIndex The layer index.
|
|
* @returns The tile's id.
|
|
*/
|
|
private getTileId(x: integer, y: integer, layerIndex: integer): integer {
|
|
const tileMap = this.getTileMap();
|
|
if (!tileMap) return -1;
|
|
return tileMap.getTileId(x, y, layerIndex);
|
|
}
|
|
|
|
/**
|
|
* @param x The layer column.
|
|
* @param y The layer row.
|
|
* @param layerIndex The layer index.
|
|
*/
|
|
private isTileFlippedOnX(
|
|
x: integer,
|
|
y: integer,
|
|
layerIndex: integer
|
|
): boolean {
|
|
const tileMap = this.getTileMap();
|
|
if (!tileMap) return false;
|
|
return tileMap.isTileFlippedOnX(x, y, layerIndex);
|
|
}
|
|
|
|
/**
|
|
* @param x The layer column.
|
|
* @param y The layer row.
|
|
* @param layerIndex The layer index.
|
|
*/
|
|
private isTileFlippedOnY(
|
|
x: integer,
|
|
y: integer,
|
|
layerIndex: integer
|
|
): boolean {
|
|
const tileMap = this.getTileMap();
|
|
if (!tileMap) return false;
|
|
return tileMap.isTileFlippedOnY(x, y, layerIndex);
|
|
}
|
|
|
|
getSceneXCoordinateOfTileCenter(
|
|
columnIndex: integer,
|
|
rowIndex: integer
|
|
): float {
|
|
const tileMap = this.getTileMap();
|
|
if (!tileMap) {
|
|
return 0;
|
|
}
|
|
const sceneCoordinates: FloatPoint =
|
|
AbstractTileMapRuntimeObject.workingPoint;
|
|
this._tileMapToSceneTransformation.transform(
|
|
[
|
|
(columnIndex + 0.5) * tileMap.getTileWidth(),
|
|
(rowIndex + 0.5) * tileMap.getTileHeight(),
|
|
],
|
|
sceneCoordinates
|
|
);
|
|
return sceneCoordinates[0];
|
|
}
|
|
|
|
getSceneYCoordinateOfTileCenter(
|
|
columnIndex: integer,
|
|
rowIndex: integer
|
|
): float {
|
|
const tileMap = this.getTileMap();
|
|
if (!tileMap) {
|
|
return 0;
|
|
}
|
|
const sceneCoordinates: FloatPoint =
|
|
AbstractTileMapRuntimeObject.workingPoint;
|
|
this._tileMapToSceneTransformation.transform(
|
|
[
|
|
(columnIndex + 0.5) * tileMap.getTileWidth(),
|
|
(rowIndex + 0.5) * tileMap.getTileHeight(),
|
|
],
|
|
sceneCoordinates
|
|
);
|
|
return sceneCoordinates[1];
|
|
}
|
|
|
|
getColumnIndexAtPosition(x: float, y: float): integer {
|
|
return this.getGridCoordinatesFromSceneCoordinates(x, y)[0];
|
|
}
|
|
|
|
getRowIndexAtPosition(x: float, y: float): integer {
|
|
return this.getGridCoordinatesFromSceneCoordinates(x, y)[1];
|
|
}
|
|
|
|
getTileAtPosition(x: float, y: float): integer {
|
|
const [columnIndex, rowIndex] =
|
|
this.getGridCoordinatesFromSceneCoordinates(x, y);
|
|
return this.getTileAtGridCoordinates(columnIndex, rowIndex);
|
|
}
|
|
|
|
getTileAtGridCoordinates(columnIndex: integer, rowIndex: integer): integer {
|
|
return this.getTileId(columnIndex, rowIndex, this.getEditedLayerIndex());
|
|
}
|
|
|
|
setTileAtPosition(tileId: number, x: float, y: float) {
|
|
const [columnIndex, rowIndex] =
|
|
this.getGridCoordinatesFromSceneCoordinates(x, y);
|
|
this.setTileAtGridCoordinates(tileId, columnIndex, rowIndex);
|
|
}
|
|
|
|
setTileAtGridCoordinates(
|
|
tileId: number,
|
|
columnIndex: integer,
|
|
rowIndex: integer
|
|
) {
|
|
const tileMap = this.getTileMapForEdition();
|
|
if (!tileMap) {
|
|
return;
|
|
}
|
|
const layer = tileMap.getTileLayer(this.getEditedLayerIndex());
|
|
if (!layer) {
|
|
return;
|
|
}
|
|
const oldTileId = layer.getTileId(columnIndex, rowIndex);
|
|
if (tileId === oldTileId) {
|
|
return;
|
|
}
|
|
layer.setTile(columnIndex, rowIndex, tileId);
|
|
|
|
const collisionTileMap = this.getCollisionTileMap();
|
|
if (collisionTileMap) {
|
|
const oldTileDefinition =
|
|
oldTileId !== undefined && tileMap.getTileDefinition(oldTileId);
|
|
const newTileDefinition = tileMap.getTileDefinition(tileId);
|
|
const hadFullHitBox =
|
|
!!oldTileDefinition &&
|
|
oldTileDefinition.hasFullHitBox(this.getCollisionMaskTag());
|
|
const haveFullHitBox =
|
|
!!newTileDefinition &&
|
|
newTileDefinition.hasFullHitBox(this.getCollisionMaskTag());
|
|
if (hadFullHitBox !== haveFullHitBox) {
|
|
collisionTileMap.invalidateTile(
|
|
this.getEditedLayerIndex(),
|
|
columnIndex,
|
|
rowIndex
|
|
);
|
|
}
|
|
}
|
|
this.invalidateTileMap();
|
|
}
|
|
|
|
flipTileOnYAtPosition(x: float, y: float, flip: boolean) {
|
|
const [columnIndex, rowIndex] =
|
|
this.getGridCoordinatesFromSceneCoordinates(x, y);
|
|
this.flipTileOnYAtGridCoordinates(columnIndex, rowIndex, flip);
|
|
}
|
|
|
|
flipTileOnXAtPosition(x: float, y: float, flip: boolean) {
|
|
const [columnIndex, rowIndex] =
|
|
this.getGridCoordinatesFromSceneCoordinates(x, y);
|
|
this.flipTileOnXAtGridCoordinates(columnIndex, rowIndex, flip);
|
|
}
|
|
|
|
flipTileOnYAtGridCoordinates(
|
|
columnIndex: integer,
|
|
rowIndex: integer,
|
|
flip: boolean
|
|
) {
|
|
this.flipTileOnY(columnIndex, rowIndex, this.getEditedLayerIndex(), flip);
|
|
this.invalidateTileMap();
|
|
// No need to invalidate hit boxes since at the moment, collision mask
|
|
// cannot be configured on each tile.
|
|
}
|
|
|
|
flipTileOnXAtGridCoordinates(
|
|
columnIndex: integer,
|
|
rowIndex: integer,
|
|
flip: boolean
|
|
) {
|
|
this.flipTileOnX(columnIndex, rowIndex, this.getEditedLayerIndex(), flip);
|
|
this.invalidateTileMap();
|
|
// No need to invalidate hit boxes since at the moment, collision mask
|
|
// cannot be configured on each tile.
|
|
}
|
|
|
|
/**
|
|
* @param x The layer column.
|
|
* @param y The layer row.
|
|
* @param layerIndex The layer index.
|
|
* @param flip true if the tile should be flipped.
|
|
*/
|
|
private flipTileOnY(
|
|
x: integer,
|
|
y: integer,
|
|
layerIndex: integer,
|
|
flip: boolean
|
|
) {
|
|
const tileMap = this.getTileMapForEdition();
|
|
if (!tileMap) return;
|
|
tileMap.flipTileOnY(x, y, layerIndex, flip);
|
|
}
|
|
|
|
/**
|
|
* @param x The layer column.
|
|
* @param y The layer row.
|
|
* @param layerIndex The layer index.
|
|
* @param flip true if the tile should be flipped.
|
|
*/
|
|
private flipTileOnX(
|
|
x: integer,
|
|
y: integer,
|
|
layerIndex: integer,
|
|
flip: boolean
|
|
) {
|
|
const tileMap = this.getTileMapForEdition();
|
|
if (!tileMap) return;
|
|
tileMap.flipTileOnX(x, y, layerIndex, flip);
|
|
}
|
|
|
|
isTileFlippedOnXAtPosition(x: float, y: float) {
|
|
const [columnIndex, rowIndex] =
|
|
this.getGridCoordinatesFromSceneCoordinates(x, y);
|
|
|
|
return this.isTileFlippedOnX(
|
|
columnIndex,
|
|
rowIndex,
|
|
this.getEditedLayerIndex()
|
|
);
|
|
}
|
|
|
|
isTileFlippedOnXAtGridCoordinates(columnIndex: integer, rowIndex: integer) {
|
|
return this.isTileFlippedOnX(
|
|
columnIndex,
|
|
rowIndex,
|
|
this.getEditedLayerIndex()
|
|
);
|
|
}
|
|
|
|
isTileFlippedOnYAtPosition(x: float, y: float) {
|
|
const [columnIndex, rowIndex] =
|
|
this.getGridCoordinatesFromSceneCoordinates(x, y);
|
|
|
|
return this.isTileFlippedOnY(
|
|
columnIndex,
|
|
rowIndex,
|
|
this.getEditedLayerIndex()
|
|
);
|
|
}
|
|
|
|
isTileFlippedOnYAtGridCoordinates(columnIndex: integer, rowIndex: integer) {
|
|
return this.isTileFlippedOnY(
|
|
columnIndex,
|
|
rowIndex,
|
|
this.getEditedLayerIndex()
|
|
);
|
|
}
|
|
|
|
removeTileAtPosition(x: float, y: float) {
|
|
const [columnIndex, rowIndex] =
|
|
this.getGridCoordinatesFromSceneCoordinates(x, y);
|
|
this.removeTileAtGridCoordinates(columnIndex, rowIndex);
|
|
}
|
|
|
|
removeTileAtGridCoordinates(columnIndex: integer, rowIndex: integer) {
|
|
const tileMap = this.getTileMapForEdition();
|
|
if (!tileMap) {
|
|
return;
|
|
}
|
|
const layer = tileMap.getTileLayer(this.getEditedLayerIndex());
|
|
if (!layer) {
|
|
return;
|
|
}
|
|
const oldTileId = layer.getTileId(columnIndex, rowIndex);
|
|
if (oldTileId === undefined) {
|
|
return;
|
|
}
|
|
layer.removeTile(columnIndex, rowIndex);
|
|
|
|
const collisionTileMap = this.getCollisionTileMap();
|
|
if (collisionTileMap) {
|
|
const oldTileDefinition =
|
|
oldTileId !== undefined && tileMap.getTileDefinition(oldTileId);
|
|
const hadFullHitBox =
|
|
!!oldTileDefinition &&
|
|
oldTileDefinition.hasFullHitBox(this.getCollisionMaskTag());
|
|
if (hadFullHitBox) {
|
|
collisionTileMap.invalidateTile(
|
|
this.getEditedLayerIndex(),
|
|
columnIndex,
|
|
rowIndex
|
|
);
|
|
}
|
|
}
|
|
this.invalidateTileMap();
|
|
}
|
|
|
|
setGridRowCount(targetRowCount: integer) {
|
|
const tileMap = this.getTileMapForEdition();
|
|
if (targetRowCount <= 0) return;
|
|
if (!tileMap) return;
|
|
tileMap.setDimensionY(targetRowCount);
|
|
const collisionTileMap = this.getCollisionTileMap();
|
|
if (collisionTileMap) {
|
|
collisionTileMap.updateDimensions();
|
|
}
|
|
this.invalidateTileMap();
|
|
this.invalidateHitboxes();
|
|
}
|
|
|
|
setGridColumnCount(targetColumnCount: integer) {
|
|
const tileMap = this.getTileMapForEdition();
|
|
if (targetColumnCount <= 0) return;
|
|
if (!tileMap) return;
|
|
tileMap.setDimensionX(targetColumnCount);
|
|
const collisionTileMap = this.getCollisionTileMap();
|
|
if (collisionTileMap) {
|
|
collisionTileMap.updateDimensions();
|
|
}
|
|
this.invalidateTileMap();
|
|
this.invalidateHitboxes();
|
|
}
|
|
}
|
|
}
|