mirror of
https://github.com/Heretek-AI/GDevelop.git
synced 2026-08-25 12:23:58 -04:00
514 lines
15 KiB
TypeScript
514 lines
15 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;
|
|
|
|
const hitBoxTag: string = 'collision';
|
|
|
|
/**
|
|
* Displays a SimpleTileMap object.
|
|
* @category Objects > Tile Map
|
|
*/
|
|
export class SimpleTileMapRuntimeObject
|
|
extends gdjs.AbstractTileMapRuntimeObject
|
|
implements gdjs.Resizable, gdjs.Scalable, gdjs.OpacityHandler
|
|
{
|
|
_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;
|
|
_initialTileMapAsJsObject: TileMapHelper.EditableTileMapAsJsObject;
|
|
readonly _initialTilesWithHitBox: number[];
|
|
_isTileMapDirty: boolean = false;
|
|
_collisionTileMap: gdjs.TileMap.TransformedCollisionTileMap | null = null;
|
|
|
|
// 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,
|
|
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,
|
|
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, hitBoxTag);
|
|
}
|
|
|
|
this.invalidateTransformation();
|
|
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(
|
|
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.invalidateTransformation();
|
|
this._renderer.setWidth(width);
|
|
this.invalidateHitboxes();
|
|
}
|
|
|
|
setHeight(height: float): void {
|
|
if (this.getHeight() === height) return;
|
|
|
|
this.invalidateTransformation();
|
|
this._renderer.setHeight(height);
|
|
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.getScaleX() === scaleX) return;
|
|
|
|
this._renderer.setScaleX(scaleX);
|
|
this.invalidateHitboxes();
|
|
this.invalidateTransformation();
|
|
}
|
|
|
|
/**
|
|
* 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.invalidateTransformation();
|
|
}
|
|
|
|
setX(x: float): void {
|
|
super.setX(x);
|
|
this._renderer.updatePosition();
|
|
this.invalidateTransformation();
|
|
}
|
|
|
|
setY(y: float): void {
|
|
super.setY(y);
|
|
this._renderer.updatePosition();
|
|
this.invalidateTransformation();
|
|
}
|
|
|
|
setAngle(angle: float): void {
|
|
super.setAngle(angle);
|
|
this._renderer.updateAngle();
|
|
this.invalidateTransformation();
|
|
}
|
|
|
|
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 {
|
|
const tileMap = this._tileMap;
|
|
return tileMap ? tileMap.getWidth() : 20;
|
|
}
|
|
|
|
override getOriginalHeight(): float {
|
|
const tileMap = this._tileMap;
|
|
return tileMap ? tileMap.getHeight() : 20;
|
|
}
|
|
|
|
getScaleX(): float {
|
|
return this._renderer.getScaleX();
|
|
}
|
|
|
|
getScaleY(): float {
|
|
return this._renderer.getScaleY();
|
|
}
|
|
|
|
getTileSetColumnCount(): integer {
|
|
return this._columnCount;
|
|
}
|
|
|
|
getTileSetRowCount(): integer {
|
|
return this._rowCount;
|
|
}
|
|
|
|
getTileMap(): TileMapHelper.EditableTileMap | null {
|
|
return this._tileMap;
|
|
}
|
|
|
|
getTileMapForEdition(): TileMapHelper.EditableTileMap | null {
|
|
return this._tileMap;
|
|
}
|
|
|
|
getCollisionTileMap(): gdjs.TileMap.TransformedCollisionTileMap | null {
|
|
return this._collisionTileMap;
|
|
}
|
|
|
|
getDisplayMode(): string {
|
|
return 'all';
|
|
}
|
|
|
|
getDisplayedLayerIndex(): integer {
|
|
return 0;
|
|
}
|
|
|
|
getEditedLayerIndex(): integer {
|
|
return 0;
|
|
}
|
|
|
|
getCollisionMaskTag(): string {
|
|
return hitBoxTag;
|
|
}
|
|
|
|
invalidateTileMap(): void {
|
|
this._isTileMapDirty = true;
|
|
}
|
|
}
|
|
gdjs.registerObject(
|
|
'TileMap::SimpleTileMap',
|
|
gdjs.SimpleTileMapRuntimeObject
|
|
);
|
|
}
|