mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00

--------- Co-authored-by: Clément Pasteau <4895034+ClementPasteau@users.noreply.github.com>
659 lines
18 KiB
TypeScript
659 lines
18 KiB
TypeScript
/// <reference path="helper/TileMapHelper.d.ts" />
|
|
namespace gdjs {
|
|
export type TilemapCollisionMaskObjectDataType = {
|
|
content: {
|
|
tilemapJsonFile: string;
|
|
tilesetJsonFile: string;
|
|
collisionMaskTag: string;
|
|
debugMode: boolean;
|
|
fillColor: string;
|
|
outlineColor: string;
|
|
};
|
|
fillOpacity: float;
|
|
outlineOpacity: float;
|
|
outlineSize: float;
|
|
};
|
|
|
|
export type TilemapCollisionMaskObjectData = ObjectData &
|
|
TilemapCollisionMaskObjectDataType;
|
|
|
|
export type TilemapCollisionMaskNetworkSyncDataType = {
|
|
tmjf: string;
|
|
tsjf: string;
|
|
dm: boolean;
|
|
oc: integer;
|
|
fc: integer;
|
|
os: float;
|
|
fo: float;
|
|
oo: float;
|
|
};
|
|
|
|
export type TilemapCollisionMaskNetworkSyncData = ObjectNetworkSyncData &
|
|
TilemapCollisionMaskNetworkSyncDataType;
|
|
|
|
/**
|
|
* An object that handle hitboxes for a tile map.
|
|
* @extends gdjs.RuntimeObject
|
|
*/
|
|
export class TileMapCollisionMaskRuntimeObject
|
|
extends gdjs.RuntimeObject
|
|
implements gdjs.Resizable, gdjs.Scalable
|
|
{
|
|
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 _layerIndex: integer | null;
|
|
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;
|
|
|
|
_width: float;
|
|
_height: float;
|
|
_scaleX: float;
|
|
_scaleY: float;
|
|
|
|
/**
|
|
* If the owner moves, the hitboxes vertices
|
|
* will have to be transformed again.
|
|
*/
|
|
private _transformationIsUpToDate: boolean = false;
|
|
|
|
constructor(instanceContainer: gdjs.RuntimeInstanceContainer, objectData) {
|
|
super(instanceContainer, objectData);
|
|
this._tilemapJsonFile = objectData.content.tilemapJsonFile;
|
|
this._tilesetJsonFile = objectData.content.tilesetJsonFile;
|
|
this._collisionMaskTag = objectData.content.collisionMaskTag;
|
|
this._layerIndex =
|
|
objectData.content.useAllLayers ||
|
|
objectData.content.useAllLayers === undefined
|
|
? null
|
|
: Number.isFinite(objectData.content.layerIndex)
|
|
? objectData.content.layerIndex
|
|
: 1;
|
|
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(instanceContainer);
|
|
|
|
// The actual size is set when the tile map file is loaded.
|
|
this._width = 0;
|
|
this._height = 0;
|
|
this._scaleX = 1;
|
|
this._scaleY = 1;
|
|
const editableTileMap = new TileMapHelper.EditableTileMap(
|
|
1,
|
|
1,
|
|
0,
|
|
0,
|
|
new Map()
|
|
);
|
|
this._collisionTileMap = new gdjs.TileMap.TransformedCollisionTileMap(
|
|
editableTileMap,
|
|
this._collisionMaskTag,
|
|
this._layerIndex
|
|
);
|
|
|
|
this._renderer = new gdjs.TileMap.TileMapCollisionMaskRenderer(
|
|
this,
|
|
instanceContainer
|
|
);
|
|
this._updateTileMap();
|
|
|
|
// *ALWAYS* call `this.onCreated()` at the very end of your object constructor.
|
|
this.onCreated();
|
|
}
|
|
|
|
updatePreRender(instanceContainer: gdjs.RuntimeInstanceContainer) {
|
|
super.updatePreRender(instanceContainer);
|
|
|
|
if (this._debugMode && this.hitBoxesDirty) {
|
|
this.updateHitBoxes();
|
|
}
|
|
}
|
|
|
|
getRendererObject() {
|
|
return this._renderer.getRendererObject();
|
|
}
|
|
|
|
getVisibilityAABB() {
|
|
return null;
|
|
}
|
|
|
|
updateFromObjectData(
|
|
oldObjectData: TilemapCollisionMaskObjectData,
|
|
newObjectData: TilemapCollisionMaskObjectData
|
|
): 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);
|
|
}
|
|
// TODO Handle changes to collisionMaskTag, useAllLayers and layerIndex.
|
|
return true;
|
|
}
|
|
|
|
getNetworkSyncData(): TilemapCollisionMaskNetworkSyncData {
|
|
return {
|
|
...super.getNetworkSyncData(),
|
|
tmjf: this.getTilemapJsonFile(),
|
|
tsjf: this.getTilesetJsonFile(),
|
|
dm: this.getDebugMode(),
|
|
oc: this.getOutlineColor(),
|
|
fc: this.getFillColor(),
|
|
os: this.getOutlineSize(),
|
|
fo: this.getFillOpacity(),
|
|
oo: this.getOutlineOpacity(),
|
|
};
|
|
}
|
|
|
|
updateFromNetworkSyncData(
|
|
networkSyncData: TilemapCollisionMaskNetworkSyncData
|
|
): void {
|
|
super.updateFromNetworkSyncData(networkSyncData);
|
|
|
|
if (networkSyncData.tmjf !== undefined) {
|
|
this.setTilemapJsonFile(networkSyncData.tmjf);
|
|
}
|
|
if (networkSyncData.tsjf !== undefined) {
|
|
this.setTilesetJsonFile(networkSyncData.tsjf);
|
|
}
|
|
if (networkSyncData.dm !== undefined) {
|
|
this.setDebugMode(networkSyncData.dm);
|
|
}
|
|
if (networkSyncData.oc !== undefined) {
|
|
this.setOutlineColor(networkSyncData.oc);
|
|
}
|
|
if (networkSyncData.fc !== undefined) {
|
|
this.setFillColor(networkSyncData.fc);
|
|
}
|
|
if (networkSyncData.os !== undefined) {
|
|
this.setOutlineSize(networkSyncData.os);
|
|
}
|
|
if (networkSyncData.fo !== undefined) {
|
|
this.setFillOpacity(networkSyncData.fo);
|
|
}
|
|
if (networkSyncData.oo !== undefined) {
|
|
this.setOutlineOpacity(networkSyncData.oo);
|
|
}
|
|
}
|
|
|
|
extraInitializationFromInitialInstance(initialInstanceData): void {
|
|
if (initialInstanceData.customSize) {
|
|
this.setWidth(initialInstanceData.width);
|
|
this.setHeight(initialInstanceData.height);
|
|
}
|
|
}
|
|
|
|
private _updateTileMap(): void {
|
|
this._tileMapManager.getOrLoadTileMap(
|
|
this._tilemapJsonFile,
|
|
this._tilesetJsonFile,
|
|
0, // levelIndex
|
|
(tileMap: TileMapHelper.EditableTileMap | null) => {
|
|
if (!tileMap) {
|
|
// getOrLoadTileMap already log errors.
|
|
return;
|
|
}
|
|
|
|
this._collisionTileMap = new gdjs.TileMap.TransformedCollisionTileMap(
|
|
tileMap,
|
|
this._collisionMaskTag,
|
|
this._layerIndex
|
|
);
|
|
// 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();
|
|
|
|
this._width = this._collisionTileMap.getWidth() * this._scaleX;
|
|
this._height = this._collisionTileMap.getHeight() * this._scaleY;
|
|
}
|
|
);
|
|
}
|
|
|
|
updateHitBoxes(): void {
|
|
this.updateTransformation();
|
|
// Update the RuntimeObject hitboxes attribute.
|
|
for (const _ 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._scaleX);
|
|
const absScaleY = Math.abs(this._scaleY);
|
|
|
|
transformation.setToIdentity();
|
|
|
|
// Translation
|
|
transformation.translate(this.x, this.y);
|
|
|
|
// Rotation
|
|
const angleInRadians = (this.angle * Math.PI) / 180;
|
|
transformation.rotateAround(
|
|
angleInRadians,
|
|
this.getCenterX(),
|
|
this.getCenterY()
|
|
);
|
|
|
|
// 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;
|
|
}
|
|
|
|
setWidth(width: float): void {
|
|
if (this._width === width) {
|
|
return;
|
|
}
|
|
this._scaleX = width / this._collisionTileMap.getWidth();
|
|
this._width = width;
|
|
this._transformationIsUpToDate = false;
|
|
this.invalidateHitboxes();
|
|
}
|
|
|
|
setHeight(height: float): void {
|
|
if (this._height === height) {
|
|
return;
|
|
}
|
|
this._scaleY = height / this._collisionTileMap.getHeight();
|
|
this._height = height;
|
|
this._transformationIsUpToDate = false;
|
|
this.invalidateHitboxes();
|
|
}
|
|
|
|
setSize(newWidth: float, newHeight: float): void {
|
|
this.setWidth(newWidth);
|
|
this.setHeight(newHeight);
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
|
|
/**
|
|
* 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._scaleX === scaleX) {
|
|
return;
|
|
}
|
|
this._scaleX = scaleX;
|
|
this._width = scaleX * this._collisionTileMap.getWidth();
|
|
this._transformationIsUpToDate = false;
|
|
this.invalidateHitboxes();
|
|
}
|
|
|
|
/**
|
|
* 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._scaleY === scaleY) {
|
|
return;
|
|
}
|
|
this._scaleY = scaleY;
|
|
this._height = scaleY * this._collisionTileMap.getHeight();
|
|
this._transformationIsUpToDate = false;
|
|
this.invalidateHitboxes();
|
|
}
|
|
|
|
getWidth(): float {
|
|
return this._width;
|
|
}
|
|
|
|
getHeight(): float {
|
|
return this._height;
|
|
}
|
|
|
|
getScaleX(): float {
|
|
return this._scaleX;
|
|
}
|
|
|
|
getScaleY(): float {
|
|
return this._scaleY;
|
|
}
|
|
}
|
|
gdjs.registerObject(
|
|
'TileMap::CollisionMask',
|
|
gdjs.TileMapCollisionMaskRuntimeObject
|
|
);
|
|
TileMapCollisionMaskRuntimeObject.supportsReinitialization = false;
|
|
}
|