Files
GDevelop/Extensions/TileMap/JsExtension.js
2024-01-16 21:50:15 +01:00

1672 lines
50 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//@ts-check
/// <reference path="../JsExtensionTypes.d.ts" />
/// <reference path="helper/TileMapHelper.d.ts" />
/**
* This is a declaration of an extension for GDevelop 5.
*
* Run `node import-GDJS-Runtime.js` (in newIDE/app/scripts) if you make any change
* to this extension file or to any other *.js file that you reference inside.
*
* The file must be named "JsExtension.js", otherwise GDevelop won't load it.
* ⚠️ If you make a change and the extension is not loaded, open the developer console
* and search for any errors.
*
* More information on https://github.com/4ian/GDevelop/blob/master/newIDE/README-extensions.md
*/
/** @type {ExtensionModule} */
const defineTileMap = function (extension, _, gd) {
var objectTileMap = new gd.ObjectJsImplementation();
// $FlowExpectedError - ignore Flow warning as we're creating an object
objectTileMap.updateProperty = function (
objectContent,
propertyName,
newValue
) {
if (propertyName === 'tilemapJsonFile') {
objectContent.tilemapJsonFile = newValue;
return true;
}
if (propertyName === 'tilesetJsonFile') {
objectContent.tilesetJsonFile = newValue;
return true;
}
if (propertyName === 'tilemapAtlasImage') {
objectContent.tilemapAtlasImage = newValue;
return true;
}
if (propertyName === 'displayMode') {
objectContent.displayMode = newValue;
return true;
}
if (propertyName === 'layerIndex') {
objectContent.layerIndex = parseFloat(newValue);
return true;
}
if (propertyName === 'levelIndex') {
objectContent.levelIndex = parseFloat(newValue);
return true;
}
if (propertyName === 'animationSpeedScale') {
objectContent.animationSpeedScale = parseFloat(newValue);
return true;
}
if (propertyName === 'animationFps') {
objectContent.animationFps = parseFloat(newValue);
return true;
}
return false;
};
// $FlowExpectedError - ignore Flow warning as we're creating an object
objectTileMap.getProperties = function (objectContent) {
var objectProperties = new gd.MapStringPropertyDescriptor();
objectProperties.set(
'tilemapJsonFile',
new gd.PropertyDescriptor(objectContent.tilemapJsonFile)
.setType('resource')
.addExtraInfo('tilemap')
.addExtraInfo('json')
.setLabel(_('Tilemap file (Tiled or LDtk)'))
.setDescription(
_('This is the file that was saved or exported from Tiled or LDtk.')
)
.setGroup(_('LDtk or Tiled'))
);
objectProperties.set(
'tilesetJsonFile',
new gd.PropertyDescriptor(objectContent.tilesetJsonFile || '')
.setType('resource')
.addExtraInfo('tileset')
.addExtraInfo('json')
.setLabel(_('Tileset JSON file (optional)'))
.setDescription(
_(
"Optional: specify this if you've saved the tileset in a different file as the Tiled tilemap."
)
)
.setGroup(_('Tiled only'))
);
objectProperties.set(
'tilemapAtlasImage',
new gd.PropertyDescriptor(objectContent.tilemapAtlasImage)
.setType('resource')
.addExtraInfo('image')
.setLabel(_('Atlas image'))
.setDescription(_('The Atlas image containing the tileset.'))
.setGroup(_('Tiled only'))
);
objectProperties.set(
'displayMode',
new gd.PropertyDescriptor(objectContent.displayMode)
.setType('choice')
.addExtraInfo('visible')
.addExtraInfo('all')
.addExtraInfo('index')
.setLabel(_('Display mode'))
.setGroup(_('Appearance'))
);
objectProperties.set(
'layerIndex',
new gd.PropertyDescriptor(objectContent.layerIndex.toString())
.setType('number')
.setLabel(_('Layer index to display'))
.setDescription(
_(
'If "index" is selected as the display mode, this is the index of the layer to display.'
)
)
.setGroup(_('Appearance'))
);
objectProperties.set(
'levelIndex',
new gd.PropertyDescriptor((objectContent.levelIndex || 0).toString())
.setType('number')
.setLabel(_('Level index to display'))
.setDescription(_('Select which level to render via its index (LDtk)'))
.setGroup(_('Appearance'))
);
objectProperties.set(
'animationSpeedScale',
new gd.PropertyDescriptor(objectContent.animationSpeedScale.toString())
.setType('number')
.setLabel(_('Animation speed scale'))
.setGroup(_('Animation'))
);
objectProperties.set(
'animationFps',
new gd.PropertyDescriptor(objectContent.animationFps.toString())
.setType('number')
.setLabel(_('Animation FPS'))
.setGroup(_('Animation'))
);
return objectProperties;
};
objectTileMap.setRawJSONContent(
JSON.stringify({
tilemapJsonFile: '',
tilesetJsonFile: '',
tilemapAtlasImage: '',
displayMode: 'visible',
layerIndex: 0,
levelIndex: 0,
animationSpeedScale: 1,
animationFps: 4,
})
);
// $FlowExpectedError - ignore Flow warning as we're creating an object
objectTileMap.updateInitialInstanceProperty = function (
objectContent,
instance,
propertyName,
newValue,
project,
layout
) {
return false;
};
// $FlowExpectedError - ignore Flow warning as we're creating an object
objectTileMap.getInitialInstanceProperties = function (
content,
instance,
project,
layout
) {
var instanceProperties = new gd.MapStringPropertyDescriptor();
return instanceProperties;
};
const object = extension
.addObject(
'TileMap',
_('Tilemap'),
_(
'Displays a tiled-based map, made with the Tiled editor (https://www.mapeditor.org/) or the LDtk editor (https://ldtk.io/).'
),
'JsPlatform/Extensions/tile_map.svg',
objectTileMap
)
.setCategoryFullName(_('Advanced'))
.addDefaultBehavior('EffectCapability::EffectBehavior')
.addDefaultBehavior('ResizableCapability::ResizableBehavior')
.addDefaultBehavior('ScalableCapability::ScalableBehavior')
.addDefaultBehavior('OpacityCapability::OpacityBehavior')
.setIncludeFile('Extensions/TileMap/tilemapruntimeobject.js')
.addIncludeFile('Extensions/TileMap/TileMapRuntimeManager.js')
.addIncludeFile('Extensions/TileMap/tilemapruntimeobject-pixi-renderer.js')
.addIncludeFile('Extensions/TileMap/pixi-tilemap/dist/pixi-tilemap.umd.js')
.addIncludeFile('Extensions/TileMap/pako/dist/pako.min.js')
.addIncludeFile('Extensions/TileMap/helper/TileMapHelper.js');
object
.addCondition(
'TilemapJsonFile',
_('Tilemap file (Tiled or LDtk)'),
_('Check the tilemap file (Tiled or LDtk) being used.'),
_('The tilemap file of _PARAM0_ is _PARAM1_'),
'',
'JsPlatform/Extensions/tile_map.svg',
'JsPlatform/Extensions/tile_map.svg'
)
.addParameter('object', _('Tile map'), 'TileMap', false)
.addParameter(
'tilemapResource',
_('Tilemap file (Tiled or LDtk)'),
'',
false
)
.getCodeExtraInformation()
.setFunctionName('isTilemapJsonFile');
object
.addAction(
'SetTilemapJsonFile',
_('Tilemap file (Tiled or LDtk)'),
_(
'Set the Tiled or LDtk file containing the Tilemap data to display. This is usually the main file exported from Tiled/LDtk.'
),
_('Set the tilemap file of _PARAM0_ to _PARAM1_'),
'',
'JsPlatform/Extensions/tile_map.svg',
'JsPlatform/Extensions/tile_map.svg'
)
.addParameter('object', _('Tile map'), 'TileMap', false)
.addParameter(
'tilemapResource',
_('Tilemap file (Tiled or LDtk)'),
'',
false
)
.getCodeExtraInformation()
.setFunctionName('setTilemapJsonFile');
object
.addCondition(
'TilesetJsonFile',
_('Tileset JSON file'),
_('Check the tileset JSON file being used.'),
_('The tileset JSON file of _PARAM0_ is _PARAM1_'),
'',
'JsPlatform/Extensions/tile_map.svg',
'JsPlatform/Extensions/tile_map.svg'
)
.addParameter('object', _('Tile map'), 'TileMap', false)
.addParameter('tilesetResource', _('Tileset JSON file'), '', false)
.getCodeExtraInformation()
.setFunctionName('isTilesetJsonFile');
object
.addAction(
'SetTilesetJsonFile',
_('Tileset JSON file'),
_(
'Set the JSON file with the tileset data (sometimes that is embedded in the Tilemap, so not needed)'
),
_('Set the tileset JSON file of _PARAM0_ to _PARAM1_'),
'',
'JsPlatform/Extensions/tile_map.svg',
'JsPlatform/Extensions/tile_map.svg'
)
.addParameter('object', _('Tile map'), 'TileMap', false)
.addParameter('tilesetResource', _('Tileset JSON file'), '', false)
.getCodeExtraInformation()
.setFunctionName('setTilesetJsonFile');
object
.addCondition(
'DisplayMode',
_('Display mode'),
_('Compare the value of the display mode.'),
_('The display mode of _PARAM0_ is _PARAM1_'),
'',
'JsPlatform/Extensions/tile_map.svg',
'JsPlatform/Extensions/tile_map.svg'
)
.addParameter('object', _('Tile map'), 'TileMap', false)
.addParameter(
'stringWithSelector',
_('Display mode'),
'["visible", "all", "index"]',
false
)
.getCodeExtraInformation()
.setFunctionName('isDisplayMode');
object
.addAction(
'SetDisplayMode',
_('Display mode'),
_('Set the display mode'),
_('Set the display mode of _PARAM0_ to _PARAM1_'),
'',
'JsPlatform/Extensions/tile_map.svg',
'JsPlatform/Extensions/tile_map.svg'
)
.addParameter('object', _('Tile map'), 'TileMap', false)
.addParameter(
'stringWithSelector',
_('Display mode'),
'["visible", "all", "index"]',
false
)
.getCodeExtraInformation()
.setFunctionName('setDisplayMode');
object
.addCondition(
'LayerIndex',
_('Layer index'),
_('Compare the value of the layer index.'),
_('the layer index'),
'',
'JsPlatform/Extensions/tile_map.svg',
'JsPlatform/Extensions/tile_map.svg'
)
.addParameter('object', _('Tile map'), 'TileMap', false)
.useStandardRelationalOperatorParameters(
'number',
gd.ParameterOptions.makeNewOptions()
)
.getCodeExtraInformation()
.setFunctionName('getLayerIndex');
object
.addAction(
'SetLayerIndex',
_('Layer index'),
_('Set the layer index of the Tilemap.'),
_('the layer index'),
'',
'JsPlatform/Extensions/tile_map.svg',
'JsPlatform/Extensions/tile_map.svg'
)
.addParameter('object', _('Tile map'), 'TileMap', false)
.useStandardOperatorParameters(
'number',
gd.ParameterOptions.makeNewOptions()
)
.getCodeExtraInformation()
.setFunctionName('setLayerIndex')
.setGetter('getLayerIndex');
object
.addExpression(
'LayerIndex',
_('Layer index'),
_('Get the layer index being displayed'),
'',
'JsPlatform/Extensions/tile_map.svg'
)
.addParameter('object', _('Tile map'), 'TileMap', false)
.getCodeExtraInformation()
.setFunctionName('getLayerIndex');
object
.addExpressionAndCondition(
'number',
'LevelIndex',
_('Level index'),
_('the level index being displayed.'),
_('the level index'),
'',
'JsPlatform/Extensions/tile_map.svg'
)
.addParameter('object', _('Tile map'), 'TileMap', false)
.useStandardParameters('number', gd.ParameterOptions.makeNewOptions())
.setFunctionName('getLevelIndex');
object
.addCondition(
'AnimationSpeedScale',
_('Animation speed scale'),
_('Compare the animation speed scale.'),
_('the animation speed scale'),
'',
'JsPlatform/Extensions/tile_map.svg',
'JsPlatform/Extensions/tile_map.svg'
)
.addParameter('object', _('Tile map'), 'TileMap', false)
.useStandardRelationalOperatorParameters(
'number',
gd.ParameterOptions.makeNewOptions().setDescription(
_('Speed scale to compare to (1 by default)')
)
)
.getCodeExtraInformation()
.setFunctionName('getAnimationSpeedScale');
object
.addAction(
'SetAnimationSpeedScale',
_('Animation speed scale'),
_('Set the animation speed scale of the Tilemap.'),
_('the animation speed scale'),
'',
'JsPlatform/Extensions/tile_map.svg',
'JsPlatform/Extensions/tile_map.svg'
)
.addParameter('object', _('Tile map'), 'TileMap', false)
.useStandardOperatorParameters(
'number',
gd.ParameterOptions.makeNewOptions().setDescription(
_('Speed scale (1 by default)')
)
)
.getCodeExtraInformation()
.setFunctionName('setAnimationSpeedScale')
.setGetter('getAnimationSpeedScale');
object
.addExpression(
'AnimationSpeedScale',
_('Animation speed scale'),
_('Get the Animation speed scale'),
'',
'JsPlatform/Extensions/tile_map.svg'
)
.addParameter('object', _('Tile map'), 'TileMap', false)
.getCodeExtraInformation()
.setFunctionName('getAnimationSpeedScale');
object
.addCondition(
'AnimationFps',
_('Animation speed (FPS)'),
_('Compare the animation speed.'),
_('the animation speed (FPS)'),
'',
'JsPlatform/Extensions/tile_map.svg',
'JsPlatform/Extensions/tile_map.svg'
)
.addParameter('object', _('Tile map'), 'TileMap', false)
.useStandardRelationalOperatorParameters(
'number',
gd.ParameterOptions.makeNewOptions().setDescription(
_('Animation speed to compare to (in frames per second)')
)
)
.getCodeExtraInformation()
.setFunctionName('getAnimationFps');
object
.addAction(
'SetAnimationFps',
_('Animation speed (FPS)'),
_('Set the animation speed of the Tilemap.'),
_('the animation speed (FPS)'),
'',
'JsPlatform/Extensions/tile_map.svg',
'JsPlatform/Extensions/tile_map.svg'
)
.addParameter('object', _('Tile map'), 'TileMap', false)
.useStandardOperatorParameters(
'number',
gd.ParameterOptions.makeNewOptions().setDescription(
_('Animation speed (in frames per second)')
)
)
.getCodeExtraInformation()
.setFunctionName('setAnimationFps')
.setGetter('getAnimationFps');
object
.addExpression(
'AnimationFps',
_('Animation speed (FPS)'),
_('Get the animation speed (in frames per second)'),
'',
'JsPlatform/Extensions/tile_map.svg'
)
.addParameter('object', _('Tile map'), 'TileMap', false)
.getCodeExtraInformation()
.setFunctionName('getAnimationFps');
// Deprecated
object
.addAction(
'Scale',
_('Scale'),
_('Modify the scale of the specified object.'),
_('the scale'),
_('Size'),
'res/actions/scale24_black.png',
'res/actions/scale_black.png'
)
.addParameter('object', _('Tile map'), 'TileMap', false)
.useStandardOperatorParameters(
'number',
gd.ParameterOptions.makeNewOptions().setDescription(
_('Scale (1 by default)')
)
)
.markAsAdvanced()
.setHidden()
.getCodeExtraInformation()
.setFunctionName('setScale')
.setGetter('getScale');
// Deprecated
object
.addExpressionAndConditionAndAction(
'number',
'ScaleX',
_('Scale on X axis'),
_("the width's scale of an object"),
_("the width's scale"),
_('Size'),
'res/actions/scaleWidth24_black.png'
)
.addParameter('object', _('Tile map'), 'TileMap', false)
.useStandardParameters(
'number',
gd.ParameterOptions.makeNewOptions().setDescription(
_('Scale (1 by default)')
)
)
.markAsAdvanced()
.setHidden()
.setFunctionName('setScaleX')
.setGetter('getScaleX');
// Deprecated
object
.addExpressionAndConditionAndAction(
'number',
'ScaleY',
_('Scale on Y axis'),
_("the height's scale of an object"),
_("the height's scale"),
_('Size'),
'res/actions/scaleHeight24_black.png'
)
.addParameter('object', _('Tile map'), 'TileMap', false)
.useStandardParameters(
'number',
gd.ParameterOptions.makeNewOptions().setDescription(
_('Scale (1 by default)')
)
)
.markAsAdvanced()
.setHidden()
.setFunctionName('setScaleY')
.setGetter('getScaleY');
// Deprecated
object
.addAction(
'Width',
_('Width'),
_('Change the width of an object.'),
_('the width'),
_('Size'),
'res/actions/scaleWidth24_black.png',
'res/actions/scaleWidth_black.png'
)
.addParameter('object', _('Tile map'), 'TileMap', false)
.useStandardOperatorParameters(
'number',
gd.ParameterOptions.makeNewOptions()
)
.markAsAdvanced()
.setHidden()
.getCodeExtraInformation()
.setFunctionName('setWidth');
// Deprecated
object
.addAction(
'Height',
_('Height'),
_('Change the height of an object.'),
_('the height'),
_('Size'),
'res/actions/scaleHeight24_black.png',
'res/actions/scaleHeight_black.png'
)
.addParameter('object', _('Tile map'), 'TileMap', false)
.useStandardOperatorParameters(
'number',
gd.ParameterOptions.makeNewOptions()
)
.markAsAdvanced()
.setHidden()
.getCodeExtraInformation()
.setFunctionName('setHeight');
};
const defineCollisionMask = function (
extension,
_ /*: (string) => string */,
gd /*: libGDevelop */
) {
var collisionMaskObject = new gd.ObjectJsImplementation();
// $FlowExpectedError - ignore Flow warning as we're creating an object
collisionMaskObject.updateProperty = function (
objectContent,
propertyName,
newValue
) {
if (propertyName === 'tilemapJsonFile') {
objectContent.tilemapJsonFile = newValue;
return true;
}
if (propertyName === 'tilesetJsonFile') {
objectContent.tilesetJsonFile = newValue;
return true;
}
if (propertyName === 'collisionMaskTag') {
objectContent.collisionMaskTag = newValue;
return true;
}
if (propertyName === 'debugMode') {
objectContent.debugMode = newValue === '1';
return true;
}
if (propertyName === 'fillColor') {
objectContent.fillColor = newValue;
return true;
}
if (propertyName === 'outlineColor') {
objectContent.outlineColor = newValue;
return true;
}
if (propertyName === 'fillOpacity') {
objectContent.fillOpacity = parseFloat(newValue);
return true;
}
if (propertyName === 'outlineOpacity') {
objectContent.outlineOpacity = parseFloat(newValue);
return true;
}
if (propertyName === 'outlineSize') {
objectContent.outlineSize = parseFloat(newValue);
return true;
}
return false;
};
// $FlowExpectedError - ignore Flow warning as we're creating an object
collisionMaskObject.getProperties = function (objectContent) {
var objectProperties = new gd.MapStringPropertyDescriptor();
objectProperties.set(
'tilemapJsonFile',
new gd.PropertyDescriptor(objectContent.tilemapJsonFile)
.setType('resource')
.addExtraInfo('tilemap')
.addExtraInfo('json')
.setLabel(_('Tilemap JSON file'))
.setDescription(
_(
'This is the JSON file that was saved or exported from Tiled. LDtk is not supported yet for collisions.'
)
)
);
objectProperties.set(
'tilesetJsonFile',
new gd.PropertyDescriptor(objectContent.tilesetJsonFile || '')
.setType('resource')
.addExtraInfo('tileset')
.addExtraInfo('json')
.setLabel(_('Tileset JSON file (optional)'))
.setDescription(
_(
"Optional, don't specify it if you've not saved the tileset in a different file."
)
)
);
objectProperties.set(
'collisionMaskTag',
new gd.PropertyDescriptor(objectContent.collisionMaskTag)
.setType('string')
.setLabel(_('Class filter'))
.setDescription(
_(
'Only the tiles with the given class (set in Tiled 1.9+) will have hitboxes created.'
)
)
);
objectProperties.set(
'debugMode',
new gd.PropertyDescriptor(objectContent.debugMode ? 'true' : 'false')
.setType('boolean')
.setLabel(_('Debug mode'))
.setDescription(
_('When activated, it displays the hitboxes in the given color.')
)
);
objectProperties.set(
'outlineColor',
new gd.PropertyDescriptor(objectContent.outlineColor)
.setType('color')
.setLabel(_('Outline color'))
);
objectProperties.set(
'outlineOpacity',
new gd.PropertyDescriptor(
objectContent.outlineOpacity === undefined
? '64'
: objectContent.outlineOpacity.toString()
)
.setType('number')
.setLabel(_('Outline opacity (0-255)'))
);
objectProperties.set(
'outlineSize',
new gd.PropertyDescriptor(
objectContent.outlineSize === undefined
? '1'
: objectContent.outlineSize.toString()
)
.setType('number')
.setLabel(_('Outline size (in pixels)'))
);
objectProperties.set(
'fillColor',
new gd.PropertyDescriptor(objectContent.fillColor)
.setType('color')
.setLabel(_('Fill color'))
);
objectProperties.set(
'fillOpacity',
new gd.PropertyDescriptor(
objectContent.fillOpacity === undefined
? '32'
: objectContent.fillOpacity.toString()
)
.setType('number')
.setLabel(_('Fill opacity (0-255)'))
);
return objectProperties;
};
collisionMaskObject.setRawJSONContent(
JSON.stringify({
tilemapJsonFile: '',
tilesetJsonFile: '',
collisionMaskTag: '',
debugMode: false,
fillColor: '255;255;255',
outlineColor: '255;255;255',
fillOpacity: 64,
outlineOpacity: 128,
outlineSize: 1,
})
);
// $FlowExpectedError - ignore Flow warning as we're creating an object
collisionMaskObject.updateInitialInstanceProperty = function (
objectContent,
instance,
propertyName,
newValue,
project,
layout
) {
return false;
};
// $FlowExpectedError - ignore Flow warning as we're creating an object
collisionMaskObject.getInitialInstanceProperties = function (
content,
instance,
project,
layout
) {
var instanceProperties = new gd.MapStringPropertyDescriptor();
return instanceProperties;
};
const object = extension
.addObject(
'CollisionMask',
_('Tilemap collision mask'),
_('Invisible object handling collisions with parts of a tilemap.'),
'JsPlatform/Extensions/tile_map_collision_mask32.svg',
collisionMaskObject
)
.setCategoryFullName(_('Advanced'))
.addDefaultBehavior('EffectCapability::EffectBehavior')
.addDefaultBehavior('ResizableCapability::ResizableBehavior')
.addDefaultBehavior('ScalableCapability::ScalableBehavior')
.setIncludeFile('Extensions/TileMap/tilemapcollisionmaskruntimeobject.js')
.addIncludeFile('Extensions/TileMap/TileMapRuntimeManager.js')
.addIncludeFile('Extensions/TileMap/pako/dist/pako.min.js')
.addIncludeFile('Extensions/TileMap/helper/TileMapHelper.js')
.addIncludeFile('Extensions/TileMap/collision/TransformedTileMap.js')
.addIncludeFile(
'Extensions/TileMap/collision/TileMapCollisionMaskRenderer.js'
);
object
.addScopedCondition(
'TilemapJsonFile',
_('Tilemap JSON file'),
_('Check the Tilemap JSON file being used.'),
_('The Tilemap JSON file of _PARAM0_ is _PARAM1_'),
'',
'JsPlatform/Extensions/tile_map_collision_mask24.svg',
'JsPlatform/Extensions/tile_map_collision_mask32.svg'
)
.addParameter(
'object',
_('Tile map collision mask'),
'CollisionMask',
false
)
.addParameter('jsonResource', _('Tilemap JSON file'), '', false)
.getCodeExtraInformation()
.setFunctionName('isTilemapJsonFile');
object
.addScopedAction(
'SetTilemapJsonFile',
_('Tilemap JSON file'),
_(
'Set the JSON file containing the Tilemap data to display. This is usually the JSON file exported from Tiled.'
),
_('Set the Tilemap JSON file of _PARAM0_ to _PARAM1_'),
'',
'JsPlatform/Extensions/tile_map_collision_mask24.svg',
'JsPlatform/Extensions/tile_map_collision_mask32.svg'
)
.addParameter(
'object',
_('Tile map collision mask'),
'CollisionMask',
false
)
.addParameter('jsonResource', _('Tilemap JSON file'), '', false)
.getCodeExtraInformation()
.setFunctionName('setTilemapJsonFile');
object
.addScopedCondition(
'TilesetJsonFile',
_('Tileset JSON file'),
_('Check the tileset JSON file being used.'),
_('The tileset JSON file of _PARAM0_ is _PARAM1_'),
'',
'JsPlatform/Extensions/tile_map_collision_mask24.svg',
'JsPlatform/Extensions/tile_map_collision_mask32.svg'
)
.addParameter(
'object',
_('Tile map collision mask'),
'CollisionMask',
false
)
.addParameter('jsonResource', _('Tileset JSON file'), '', false)
.getCodeExtraInformation()
.setFunctionName('isTilesetJsonFile');
object
.addScopedAction(
'SetTilesetJsonFile',
_('Tileset JSON file'),
_(
'Set the JSON file with the tileset data (sometimes that is embedded in the Tilemap, so not needed)'
),
_('Set the tileset JSON file of _PARAM0_ to _PARAM1_'),
'',
'JsPlatform/Extensions/tile_map_collision_mask24.svg',
'JsPlatform/Extensions/tile_map_collision_mask32.svg'
)
.addParameter(
'object',
_('Tile map collision mask'),
'CollisionMask',
false
)
.addParameter('jsonResource', _('Tileset JSON file'), '', false)
.getCodeExtraInformation()
.setFunctionName('setTilesetJsonFile');
// Deprecated
object
.addScopedAction(
'Scale',
_('Scale'),
_('Modify the scale of the specified object.'),
_('the scale'),
_('Size'),
'res/actions/scale24_black.png',
'res/actions/scale_black.png'
)
.addParameter(
'object',
_('Tile map collision mask'),
'CollisionMask',
false
)
.useStandardOperatorParameters(
'number',
gd.ParameterOptions.makeNewOptions().setDescription(
_('Scale (1 by default)')
)
)
.markAsAdvanced()
.setHidden()
.getCodeExtraInformation()
.setFunctionName('setScale')
.setGetter('getScale');
// Deprecated
object
.addExpressionAndConditionAndAction(
'number',
'ScaleX',
_('Scale on X axis'),
_("the width's scale of an object"),
_("the width's scale"),
_('Size'),
'res/actions/scaleWidth24_black.png'
)
.addParameter(
'object',
_('Tile map collision mask'),
'CollisionMask',
false
)
.useStandardParameters(
'number',
gd.ParameterOptions.makeNewOptions().setDescription(
_('Scale (1 by default)')
)
)
.markAsAdvanced()
.setHidden()
.setFunctionName('setScaleX')
.setGetter('getScaleX');
// Deprecated
object
.addExpressionAndConditionAndAction(
'number',
'ScaleY',
_('Scale on Y axis'),
_("the height's scale of an object"),
_("the height's scale"),
_('Size'),
'res/actions/scaleHeight24_black.png'
)
.addParameter(
'object',
_('Tile map collision mask'),
'CollisionMask',
false
)
.useStandardParameters(
'number',
gd.ParameterOptions.makeNewOptions().setDescription(
_('Scale (1 by default)')
)
)
.markAsAdvanced()
.setHidden()
.setFunctionName('setScaleY')
.setGetter('getScaleY');
// Deprecated
object
.addScopedAction(
'Width',
_('Width'),
_('Change the width of an object.'),
_('the width'),
_('Size'),
'res/actions/scaleWidth24_black.png',
'res/actions/scaleWidth_black.png'
)
.addParameter(
'object',
_('Tile map collision mask'),
'CollisionMask',
false
)
.useStandardOperatorParameters(
'number',
gd.ParameterOptions.makeNewOptions()
)
.markAsAdvanced()
.setHidden()
.getCodeExtraInformation()
.setFunctionName('setWidth');
// Deprecated
object
.addScopedAction(
'Height',
_('Height'),
_('Change the height of an object.'),
_('the height'),
_('Size'),
'res/actions/scaleHeight24_black.png',
'res/actions/scaleHeight_black.png'
)
.addParameter(
'object',
_('Tile map collision mask'),
'CollisionMask',
false
)
.useStandardOperatorParameters(
'number',
gd.ParameterOptions.makeNewOptions()
)
.markAsAdvanced()
.setHidden()
.getCodeExtraInformation()
.setFunctionName('setHeight');
};
module.exports = {
createExtension: function (
_ /*: (string) => string */,
gd /*: libGDevelop */
) {
const extension = new gd.PlatformExtension();
extension
.setExtensionInformation(
'TileMap',
_('Tilemap'),
_(
"The Tilemap object can be used to display tile-based objects. It's a good way to create maps for RPG, strategy games or create objects by assembling tiles, useful for platformer, retro-looking games, etc..."
),
'Todor Imreorov',
'Open source (MIT License)'
)
.setCategory('Advanced')
.setExtensionHelpPath('/objects/tilemap');
extension
.addInstructionOrExpressionGroupMetadata(_('Tilemap'))
.setIcon('JsPlatform/Extensions/tile_map.svg');
defineTileMap(extension, _, gd);
defineCollisionMask(extension, _, gd);
return extension;
},
registerClearCache: function (
objectsRenderingService /*: ObjectsRenderingService */
) {
const TilemapHelper = objectsRenderingService.requireModule(
__dirname,
'helper/TileMapHelper'
);
const clearCaches = (
project /* InstanceHolder - gdProject in the editor */
) => {
/** @type {TileMapHelper.TileMapManager} */
const manager = TilemapHelper.TileMapManager.getManager(project);
manager.clearCaches();
};
objectsRenderingService.registerClearCache(clearCaches);
},
/**
* You can optionally add sanity tests that will check the basic working
* of your extension behaviors/objects by instantiating behaviors/objects
* and setting the property to a given value.
*
* If you don't have any tests, you can simply return an empty array like this:
* `runExtensionSanityTests: function(gd, extension) { return []; }`
*
* But it is recommended to create tests for the behaviors/objects properties you created
* to avoid mistakes.
*/
runExtensionSanityTests: function (gd, extension) {
return [];
},
/**
* Register editors for objects.
*
* Run `node import-GDJS-Runtime.js` (in newIDE/app/scripts) if you make any change.
*/
registerEditorConfigurations: function (objectsEditorService) {
objectsEditorService.registerEditorConfiguration(
'TileMap::TileMap',
objectsEditorService.getDefaultObjectJsImplementationPropertiesEditor({
helpPagePath: '/objects/tilemap',
})
);
objectsEditorService.registerEditorConfiguration(
'TileMap::CollisionMask',
objectsEditorService.getDefaultObjectJsImplementationPropertiesEditor({
helpPagePath: '/objects/tilemap',
})
);
},
/**
* Register renderers for instance of objects on the scene editor.
*
* Run `node import-GDJS-Runtime.js` (in newIDE/app/scripts) if you make any change.
*/
registerInstanceRenderers: function (objectsRenderingService) {
const RenderedInstance = objectsRenderingService.RenderedInstance;
const PIXI = objectsRenderingService.PIXI;
const Tilemap = objectsRenderingService.requireModule(
__dirname,
'pixi-tilemap/dist/pixi-tilemap.umd'
);
const TilemapHelper = objectsRenderingService.requireModule(
__dirname,
'helper/TileMapHelper'
);
// required for decoding tiled zlib compressed layer data
const pako = objectsRenderingService.requireModule(
__dirname,
'pako/dist/pako.min'
);
// When on the webapp, and using webpack, the extension does not seem to
// be able to register itself properly. So we do it manually.
// (This should be done here https://github.com/pixijs/tilemap/blob/master/src/index.ts#L43-L47)
PIXI.extensions.add({
name: 'tilemap',
type: PIXI.ExtensionType.RendererPlugin,
ref: Tilemap.TileRenderer,
});
/**
* Renderer for instances of TileMap inside the IDE.
*/
class RenderedTileMapInstance extends RenderedInstance {
constructor(
project,
layout,
instance,
associatedObjectConfiguration,
pixiContainer,
pixiResourcesLoader
) {
super(
project,
layout,
instance,
associatedObjectConfiguration,
pixiContainer,
pixiResourcesLoader
);
// This setting allows tile maps with more than 16K tiles.
Tilemap.settings.use32bitIndex = true;
this.tileMapPixiObject = new Tilemap.CompositeTilemap();
this._pixiObject = this.tileMapPixiObject;
// Implement `containsPoint` so that we can set `interactive` to true and
// the Tilemap will properly emit events when hovered/clicked.
// By default, this is not implemented in pixi-tilemap.
this._pixiObject.containsPoint = (position) => {
// Turns the world position to the local object coordinates
const localPosition = new PIXI.Point();
this._pixiObject.worldTransform.applyInverse(position, localPosition);
return (
localPosition.x >= 0 &&
localPosition.x < this.width &&
localPosition.y >= 0 &&
localPosition.y < this.height
);
};
this._pixiContainer.addChild(this._pixiObject);
this.width = 48;
this.height = 48;
this.update();
this.updateTileMap();
}
onRemovedFromScene() {
super.onRemovedFromScene();
// Keep textures because they are shared by all tile maps.
this._pixiObject.destroy(false);
}
onLoadingError() {
this.errorPixiObject =
this.errorPixiObject ||
new PIXI.Sprite(this._pixiResourcesLoader.getInvalidPIXITexture());
this._pixiContainer.addChild(this.errorPixiObject);
this._pixiObject = this.errorPixiObject;
}
onLoadingSuccess() {
if (this.errorPixiObject) {
this._pixiContainer.removeChild(this.errorPixiObject);
this.errorPixiObject = null;
this._pixiObject = this.tileMapPixiObject;
}
}
/**
* Return the path to the thumbnail of the specified object.
*/
static getThumbnail(project, resourcesLoader, objectConfiguration) {
return 'JsPlatform/Extensions/tile_map.svg';
}
/**
* This is used to reload the Tilemap
*/
updateTileMap() {
// Get the tileset resource to use
const tilemapAtlasImage = this._associatedObjectConfiguration
.getProperties(this.project)
.get('tilemapAtlasImage')
.getValue();
const tilemapJsonFile = this._associatedObjectConfiguration
.getProperties(this.project)
.get('tilemapJsonFile')
.getValue();
const tilesetJsonFile = this._associatedObjectConfiguration
.getProperties(this.project)
.get('tilesetJsonFile')
.getValue();
const layerIndex = parseInt(
this._associatedObjectConfiguration
.getProperties(this.project)
.get('layerIndex')
.getValue(),
10
);
const levelIndex = parseInt(
this._associatedObjectConfiguration
.getProperties(this.project)
.get('levelIndex')
.getValue(),
10
);
const displayMode = this._associatedObjectConfiguration
.getProperties(this.project)
.get('displayMode')
.getValue();
const tilemapResource = this._project
.getResourcesManager()
.getResource(tilemapJsonFile);
let metadata = {};
try {
const tilemapMetadataAsString = tilemapResource.getMetadata();
if (tilemapMetadataAsString)
metadata = JSON.parse(tilemapMetadataAsString);
} catch (error) {
console.warn('Malformed metadata in a tilemap object:', error);
}
const mapping = metadata.embeddedResourcesMapping || {};
const atlasTexture = this._pixiResourcesLoader.getPIXITexture(
this._project,
tilemapAtlasImage
);
const loadTileMap = () => {
/** @type {TileMapHelper.TileMapManager} */
const manager = TilemapHelper.TileMapManager.getManager(
this._project
);
manager.getOrLoadTileMap(
this._loadTileMapWithCallback.bind(this),
tilemapJsonFile,
tilesetJsonFile,
levelIndex,
pako,
(tileMap) => {
if (!tileMap) {
this.onLoadingError();
// _loadTileMapWithCallback already log errors
return;
}
/** @type {TileMapHelper.TileTextureCache} */
const textureCache = manager.getOrLoadTextureCache(
this._loadTileMapWithCallback.bind(this),
(textureName) =>
this._pixiResourcesLoader.getPIXITexture(
this._project,
mapping[textureName] || textureName
),
tilemapAtlasImage,
tilemapJsonFile,
tilesetJsonFile,
levelIndex,
(textureCache) => {
if (!textureCache) {
this.onLoadingError();
// getOrLoadTextureCache already log warns and errors.
return;
}
this.onLoadingSuccess();
this.width = tileMap.getWidth();
this.height = tileMap.getHeight();
TilemapHelper.PixiTileMapHelper.updatePixiTileMap(
this.tileMapPixiObject,
tileMap,
textureCache,
displayMode,
layerIndex
);
}
);
}
);
};
if (atlasTexture.valid) {
loadTileMap();
} else {
// Wait for the atlas image to load.
atlasTexture.once('update', () => {
loadTileMap();
});
}
}
// GDJS doesn't use Promise to avoid allocation.
_loadTileMapWithCallback(tilemapJsonFile, tilesetJsonFile, callback) {
this._loadTileMap(tilemapJsonFile, tilesetJsonFile).then(callback);
}
async _loadTileMap(tilemapJsonFile, tilesetJsonFile) {
try {
const tileMapJsonData = await this._pixiResourcesLoader.getResourceJsonData(
this._project,
tilemapJsonFile
);
const tileMap = TilemapHelper.TileMapManager.identify(
tileMapJsonData
);
if (tileMap.kind === 'tiled') {
const tilesetJsonData = tilesetJsonFile
? await this._pixiResourcesLoader.getResourceJsonData(
this._project,
tilesetJsonFile
)
: null;
if (tilesetJsonData) {
tileMapJsonData.tilesets = [tilesetJsonData];
}
}
return tileMap;
} catch (err) {
console.error('Unable to load a Tilemap JSON data: ', err);
}
return null;
}
/**
* This is called to update the PIXI object on the scene editor
*/
update() {
if (this._instance.hasCustomSize()) {
this._pixiObject.scale.x = this.getCustomWidth() / this.width;
this._pixiObject.scale.y = this.getCustomHeight() / this.height;
} else {
this._pixiObject.scale.x = 1;
this._pixiObject.scale.y = 1;
}
// Place the center of rotation in the center of the object. Because pivot position in Pixi
// is in the **local coordinates of the object**, we need to find back the original width
// and height of the object before scaling (then divide by 2 to find the center)
const originalWidth = this.width;
const originalHeight = this.height;
this._pixiObject.pivot.x = originalWidth / 2;
this._pixiObject.pivot.y = originalHeight / 2;
// Modifying the pivot position also has an impact on the transform. The instance (X,Y) position
// of this object refers to the top-left point, but now in Pixi, as we changed the pivot, the Pixi
// object (X,Y) position refers to the center. So we add an offset to convert from top-left to center.
this._pixiObject.x =
this._instance.getX() +
this._pixiObject.pivot.x * this._pixiObject.scale.x;
this._pixiObject.y =
this._instance.getY() +
this._pixiObject.pivot.y * this._pixiObject.scale.y;
// Rotation works as intended because we put the pivot in the center
this._pixiObject.rotation = RenderedInstance.toRad(
this._instance.getAngle()
);
}
/**
* Return the width of the instance, when it's not resized.
*/
getDefaultWidth() {
return this.width;
}
/**
* Return the height of the instance, when it's not resized.
*/
getDefaultHeight() {
return this.height;
}
}
objectsRenderingService.registerInstanceRenderer(
'TileMap::TileMap',
RenderedTileMapInstance
);
/**
* Renderer for instances of TileMap inside the IDE.
*/
class RenderedCollisionMaskInstance extends RenderedInstance {
constructor(
project,
layout,
instance,
associatedObjectConfiguration,
pixiContainer,
pixiResourcesLoader
) {
super(
project,
layout,
instance,
associatedObjectConfiguration,
pixiContainer,
pixiResourcesLoader
);
this.tileMapPixiObject = new PIXI.Graphics();
this._pixiObject = this.tileMapPixiObject;
// Implement `containsPoint` so that we can set `interactive` to true and
// the Tilemap will properly emit events when hovered/clicked.
// By default, this is not implemented in pixi-tilemap.
this._pixiObject.containsPoint = (position) => {
// Turns the world position to the local object coordinates
const localPosition = new PIXI.Point();
this._pixiObject.worldTransform.applyInverse(position, localPosition);
// Check if the point is inside the object bounds
return (
localPosition.x >= 0 &&
localPosition.x < this.width &&
localPosition.y >= 0 &&
localPosition.y < this.height
);
};
this._pixiContainer.addChild(this._pixiObject);
this.width = 48;
this.height = 48;
this.update();
this.updateTileMap();
}
onRemovedFromScene() {
super.onRemovedFromScene();
this._pixiObject.destroy();
}
onLoadingError() {
this.errorPixiObject =
this.errorPixiObject ||
new PIXI.Sprite(this._pixiResourcesLoader.getInvalidPIXITexture());
this._pixiContainer.addChild(this.errorPixiObject);
this._pixiObject = this.errorPixiObject;
}
onLoadingSuccess() {
if (this.errorPixiObject) {
this._pixiContainer.removeChild(this.errorPixiObject);
this.errorPixiObject = null;
this._pixiObject = this.tileMapPixiObject;
}
}
/**
* Return the path to the thumbnail of the specified object.
*/
static getThumbnail(project, resourcesLoader, objectConfiguration) {
return 'JsPlatform/Extensions/tile_map_collision_mask24.svg';
}
/**
* This is used to reload the Tilemap
*/
updateTileMap() {
// Get the tileset resource to use
const tilemapAtlasImage = this._associatedObjectConfiguration
.getProperties(this.project)
.get('tilemapAtlasImage')
.getValue();
const tilemapJsonFile = this._associatedObjectConfiguration
.getProperties(this.project)
.get('tilemapJsonFile')
.getValue();
const tilesetJsonFile = this._associatedObjectConfiguration
.getProperties(this.project)
.get('tilesetJsonFile')
.getValue();
const collisionMaskTag = this._associatedObjectConfiguration
.getProperties(this.project)
.get('collisionMaskTag')
.getValue();
const outlineColor = objectsRenderingService.rgbOrHexToHexNumber(
this._associatedObjectConfiguration
.getProperties(this.project)
.get('outlineColor')
.getValue()
);
const fillColor = objectsRenderingService.rgbOrHexToHexNumber(
this._associatedObjectConfiguration
.getProperties(this.project)
.get('fillColor')
.getValue()
);
const outlineOpacity =
this._associatedObjectConfiguration
.getProperties(this.project)
.get('outlineOpacity')
.getValue() / 255;
const fillOpacity =
this._associatedObjectConfiguration
.getProperties(this.project)
.get('fillOpacity')
.getValue() / 255;
const outlineSize = 1;
/** @type {TileMapHelper.TileMapManager} */
const manager = TilemapHelper.TileMapManager.getManager(this._project);
manager.getOrLoadTileMap(
this._loadTiledMapWithCallback.bind(this),
tilemapJsonFile,
tilesetJsonFile,
0, // levelIndex
pako,
(tileMap) => {
if (!tileMap) {
this.onLoadingError();
// _loadTiledMapWithCallback already log errors
return;
}
this.onLoadingSuccess();
this.width = tileMap.getWidth();
this.height = tileMap.getHeight();
TilemapHelper.PixiTileMapHelper.updatePixiCollisionMask(
this._pixiObject,
tileMap,
collisionMaskTag,
outlineSize,
outlineColor,
outlineOpacity,
fillColor,
fillOpacity
);
}
);
}
// GDJS doesn't use Promise to avoid allocation.
_loadTiledMapWithCallback(tilemapJsonFile, tilesetJsonFile, callback) {
this._loadTileMap(tilemapJsonFile, tilesetJsonFile).then(callback);
}
async _loadTileMap(tilemapJsonFile, tilesetJsonFile) {
try {
const tileMapJsonData = await this._pixiResourcesLoader.getResourceJsonData(
this._project,
tilemapJsonFile
);
const tileMap = TilemapHelper.TileMapManager.identify(
tileMapJsonData
);
if (tileMap.kind === 'tiled') {
const tilesetJsonData = tilesetJsonFile
? await this._pixiResourcesLoader.getResourceJsonData(
this._project,
tilesetJsonFile
)
: null;
if (tilesetJsonData) {
tileMapJsonData.tilesets = [tilesetJsonData];
}
}
return tileMap;
} catch (err) {
console.error('Unable to load a Tilemap JSON data: ', err);
}
return null;
}
/**
* This is called to update the PIXI object on the scene editor
*/
update() {
if (this._instance.hasCustomSize()) {
this._pixiObject.scale.x = this.getCustomWidth() / this.width;
this._pixiObject.scale.y = this.getCustomHeight() / this.height;
} else {
this._pixiObject.scale.x = 1;
this._pixiObject.scale.y = 1;
}
// Place the center of rotation in the center of the object. Because pivot position in Pixi
// is in the **local coordinates of the object**, we need to find back the original width
// and height of the object before scaling (then divide by 2 to find the center)
const originalWidth = this.width;
const originalHeight = this.height;
this._pixiObject.pivot.x = originalWidth / 2;
this._pixiObject.pivot.y = originalHeight / 2;
// Modifying the pivot position also has an impact on the transform. The instance (X,Y) position
// of this object refers to the top-left point, but now in Pixi, as we changed the pivot, the Pixi
// object (X,Y) position refers to the center. So we add an offset to convert from top-left to center.
this._pixiObject.x =
this._instance.getX() +
this._pixiObject.pivot.x * this._pixiObject.scale.x;
this._pixiObject.y =
this._instance.getY() +
this._pixiObject.pivot.y * this._pixiObject.scale.y;
// Rotation works as intended because we put the pivot in the center
this._pixiObject.rotation = RenderedInstance.toRad(
this._instance.getAngle()
);
}
/**
* Return the width of the instance, when it's not resized.
*/
getDefaultWidth() {
return this.width;
}
/**
* Return the height of the instance, when it's not resized.
*/
getDefaultHeight() {
return this.height;
}
}
objectsRenderingService.registerInstanceRenderer(
'TileMap::CollisionMask',
RenderedCollisionMaskInstance
);
},
};