mirror of
https://github.com/Heretek-AI/GDevelop.git
synced 2026-07-21 01:25:32 -04:00
f76a834284
- Huge tile maps are faster to create and modify at runtime
955 lines
29 KiB
TypeScript
955 lines
29 KiB
TypeScript
/// <reference path="helper/TileMapHelper.d.ts" />
|
|
namespace gdjs {
|
|
/**
|
|
* @category Objects > Tile Map
|
|
*/
|
|
export type SimpleTileMapObjectDataType = {
|
|
content: {
|
|
atlasImage: string;
|
|
rowCount: number;
|
|
columnCount: number;
|
|
tileSize: number;
|
|
tilesWithHitBox: string;
|
|
};
|
|
};
|
|
|
|
/**
|
|
* @category Objects > Tile Map
|
|
*/
|
|
export type SimpleTileMapObjectData = ObjectData &
|
|
SimpleTileMapObjectDataType;
|
|
|
|
/**
|
|
* @category Objects > Tile Map
|
|
*/
|
|
export type SimpleTileMapNetworkSyncDataType = {
|
|
op: number;
|
|
tm?: TileMapHelper.EditableTileMapAsJsObject;
|
|
};
|
|
|
|
/**
|
|
* @category Objects > Tile Map
|
|
*/
|
|
export type SimpleTileMapNetworkSyncData = ObjectNetworkSyncData &
|
|
SimpleTileMapNetworkSyncDataType;
|
|
|
|
/**
|
|
* Displays a SimpleTileMap object.
|
|
* @category Objects > Tile Map
|
|
*/
|
|
export class SimpleTileMapRuntimeObject
|
|
extends gdjs.RuntimeObject
|
|
implements gdjs.Resizable, gdjs.Scalable, gdjs.OpacityHandler
|
|
{
|
|
/**
|
|
* A reusable Point to avoid allocations.
|
|
*/
|
|
private static readonly workingPoint: FloatPoint = [0, 0];
|
|
|
|
_opacity: float = 255;
|
|
_atlasImage: string;
|
|
_tileMapManager: gdjs.TileMap.TileMapRuntimeManager;
|
|
_tileMap: TileMapHelper.EditableTileMap | null = null;
|
|
_renderer: gdjs.TileMapRuntimeObjectPixiRenderer;
|
|
readonly _rowCount: number;
|
|
readonly _columnCount: number;
|
|
readonly _tileSize: number;
|
|
_displayMode = 'all';
|
|
_layerIndex = 0;
|
|
_initialTileMapAsJsObject: TileMapHelper.EditableTileMapAsJsObject;
|
|
readonly _initialTilesWithHitBox: number[];
|
|
_isTileMapDirty: boolean = false;
|
|
_sceneToTileMapTransformation: gdjs.AffineTransformation =
|
|
new gdjs.AffineTransformation();
|
|
_tileMapToSceneTransformation: gdjs.AffineTransformation =
|
|
new gdjs.AffineTransformation();
|
|
_collisionTileMap: gdjs.TileMap.TransformedCollisionTileMap | null = null;
|
|
_hitBoxTag: string = 'collision';
|
|
private _transformationIsUpToDate: boolean = false;
|
|
|
|
// TODO: Add a debug mode like for TileMapCollisionMaskRuntimeObject to draw?
|
|
|
|
constructor(
|
|
instanceContainer: gdjs.RuntimeInstanceContainer,
|
|
objectData: ObjectData & SimpleTileMapObjectDataType,
|
|
instanceData?: InstanceData
|
|
) {
|
|
super(instanceContainer, objectData, instanceData);
|
|
this._atlasImage = objectData.content.atlasImage;
|
|
this._rowCount = objectData.content.rowCount;
|
|
this._columnCount = objectData.content.columnCount;
|
|
this._tileSize = objectData.content.tileSize;
|
|
this._initialTileMapAsJsObject = {
|
|
tileWidth: this._tileSize,
|
|
tileHeight: this._tileSize,
|
|
dimX: 1,
|
|
dimY: 1,
|
|
layers: [{ id: 0, alpha: this._opacity / 255, tiles: [] }],
|
|
};
|
|
this._initialTilesWithHitBox = (
|
|
objectData.content.tilesWithHitBox as string
|
|
)
|
|
.split(',')
|
|
.filter((id) => !!id)
|
|
.map((idAsString) => parseInt(idAsString, 10));
|
|
this._tileMapManager =
|
|
gdjs.TileMap.TileMapRuntimeManager.getManager(instanceContainer);
|
|
this._renderer = new gdjs.TileMapRuntimeObjectRenderer(
|
|
this,
|
|
instanceContainer
|
|
);
|
|
|
|
this._loadTileMap(
|
|
this._initialTileMapAsJsObject,
|
|
(tileMap: TileMapHelper.EditableTileMap) => {
|
|
this._renderer.updatePosition();
|
|
|
|
this._collisionTileMap = new gdjs.TileMap.TransformedCollisionTileMap(
|
|
tileMap,
|
|
this._hitBoxTag
|
|
);
|
|
|
|
this.updateTransformation();
|
|
}
|
|
);
|
|
|
|
// *ALWAYS* call `this.onCreated()` at the very end of your object constructor.
|
|
this.onCreated();
|
|
}
|
|
|
|
getRendererObject() {
|
|
return this._renderer.getRendererObject();
|
|
}
|
|
|
|
updateTileMap(forceUpdate: boolean): void {
|
|
this._tileMapManager.getOrLoadSimpleTileMapTextureCache(
|
|
(textureName) => {
|
|
return this.getInstanceContainer()
|
|
.getGame()
|
|
.getImageManager()
|
|
.getPIXITexture(
|
|
textureName
|
|
) as unknown as PIXI.BaseTexture<PIXI.Resource>;
|
|
},
|
|
this._atlasImage,
|
|
this._tileSize,
|
|
this._columnCount,
|
|
this._rowCount,
|
|
(textureCache: TileMapHelper.TileTextureCache | null) => {
|
|
if (!textureCache) {
|
|
// getOrLoadTextureCache already log warns and errors.
|
|
return;
|
|
}
|
|
this._renderer.refreshPixiTileMap(textureCache, forceUpdate);
|
|
},
|
|
(error) => {
|
|
console.error(
|
|
`Could not load texture cache for atlas ${this._atlasImage} during prerender. The tilemap might be badly configured or an issues happened with the loaded atlas image:`,
|
|
error
|
|
);
|
|
}
|
|
);
|
|
}
|
|
|
|
updatePreRender(instanceContainer: gdjs.RuntimeInstanceContainer): void {
|
|
if (!this.isHidden()) {
|
|
this.updateTileMap(this._isTileMapDirty);
|
|
this._isTileMapDirty = false;
|
|
}
|
|
}
|
|
|
|
updateFromObjectData(
|
|
oldObjectData: SimpleTileMapObjectData,
|
|
newObjectData: SimpleTileMapObjectData
|
|
): boolean {
|
|
if (
|
|
oldObjectData.content.atlasImage !== newObjectData.content.atlasImage
|
|
) {
|
|
// TODO: support changing the atlas texture
|
|
return false;
|
|
}
|
|
// Map content is updated at hot-reload by extraInitializationFromInitialInstance.
|
|
return true;
|
|
}
|
|
|
|
getNetworkSyncData(
|
|
syncOptions: GetNetworkSyncDataOptions
|
|
): SimpleTileMapNetworkSyncData {
|
|
const syncData: SimpleTileMapNetworkSyncData = {
|
|
...super.getNetworkSyncData(syncOptions),
|
|
op: this._opacity,
|
|
};
|
|
if (this._tileMap && syncOptions.syncFullTileMaps) {
|
|
const currentTileMapAsJsObject = this._tileMap.toJSObject();
|
|
syncData.tm = currentTileMapAsJsObject;
|
|
}
|
|
|
|
return syncData;
|
|
}
|
|
|
|
updateFromNetworkSyncData(
|
|
networkSyncData: SimpleTileMapNetworkSyncData,
|
|
options: UpdateFromNetworkSyncDataOptions
|
|
): void {
|
|
super.updateFromNetworkSyncData(networkSyncData, options);
|
|
|
|
if (networkSyncData.tm !== undefined) {
|
|
this._loadTileMap(
|
|
networkSyncData.tm,
|
|
(tileMap: TileMapHelper.EditableTileMap) => {
|
|
if (networkSyncData.w !== undefined) {
|
|
this.setWidth(networkSyncData.w);
|
|
}
|
|
if (networkSyncData.h !== undefined) {
|
|
this.setHeight(networkSyncData.h);
|
|
}
|
|
if (networkSyncData.op !== undefined) {
|
|
this.setOpacity(networkSyncData.op);
|
|
}
|
|
|
|
// 4. Update position (calculations based on renderer's dimensions).
|
|
this._renderer.updatePosition();
|
|
|
|
if (this._collisionTileMap) {
|
|
// If collision tile map is already defined, only update it.
|
|
this._collisionTileMap.updateFromTileMap(tileMap);
|
|
} else {
|
|
this._collisionTileMap =
|
|
new gdjs.TileMap.TransformedCollisionTileMap(
|
|
tileMap,
|
|
this._hitBoxTag
|
|
);
|
|
}
|
|
|
|
this.updateTransformation();
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
extraInitializationFromInitialInstance(
|
|
initialInstanceData: InstanceData
|
|
): void {
|
|
// 1. load the tilemap from the instance.
|
|
for (const property of initialInstanceData.stringProperties) {
|
|
if (property.name === 'tilemap') {
|
|
this._initialTileMapAsJsObject = JSON.parse(property.value);
|
|
}
|
|
}
|
|
|
|
// 2. Update the renderer so that it updates the tilemap object
|
|
// (used for width and position calculations).
|
|
this._loadTileMap(
|
|
this._initialTileMapAsJsObject,
|
|
(tileMap: TileMapHelper.EditableTileMap) => {
|
|
// 3. Set custom dimensions & opacity if applicable.
|
|
if (initialInstanceData.customSize) {
|
|
this.setWidth(initialInstanceData.width);
|
|
this.setHeight(initialInstanceData.height);
|
|
}
|
|
this.setOpacity(
|
|
initialInstanceData.opacity === undefined
|
|
? 255
|
|
: initialInstanceData.opacity
|
|
);
|
|
|
|
// 4. Update position (calculations based on renderer's dimensions).
|
|
this._renderer.updatePosition();
|
|
|
|
if (this._collisionTileMap) {
|
|
// If collision tile map is already defined, there's a good chance it means
|
|
// extraInitializationFromInitialInstance is called when hot reloading the
|
|
// scene so the collision is tile map is updated instead of being re-created.
|
|
this._collisionTileMap.updateFromTileMap(tileMap);
|
|
} else {
|
|
this._collisionTileMap =
|
|
new gdjs.TileMap.TransformedCollisionTileMap(
|
|
tileMap,
|
|
this._hitBoxTag
|
|
);
|
|
}
|
|
|
|
this._transformationIsUpToDate = false;
|
|
this.updateTransformation();
|
|
this.invalidateHitboxes();
|
|
}
|
|
);
|
|
}
|
|
|
|
private _loadTileMap(
|
|
tileMapAsJsObject: TileMapHelper.EditableTileMapAsJsObject,
|
|
tileMapLoadingCallback: (tileMap: TileMapHelper.EditableTileMap) => void
|
|
): void {
|
|
if (this._columnCount <= 0 || this._rowCount <= 0) {
|
|
console.error(
|
|
`Tilemap object ${this.name} is not configured properly.`
|
|
);
|
|
return;
|
|
}
|
|
|
|
this._tileMapManager.getOrLoadSimpleTileMap(
|
|
tileMapAsJsObject,
|
|
this.name,
|
|
this._tileSize,
|
|
this._columnCount,
|
|
this._rowCount,
|
|
(tileMap: TileMapHelper.EditableTileMap) => {
|
|
this._initialTilesWithHitBox.forEach((tileId) => {
|
|
const tileDefinition = tileMap.getTileDefinition(tileId);
|
|
if (!tileDefinition) {
|
|
console.warn(
|
|
`Could not set hit box for tile with id ${tileId}. Continuing.`
|
|
);
|
|
return;
|
|
}
|
|
tileDefinition.addHitBox(
|
|
this._hitBoxTag,
|
|
[
|
|
[0, 0],
|
|
[0, tileMap.getTileHeight()],
|
|
[tileMap.getTileWidth(), tileMap.getTileHeight()],
|
|
[tileMap.getTileWidth(), 0],
|
|
],
|
|
true
|
|
);
|
|
});
|
|
|
|
this._tileMapManager.getOrLoadSimpleTileMapTextureCache(
|
|
(textureName) => {
|
|
return this.getInstanceContainer()
|
|
.getGame()
|
|
.getImageManager()
|
|
.getPIXITexture(
|
|
textureName
|
|
) as unknown as PIXI.BaseTexture<PIXI.Resource>;
|
|
},
|
|
this._atlasImage,
|
|
this._tileSize,
|
|
this._columnCount,
|
|
this._rowCount,
|
|
(textureCache: TileMapHelper.TileTextureCache | null) => {
|
|
if (!textureCache) {
|
|
// getOrLoadTextureCache already log warns and errors.
|
|
return;
|
|
}
|
|
this._tileMap = tileMap;
|
|
this._renderer.refreshPixiTileMap(textureCache, true);
|
|
tileMapLoadingCallback(tileMap);
|
|
},
|
|
(error) => {
|
|
console.error(
|
|
`Could not load texture cache for atlas ${this._atlasImage} during initial loading. The tilemap might be badly configured or an issues happened with the loaded atlas image:`,
|
|
error
|
|
);
|
|
}
|
|
);
|
|
}
|
|
);
|
|
}
|
|
|
|
onDestroyed(): void {
|
|
super.onDestroyed();
|
|
this._renderer.destroy();
|
|
}
|
|
|
|
setWidth(width: float): void {
|
|
if (this.getWidth() === width) return;
|
|
|
|
this._transformationIsUpToDate = false;
|
|
this._renderer.setWidth(width);
|
|
this.invalidateHitboxes();
|
|
}
|
|
|
|
setHeight(height: float): void {
|
|
if (this.getHeight() === height) return;
|
|
|
|
this._transformationIsUpToDate = false;
|
|
this._renderer.setHeight(height);
|
|
this.invalidateHitboxes();
|
|
}
|
|
|
|
setSize(newWidth: float, newHeight: float): void {
|
|
this.setWidth(newWidth);
|
|
this.setHeight(newHeight);
|
|
this._transformationIsUpToDate = false;
|
|
}
|
|
|
|
/**
|
|
* Get the scale of the object (or the geometric mean of the X and Y scale in case they are different).
|
|
*
|
|
* @return the scale of the object (or the geometric mean of the X and Y scale in case they are different).
|
|
*/
|
|
getScale(): float {
|
|
const scaleX = this.getScaleX();
|
|
const scaleY = this.getScaleY();
|
|
return scaleX === scaleY ? scaleX : Math.sqrt(scaleX * scaleY);
|
|
}
|
|
|
|
/**
|
|
* Change the scale on X and Y axis of the object.
|
|
*
|
|
* @param scale The new scale (must be greater than 0).
|
|
*/
|
|
setScale(scale: float): void {
|
|
this.setScaleX(scale);
|
|
this.setScaleY(scale);
|
|
this._transformationIsUpToDate = false;
|
|
}
|
|
|
|
/**
|
|
* Change the scale on X axis of the object (changing its width).
|
|
*
|
|
* @param scaleX The new scale (must be greater than 0).
|
|
*/
|
|
setScaleX(scaleX: float): void {
|
|
if (scaleX < 0) {
|
|
scaleX = 0;
|
|
}
|
|
if (this.getScaleX() === scaleX) return;
|
|
|
|
this._renderer.setScaleX(scaleX);
|
|
this.invalidateHitboxes();
|
|
this._transformationIsUpToDate = false;
|
|
}
|
|
|
|
/**
|
|
* Change the scale on Y axis of the object (changing its width).
|
|
*
|
|
* @param scaleY The new scale (must be greater than 0).
|
|
*/
|
|
setScaleY(scaleY: float): void {
|
|
if (scaleY < 0) {
|
|
scaleY = 0;
|
|
}
|
|
if (this.getScaleY() === scaleY) return;
|
|
|
|
this._renderer.setScaleY(scaleY);
|
|
this.invalidateHitboxes();
|
|
this._transformationIsUpToDate = false;
|
|
}
|
|
|
|
setX(x: float): void {
|
|
super.setX(x);
|
|
this._renderer.updatePosition();
|
|
this._transformationIsUpToDate = false;
|
|
}
|
|
|
|
setY(y: float): void {
|
|
super.setY(y);
|
|
this._renderer.updatePosition();
|
|
this._transformationIsUpToDate = false;
|
|
}
|
|
|
|
setAngle(angle: float): void {
|
|
super.setAngle(angle);
|
|
this._renderer.updateAngle();
|
|
this._transformationIsUpToDate = false;
|
|
}
|
|
|
|
setOpacity(opacity: float): void {
|
|
this._opacity = opacity;
|
|
this._renderer.updateOpacity();
|
|
this._isTileMapDirty = true;
|
|
}
|
|
|
|
getOpacity(): float {
|
|
return this._opacity;
|
|
}
|
|
|
|
getWidth(): float {
|
|
return this._renderer.getWidth();
|
|
}
|
|
|
|
getHeight(): float {
|
|
return this._renderer.getHeight();
|
|
}
|
|
|
|
override getOriginalWidth(): float {
|
|
return this.getTileMapWidth();
|
|
}
|
|
|
|
override getOriginalHeight(): float {
|
|
return this.getTileMapHeight();
|
|
}
|
|
|
|
getScaleX(): float {
|
|
return this._renderer.getScaleX();
|
|
}
|
|
|
|
getScaleY(): float {
|
|
return this._renderer.getScaleY();
|
|
}
|
|
|
|
/**
|
|
* 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();
|
|
if (!this._collisionTileMap) return;
|
|
this.hitBoxes = Array.from(
|
|
this._collisionTileMap.getAllHitboxes(this._hitBoxTag)
|
|
);
|
|
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 {
|
|
if (!this._collisionTileMap) return;
|
|
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
|
|
);
|
|
}
|
|
}
|
|
|
|
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();
|
|
if (!this._collisionTileMap) return [];
|
|
return this._collisionTileMap.getHitboxesAround(
|
|
this._hitBoxTag,
|
|
left,
|
|
top,
|
|
right,
|
|
bottom
|
|
);
|
|
}
|
|
|
|
override isSpatiallyIndexed(): boolean {
|
|
return true;
|
|
}
|
|
|
|
updateTransformation() {
|
|
if (this._transformationIsUpToDate) {
|
|
return;
|
|
}
|
|
const absScaleX = Math.abs(this._renderer.getScaleX());
|
|
const absScaleY = Math.abs(this._renderer.getScaleY());
|
|
|
|
this._tileMapToSceneTransformation.setToIdentity();
|
|
|
|
// Translation
|
|
this._tileMapToSceneTransformation.translate(this.getX(), this.getY());
|
|
|
|
// Rotation
|
|
const angleInRadians = (this.getAngle() * Math.PI) / 180;
|
|
this._tileMapToSceneTransformation.rotateAround(
|
|
angleInRadians,
|
|
this.getCenterX(),
|
|
this.getCenterY()
|
|
);
|
|
|
|
// Scale
|
|
this._tileMapToSceneTransformation.scale(absScaleX, absScaleY);
|
|
if (this._collisionTileMap) {
|
|
const collisionTileMapTransformation =
|
|
this._collisionTileMap.getTransformation();
|
|
collisionTileMapTransformation.copyFrom(
|
|
this._tileMapToSceneTransformation
|
|
);
|
|
this._collisionTileMap.setTransformation(
|
|
collisionTileMapTransformation
|
|
);
|
|
}
|
|
this._sceneToTileMapTransformation.copyFrom(
|
|
this._tileMapToSceneTransformation
|
|
);
|
|
this._sceneToTileMapTransformation.invert();
|
|
this._transformationIsUpToDate = true;
|
|
}
|
|
|
|
getSceneXCoordinateOfTileCenter(
|
|
columnIndex: integer,
|
|
rowIndex: integer
|
|
): float {
|
|
const sceneCoordinates: FloatPoint =
|
|
SimpleTileMapRuntimeObject.workingPoint;
|
|
this._tileMapToSceneTransformation.transform(
|
|
[
|
|
(columnIndex + 0.5) * this._tileSize,
|
|
(rowIndex + 0.5) * this._tileSize,
|
|
],
|
|
sceneCoordinates
|
|
);
|
|
return sceneCoordinates[0];
|
|
}
|
|
|
|
getSceneYCoordinateOfTileCenter(
|
|
columnIndex: integer,
|
|
rowIndex: integer
|
|
): float {
|
|
const sceneCoordinates: FloatPoint =
|
|
SimpleTileMapRuntimeObject.workingPoint;
|
|
this._tileMapToSceneTransformation.transform(
|
|
[
|
|
(columnIndex + 0.5) * this._tileSize,
|
|
(rowIndex + 0.5) * this._tileSize,
|
|
],
|
|
sceneCoordinates
|
|
);
|
|
return sceneCoordinates[1];
|
|
}
|
|
|
|
getGridCoordinatesFromSceneCoordinates(
|
|
x: float,
|
|
y: float
|
|
): [integer, integer] {
|
|
this.updateTransformation();
|
|
|
|
const gridCoordinates: FloatPoint =
|
|
SimpleTileMapRuntimeObject.workingPoint;
|
|
this._sceneToTileMapTransformation.transform([x, y], gridCoordinates);
|
|
|
|
const columnIndex = Math.floor(gridCoordinates[0] / this._tileSize);
|
|
const rowIndex = Math.floor(gridCoordinates[1] / this._tileSize);
|
|
|
|
return [columnIndex, rowIndex];
|
|
}
|
|
|
|
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, 0);
|
|
}
|
|
|
|
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
|
|
) {
|
|
if (!this._tileMap) {
|
|
return;
|
|
}
|
|
const layer = this._tileMap.getTileLayer(this._layerIndex);
|
|
if (!layer) {
|
|
return;
|
|
}
|
|
const oldTileId = layer.getTileId(columnIndex, rowIndex);
|
|
if (tileId === oldTileId) {
|
|
return;
|
|
}
|
|
layer.setTile(columnIndex, rowIndex, tileId);
|
|
|
|
if (this._collisionTileMap) {
|
|
const oldTileDefinition =
|
|
oldTileId !== undefined && this._tileMap.getTileDefinition(oldTileId);
|
|
const newTileDefinition = this._tileMap.getTileDefinition(tileId);
|
|
const hadFullHitBox =
|
|
!!oldTileDefinition &&
|
|
oldTileDefinition.hasFullHitBox(this._hitBoxTag);
|
|
const haveFullHitBox =
|
|
!!newTileDefinition &&
|
|
newTileDefinition.hasFullHitBox(this._hitBoxTag);
|
|
if (hadFullHitBox !== haveFullHitBox) {
|
|
this._collisionTileMap.invalidateTile(
|
|
this._layerIndex,
|
|
columnIndex,
|
|
rowIndex
|
|
);
|
|
}
|
|
}
|
|
this._isTileMapDirty = true;
|
|
}
|
|
|
|
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, 0, flip);
|
|
this._isTileMapDirty = true;
|
|
// 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, 0, flip);
|
|
this._isTileMapDirty = true;
|
|
// No need to invalidate hit boxes since at the moment, collision mask
|
|
// cannot be configured on each tile.
|
|
}
|
|
|
|
isTileFlippedOnXAtPosition(x: float, y: float) {
|
|
const [columnIndex, rowIndex] =
|
|
this.getGridCoordinatesFromSceneCoordinates(x, y);
|
|
|
|
return this.isTileFlippedOnX(columnIndex, rowIndex, 0);
|
|
}
|
|
|
|
isTileFlippedOnXAtGridCoordinates(columnIndex: integer, rowIndex: integer) {
|
|
return this.isTileFlippedOnX(columnIndex, rowIndex, 0);
|
|
}
|
|
|
|
isTileFlippedOnYAtPosition(x: float, y: float) {
|
|
const [columnIndex, rowIndex] =
|
|
this.getGridCoordinatesFromSceneCoordinates(x, y);
|
|
|
|
return this.isTileFlippedOnY(columnIndex, rowIndex, 0);
|
|
}
|
|
|
|
isTileFlippedOnYAtGridCoordinates(columnIndex: integer, rowIndex: integer) {
|
|
return this.isTileFlippedOnY(columnIndex, rowIndex, 0);
|
|
}
|
|
|
|
removeTileAtPosition(x: float, y: float) {
|
|
const [columnIndex, rowIndex] =
|
|
this.getGridCoordinatesFromSceneCoordinates(x, y);
|
|
this.removeTileAtGridCoordinates(columnIndex, rowIndex);
|
|
}
|
|
|
|
removeTileAtGridCoordinates(columnIndex: integer, rowIndex: integer) {
|
|
if (!this._tileMap) {
|
|
return;
|
|
}
|
|
const layer = this._tileMap.getTileLayer(this._layerIndex);
|
|
if (!layer) {
|
|
return;
|
|
}
|
|
const oldTileId = layer.getTileId(columnIndex, rowIndex);
|
|
if (oldTileId === undefined) {
|
|
return;
|
|
}
|
|
layer.removeTile(columnIndex, rowIndex);
|
|
if (this._collisionTileMap) {
|
|
const oldTileDefinition =
|
|
oldTileId !== undefined && this._tileMap.getTileDefinition(oldTileId);
|
|
const hadFullHitBox =
|
|
!!oldTileDefinition &&
|
|
oldTileDefinition.hasFullHitBox(this._hitBoxTag);
|
|
if (hadFullHitBox) {
|
|
this._collisionTileMap.invalidateTile(
|
|
this._layerIndex,
|
|
columnIndex,
|
|
rowIndex
|
|
);
|
|
}
|
|
}
|
|
this._isTileMapDirty = true;
|
|
}
|
|
|
|
setGridRowCount(targetRowCount: integer) {
|
|
if (targetRowCount <= 0) return;
|
|
if (!this._tileMap) return;
|
|
this._tileMap.setDimensionY(targetRowCount);
|
|
this._isTileMapDirty = true;
|
|
this.invalidateHitboxes();
|
|
}
|
|
|
|
setGridColumnCount(targetColumnCount: integer) {
|
|
if (targetColumnCount <= 0) return;
|
|
if (!this._tileMap) return;
|
|
this._tileMap.setDimensionX(targetColumnCount);
|
|
this._isTileMapDirty = true;
|
|
this.invalidateHitboxes();
|
|
}
|
|
|
|
getGridRowCount(): integer {
|
|
if (!this._tileMap) return 0;
|
|
return this._tileMap.getDimensionY();
|
|
}
|
|
|
|
getGridColumnCount(): integer {
|
|
if (!this._tileMap) return 0;
|
|
return this._tileMap.getDimensionX();
|
|
}
|
|
|
|
getTilesetColumnCount(): integer {
|
|
return this._columnCount;
|
|
}
|
|
|
|
getTilesetRowCount(): integer {
|
|
return this._rowCount;
|
|
}
|
|
|
|
getTileMap(): TileMapHelper.EditableTileMap | null {
|
|
return this._tileMap;
|
|
}
|
|
|
|
getTileMapWidth() {
|
|
const tileMap = this._tileMap;
|
|
return tileMap ? tileMap.getWidth() : 20;
|
|
}
|
|
|
|
getTileMapHeight() {
|
|
const tileMap = this._tileMap;
|
|
return tileMap ? tileMap.getHeight() : 20;
|
|
}
|
|
|
|
/**
|
|
* @param x The layer column.
|
|
* @param y The layer row.
|
|
* @param layerIndex The layer index.
|
|
* @returns The tile's id.
|
|
*/
|
|
getTileId(x: integer, y: integer, layerIndex: integer): integer {
|
|
if (!this._tileMap) return -1;
|
|
return this._tileMap.getTileId(x, y, layerIndex);
|
|
}
|
|
|
|
/**
|
|
* @param x The layer column.
|
|
* @param y The layer row.
|
|
* @param layerIndex The layer index.
|
|
* @param flip true if the tile should be flipped.
|
|
*/
|
|
flipTileOnY(x: integer, y: integer, layerIndex: integer, flip: boolean) {
|
|
if (!this._tileMap) return;
|
|
this._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.
|
|
*/
|
|
flipTileOnX(x: integer, y: integer, layerIndex: integer, flip: boolean) {
|
|
if (!this._tileMap) return;
|
|
this._tileMap.flipTileOnX(x, y, layerIndex, flip);
|
|
}
|
|
|
|
/**
|
|
* @param x The layer column.
|
|
* @param y The layer row.
|
|
* @param layerIndex The layer index.
|
|
*/
|
|
isTileFlippedOnX(x: integer, y: integer, layerIndex: integer): boolean {
|
|
if (!this._tileMap) return false;
|
|
return this._tileMap.isTileFlippedOnX(x, y, layerIndex);
|
|
}
|
|
|
|
/**
|
|
* @param x The layer column.
|
|
* @param y The layer row.
|
|
* @param layerIndex The layer index.
|
|
*/
|
|
isTileFlippedOnY(x: integer, y: integer, layerIndex: integer): boolean {
|
|
if (!this._tileMap) return false;
|
|
return this._tileMap.isTileFlippedOnY(x, y, layerIndex);
|
|
}
|
|
}
|
|
gdjs.registerObject(
|
|
'TileMap::SimpleTileMap',
|
|
gdjs.SimpleTileMapRuntimeObject
|
|
);
|
|
}
|