/* global atob, platypus */
import {arrayCache, greenSlice, union} from '../utils/array.js';
import AABB from '../AABB.js';
import Data from '../Data.js';
import DataMap from '../DataMap.js';
import Entity from '../Entity.js';
import EntityLinker from '../EntityLinker';
import Vector from '../Vector.js';
import createComponentClass from '../factory.js';
import {inflate} from 'pako';
const
FILENAME_TO_ID = /^(?:(\w+:)\/{2}(\w+(?:\.\w+)*\/?))?([\/.]*?(?:[^?]+)?\/)?(?:(([^\/?]+)\.(\w+))|([^\/?]+))(?:\?((?:(?:[^&]*?[\/=])?(?:((?:(?:[^\/?&=]+)\.(\w+)))\S*?)|\S+))?)?$/,
POSITIONS = {
top: -1,
bottom: 1,
left: -1,
right: 1,
center: 0,
middle: 0
},
defaultEntityModifier = (object) => object,
maskId = 0x0fffffff,
maskXFlip = 0x80000000,
maskYFlip = 0x40000000,
maskRotate = 0x20000000,
decodeString = (str, index) => (((str.charCodeAt(index)) + (str.charCodeAt(index + 1) << 8) + (str.charCodeAt(index + 2) << 16) + (str.charCodeAt(index + 3) << 24 )) >>> 0),
decodeArray = (arr, index) => ((arr[index] + (arr[index + 1] << 8) + (arr[index + 2] << 16) + (arr[index + 3] << 24 )) >>> 0),
decodeBase64 = (data, compression) => {
const
arr = [],
compressed = compression === 'zlib',
step1 = window.atob(data.replace(/\\/g, '')),
step2 = compressed ? inflate(step1) : step1,
decode = compressed ? decodeArray : decodeString;
let index = 4;
while (index <= step2.length) {
arr.push(decode(step2, index - 4));
index += 4;
}
return arr;
},
decodeLayer = function (layer) {
if (layer.encoding === 'base64') {
layer.data = decodeBase64(layer.data, layer.compression);
layer.encoding = 'csv'; // So we won't have to decode again.
}
return layer;
},
getKey = function (path) {
const
result = path.match(FILENAME_TO_ID);
return result[5] ?? result[7];
},
getPowerOfTen = function (amount) {
let x = 1;
while (x < amount) {
x *= 10;
}
return x;
},
transform = {
x: 1,
y: 1,
id: -1
},
getProperty = (...args) => {
const obj = getPropertyObject(...args);
return obj && obj.value;
},
getPropertyObject = (obj, key) => { // Handle Tiled map versions
if (obj) {
if (Array.isArray(obj)) {
let i = obj.length;
while (i--) {
if (obj[i].name === key) {
return obj[i];
}
}
return null;
} else {
platypus.debug.warn('This Tiled map version is deprecated.');
return {
name: key,
type: typeof obj[key],
value: obj[key]
};
}
} else {
return null;
}
},
setProperty = (obj, key, value) => { // Handle Tiled map versions
if (obj) {
if (Array.isArray(obj)) {
let i = obj.length;
while (i--) {
if (obj[i].name === key) {
obj[i].type = typeof value;
obj[i].value = value;
return;
}
}
obj.push({
name: key,
type: typeof value,
value: value
});
} else {
obj[key] = value;
}
}
},
entityTransformCheck = function (v) {
const
a = !!(maskRotate & v),
b = !!(maskYFlip & v),
c = !!(maskXFlip & v);
transform.id = maskId & v;
transform.x = c ? -1 : 1;
transform.y = b ? -1 : 1;
transform.rotation = a ? 90 : 0;
return transform;
},
createTilesetObjectGroupReference = function (reference, tilesets) {
for (let i = 0; i < tilesets.length; i++) {
const
tileset = tilesets[i],
tiles = tileset.tiles;
if (tiles) {
for (let j = 0; j < tiles.length; j++) {
const
tile = tiles[j];
reference.set(tile.id + tileset.firstgid, tile);
}
}
}
},
getObjectCentered = (object) => {
const
{height = 0, rotation = 0, width = 0, x = 0, y = 0} = object,
hw = width / 2,
hh = height / 2,
angle = rotation / 180 * Math.PI,
topLeftOffset = Vector.setUp(hw, hh).rotate(angle),
centerOfObject = Vector.setUp(x, y).addVector(topLeftOffset),
centeredObject = {
...object,
x: centerOfObject.x,
y: centerOfObject.y
};
topLeftOffset.recycle();
centerOfObject.recycle();
return centeredObject;
},
getObjectTransformed = (object, {rotation = 0, x = 1, y = 1}, width = 0, height = 0) => {
const
coords = Vector.setUp(object.x, object.y).add(-width / 2, -height / 2);
if (rotation) {
coords.rotate(rotation / 180 * Math.PI);
coords.x *= -x;
coords.y *= y;
} else {
coords.x *= x;
coords.y *= y;
}
coords.add(width / 2, height / 2);
return {
...object,
rotation: object.rotation + rotation,
flipX: x,
flipY: y,
x: coords.x,
y: coords.y
};
},
getEntityData = function (obj, tilesets, entityLinker) {
const
properties = {},
data = {
gid: -1,
transform: null,
properties,
type: ''
};
let gid = obj.gid || -1,
props = null;
if (gid !== -1) {
data.transform = entityTransformCheck(gid);
gid = data.gid = transform.id;
}
if (tilesets) {
let tileset = null;
for (let x = 0; x < tilesets.length; x++) {
if (tilesets[x].firstgid > gid) {
break;
} else {
tileset = tilesets[x];
}
}
if (tileset?.tiles) {
const
tiles = tileset.tiles,
entityTilesetIndex = gid - tileset.firstgid;
for (let i = 0; i < tiles.length; i++) {
const tile = tiles[i];
if (tile.id === entityTilesetIndex) {
props = tile.properties || null;
data.type = tile.type || '';
break;
}
}
}
}
// Check Tiled data to find this object's type
data.type = obj.type || data.type;
if (!data.type) { // undefined entity
return null;
}
//Copy properties from Tiled
if (data.transform) {
properties.scaleX = data.transform.x;
properties.scaleY = data.transform.y;
} else {
properties.scaleX = 1;
properties.scaleY = 1;
}
if (obj.name) {
properties.name = obj.name; // Unlike "type", this specifies particular entities. Unlike "id", entities can share the same name.
}
if (entityLinker) {
entityLinker.linkObject(data.properties.tiledId = obj.id);
}
mergeAndFormatProperties(props, data.properties, entityLinker);
mergeAndFormatProperties(obj.properties, data.properties, entityLinker);
return data;
},
mergeAndFormatProperties = function (src, dest, entityLinker) {
if (src && dest) {
if (Array.isArray(src)) {
for (let i = 0; i < src.length; i++) {
setProperty(dest, src[i].name, formatPropertyObject(src[i], entityLinker));
}
} else {
const
keys = Object.keys(src),
{length} = keys;
for (let i = 0; i < length; i++) {
const
key = keys[i];
setProperty(dest, key, formatProperty(src[key]));
}
}
}
return dest;
},
formatProperty = function (value) {
if (typeof value === 'string') {
//This is going to assume that if you pass in something that starts with a number, it is a number and converts it to one.
// eslint-disable-next-line radix
const
numberProperty = parseFloat(value) ?? parseInt(value); // to handle floats and 0x respectively.
if (numberProperty === 0 || (!!numberProperty)) {
return numberProperty;
} else if (value === 'true') {
return true;
} else if (value === 'false') {
return false;
} else if ((value.length > 1) && (((value[0] === '{') && (value[value.length - 1] === '}')) || ((value[0] === '[') && (value[value.length - 1] === ']')))) {
try {
return JSON.parse(value);
} catch (e) {
}
}
}
return value;
},
formatPropertyObject = function ({name, value, type}, entityLinker) {
switch (type) {
case 'color':
// Convert ARGB to RGBA
return `#${value.substring(3)}${value.substring(1, 3)}`;
case 'object':
if (entityLinker && value !== 0) {
return entityLinker.getEntity(value, name); // if unfound, entityLinker saves this request and will try to fulfill it once the entity is added.
} else {
return null;
}
case 'string':
if ((value.length > 1) && (((value[0] === '{') && (value[value.length - 1] === '}')) || ((value[0] === '[') && (value[value.length - 1] === ']')))) {
try {
return JSON.parse(value);
} catch (e) {
}
}
break;
case 'bool':
case 'float':
case 'file':
case 'int':
default:
break;
}
return value;
},
checkLevel = function (levelOrLevelId, ss) {
const
addObjectGroupAssets = (assets, objectGroup, tilesets) => {
const objects = objectGroup.objects;
for (let i = 0; i < objects.length; i++) {
const entity = getEntityData(objects[i], tilesets);
if (entity) {
const entityAssets = Entity.getAssetList(entity);
union(assets, entityAssets);
arrayCache.recycle(entityAssets);
}
}
},
level = (typeof levelOrLevelId === 'string') ? platypus.game.settings.levels[levelOrLevelId] : levelOrLevelId,
assets = arrayCache.setUp();
if (level) {
if (level.tilesets) {
level.tilesets = importTilesetData(level.tilesets);
}
if (level.assets) { // Property added by a previous parse (so that this algorithm isn't run on the same level multiple times)
union(assets, level.assets);
} else if (level.layers) {
for (let i = 0; i < level.layers.length; i++) {
const
layer = level.layers[i];
if (layer.type === 'objectgroup') {
addObjectGroupAssets(assets, layer, level.tilesets);
} else if (layer.type === 'imagelayer') {
// Check for custom layer entity
const
entityType = getProperty(layer.properties, 'entity');
if (entityType) {
const
data = Data.setUp('type', entityType, 'properties', mergeAndFormatProperties(layer.properties, {})),
arr = Entity.getAssetList(data);
union(assets, arr);
arrayCache.recycle(arr);
data.recycle();
} else {
union(assets, [layer.image]);
}
} else {
const
entityType = getProperty(level.layers[i].properties, 'entity'),
tiles = arrayCache.setUp();
// must decode first so we can check for tiles' objects
decodeLayer(layer);
// Check for relevant objectgroups in tileset
union(tiles, layer.data); // merge used tiles into one-off list
for (let j = 0; j < tiles.length; j++) {
const
id = maskId & tiles[j];
for (let k = 0; k < level.tilesets.length; k++) {
const
{tiles, firstgid} = level.tilesets[k];
if (tiles) {
for (let l = 0; l < tiles.length; l++) {
const
tile = tiles[l];
if (((tile.id + firstgid) === id) && tile.objectgroup) {
addObjectGroupAssets(assets, tile.objectgroup);
}
}
}
}
}
// Check for custom layer entity
if (entityType) {
const
data = Data.setUp('type', entityType),
arr = Entity.getAssetList(data);
union(assets, arr);
arrayCache.recycle(arr);
data.recycle();
}
}
}
if (!ss) { //We need to load the tileset images since there is not a separate spriteSheet describing them
const
levelTilesets = level.tilesets,
tilesets = arrayCache.setUp();
for (let i = 0; i < levelTilesets.length; i++) {
const
tileset = levelTilesets[i];
if (tileset.image) {
tilesets.push(tileset.image);
} else {
const
tiles = tileset.tiles;
for (let j = 0; j < tiles.length; j++) {
const
tile = tiles[j];
if (tile.image) {
tilesets.push(tile.image);
}
}
}
}
union(assets, tilesets);
arrayCache.recycle(tilesets);
}
level.assets = greenSlice(assets); // Save for later in case this level is checked again.
}
}
return assets;
},
// These are provided but can be overwritten by entities of the same name in the configuration.
standardEntityLayers = {
"render-layer": {
"id": "render-layer",
"components": [{
"type": "RenderTiles",
"spriteSheet": "import",
"imageMap": "import",
"entityCache": true
}]
},
"collision-layer": {
"id": "collision-layer",
"components": [{
"type": "CollisionTiles",
"collisionMap": "import"
}]
},
"image-layer": {
"id": "image-layer",
"components": [{
"type": "RenderTiles",
"spriteSheet": "import",
"imageMap": "import"
}]
}
},
importTileset = function (tileset) {
const
source = platypus.game.settings.tilesets[getKey(tileset.source)],
keys = Object.keys(source),
{length} = keys;
for (let i = 0; i < length; i++) {
const
key = keys[i];
tileset[key] = source[key];
}
delete tileset.source; // We remove this so we never have to rerun this import. Note that we can't simply replace the tileset properties since the tileset's firstgid property may change from level to level.
return tileset;
},
importTilesetData = function (tilesets) {
for (let i = 0; i < tilesets.length; i++) {
if (tilesets[i].source) {
tilesets[i] = importTileset(tilesets[i]);
}
}
return tilesets;
};
export default createComponentClass(/** @lends platypus.components.TiledLoader.prototype */{
id: 'TiledLoader',
properties: {
/**
* This causes the entire map to be offset automatically by an order of magnitude higher than the height and width of the world so that the number of digits below zero is constant throughout the world space. This fixes potential floating point issues when, for example, 97 is added to 928.0000000000001 giving 1025 since a significant digit was lost when going into the thousands.
*
* @property offsetMap
* @type Boolean
* @default false
*/
offsetMap: false,
/**
* If set to `true` and if the game is running in debug mode, this causes the collision layer to appear.
*
* @property showCollisionTiles
* @type Boolean
* @default false
*/
showCollisionTiles: false,
/**
* If specified, the referenced images are used as the game sprite sheets instead of the images referenced in the Tiled map. This is useful for using different or better quality art from the art used in creating the Tiled map.
*
* @property images
* @type Array
* @default null
*/
images: null,
/**
* Adds a number to each additional Tiled layer's z coordinate to maintain z-order. Defaults to 1000.
*
* @property layerIncrement
* @type number
* @default 1000
*/
layerIncrement: 1000,
/**
* Keeps the tile maps in separate render layers. Default is 'false' to for better optimization.
*
* @property separateTiles
* @type boolean
* @default false
*/
separateTiles: false,
/**
* If a particular sprite sheet should be used that's not defined by the level images themselves. This is useful for making uniquely-themed variations of the same level. This is overridden by `"spriteSheet": "import"` in the "render-layer" Entity definition, so be sure to remove that when setting this property.
*
* @property spriteSheet
* @type String | Object
* @default null
*/
spriteSheet: null,
/**
* Whether to continue loading `lazyLoad` entities in the background after level starts regardless of camera position. If `false`, entities with a `lazyLoad` property will only load once within camera range.
*
* @property backgroundLoad
* @type Boolean
* @default true
*/
backgroundLoad: true
},
publicProperties: {
/**
* Specifies the JSON level to load. Available on the entity as `entity.level`.
*
* @property level
* @type String
* @default null
*/
level: null,
/**
* Specifies a function that entity definitions should be passed through before becoming entities to allow for specific changes apart from the level definition (like stored data or meta game mechanics)
*
* @property entityModifier
* @type Function
* @default defaultEntityModifier (identity function)
*/
entityModifier: defaultEntityModifier,
/**
* Can be "left", "right", or "center". Defines where entities registered X position should be when spawned. Available on the entity as `entity.entityPositionX`.
*
* @property entityPositionX
* @type String
* @default "center"
*/
entityPositionX: "center",
/**
* Can be "top", "bottom", or "center". Defines where entities registered Y position should be when spawned. Available on the entity as `entity.entityPositionY`.
*
* @property entityPositionY
* @type String
* @default "bottom"
*/
entityPositionY: "bottom",
/**
* Whether to wait for a "load-level" event before before loading. Available on the entity as `entity.manuallyLoad`.
*
* @property manuallyLoad
* @type boolean
* @default false
*/
manuallyLoad: false,
tileWidth: null,
tileHeight: null
},
/**
* This component is attached to a top-level entity and, once its peer components are loaded, ingests a JSON file exported from the [Tiled map editor](http://www.mapeditor.org/) and creates the tile maps and entities. Once it has finished loading the map, it removes itself from the list of components on the entity.
*
* This component requires an [EntityContainer](platypus.components.EntityContainer.html) since it calls `entity.addEntity()` on the entity, provided by `EntityContainer`.
*
* This component looks for the following entities, and if not found will load default versions:
{
"render-layer": {
"id": "render-layer",
"components":[{
"type": "RenderTiles",
"spriteSheet": "import",
"imageMap": "import",
"entityCache": true
}]
},
"collision-layer": {
"id": "collision-layer",
"components":[{
"type": "CollisionTiles",
"collisionMap": "import"
}]
},
"image-layer": {
"id": "image-layer",
"components":[{
"type": "RenderTiles",
"spriteSheet": "import",
"imageMap": "import"
}]
}
}
* @memberof platypus.components
* @uses platypus.Component
* @constructs
* @listens platypus.Entity#camera-update
* @listens platypus.Entity#layer-loaded
* @listens platypus.Entity#load-level
* @listens platypus.Entity#tick
* @fires platypus.Entity#world-loaded
* @fires platypus.Entity#level-loading-progress
*/
initialize: function () {
this.assetCache = platypus.assetCache;
this.layerZ = 0;
this.followEntity = false;
this.lazyLoads = arrayCache.setUp();
},
events: {
"layer-loaded": function (persistentData, holds) {
if (!this.manuallyLoad) {
holds.count += 1;
this.loadLevel({
level: this.level || persistentData.level,
persistentData: persistentData
}, holds.release);
}
},
"load-level": function (levelData, callback) {
this.loadLevel(levelData, callback);
}
},
methods: {
createLayer: function (entityKind, rawLayer, offsetX, offsetY, tileWidth, tileHeight, tilesets, tileSetTileData, images, combineRenderLayer, progress, entityLinker) {
const
//This builds in parallaxing support by allowing the addition of width and height properties into Tiled layers so they pan at a separate rate than other layers.
checkParallax = ({data, height, properties, width}) => {
const
layerHeight = getProperty(properties, 'height'),
layerWidth = getProperty(properties, 'width'),
newWidth = layerWidth ? parseInt(layerWidth, 10) : width,
newHeight = layerHeight ? parseInt(layerHeight, 10) : height,
newData = (newWidth !== width || newHeight !== height) ? [] : data;
if (newData.length === 0) {
for (let x = 0; x < newWidth; x++) {
for (let y = 0; y < newHeight; y++) {
if ((x < width) && (y < height)) {
newData[x + y * newWidth] = data[x + y * width];
} else {
newData[x + y * newWidth] = 0;
}
}
}
}
return {
width: newWidth,
height: newHeight,
data: newData
};
},
layer = decodeLayer(rawLayer),
{
offsetx: layerOffsetX = 0,
offsety: layerOffsetY = 0,
properties: layerProperties,
tileheight: tHeight = tileHeight,
tilewidth: tWidth = tileWidth
} = layer,
{
data,
height,
width
} = layerProperties ? checkParallax(layer) : layer,
mapOffsetX = offsetX + layerOffsetX,
mapOffsetY = offsetY + layerOffsetY,
layerDefinition = JSON.parse(JSON.stringify(platypus.game.settings.entities[entityKind] ?? standardEntityLayers[entityKind])), //TODO: a bit of a hack to copy an object instead of overwrite values
layerDefinitionProperties = layerDefinition.properties = layerDefinition.properties ?? {},
{components} = layerDefinition,
importAnimation = {},
importCollision = [],
importFrames = [],
importRender = [],
importSpriteSheet = {
images: (layer.image ? [layer.image] : images),
frames: importFrames,
animations: importAnimation
},
createFrames = function (frames, index, tileset) {
const
{image, tiles} = tileset;
if (image) {
const
{
columns = (((tileset.imagewidth / (tileset.tilewidth + tileset.spacing)) + (tileset.margin * 2 - tileset.spacing)) >> 0),
imageheight,
margin = 0,
spacing = 0,
tileheight,
tilewidth
} = tileset,
tileWidthHalf = tilewidth / 2,
tileHeightHalf = tileheight / 2,
tileWidthSpace = tilewidth + spacing,
tileHeightSpace = tileheight + spacing,
margin2 = margin * 2,
marginSpace = margin2 - spacing,
rows = /* Tiled tileset def doesn't seem to have rows */ (((imageheight / tileHeightSpace) + marginSpace) >> 0);
for (let y = 0; y < rows; y++) {
for (let x = 0; x < columns; x++) {
frames.push([
margin + x * tileWidthSpace,
margin + y * tileHeightSpace,
tilewidth,
tileheight,
index,
tileWidthHalf,
tileHeightHalf
]);
}
}
index += 1;
} else if (tiles) {
for (let i = 0; i < tiles.length; i++) {
const
{imageheight, imagewidth} = tiles[i];
frames.push([
0,
0,
imageheight,
imagewidth,
index,
imageheight / 2,
imagewidth / 2
]);
index += 1;
}
}
return index;
};
let index = 0,
lastSet = null,
renderTiles = false,
collisionTiles = false;
entityLinker.linkObject(layerDefinitionProperties.tiledId = layer.id);
if (layerProperties) {
mergeAndFormatProperties(layerProperties, layerDefinitionProperties, entityLinker);
}
layerDefinitionProperties.width = tWidth * width;
layerDefinitionProperties.height = tHeight * height;
layerDefinitionProperties.columns = width;
layerDefinitionProperties.rows = height;
layerDefinitionProperties.tileWidth = tWidth;
layerDefinitionProperties.tileHeight = tHeight;
layerDefinitionProperties.scaleX = 1;
layerDefinitionProperties.scaleY = 1;
layerDefinitionProperties.layerZ = this.layerZ;
layerDefinitionProperties.left = layerDefinitionProperties.x || mapOffsetX;
layerDefinitionProperties.top = layerDefinitionProperties.y || mapOffsetY;
layerDefinitionProperties.z = layerDefinitionProperties.z || this.layerZ;
if (tilesets.length) {
let imageIndex = 0;
for (let x = 0; x < tilesets.length; x++) {
imageIndex = createFrames(importFrames, imageIndex, tilesets[x]);
}
lastSet = tilesets[tilesets.length - 1];
{
const
tileTypes = lastSet.firstgid + lastSet.tilecount;
for (let x = -1; x < tileTypes; x++) {
importAnimation['tile' + x] = x;
}
}
}
for (let x = 0; x < width; x++) {
importCollision[x] = [];
importRender[x] = [];
for (let y = 0; y < height; y++) {
index = +data[x + y * width] - 1; // -1 from original src to make it zero-based.
importRender[x][y] = 'tile' + index;
index += 1; // So collision map matches original src indexes. Render (above) should probably be changed at some point as well. DDD 3/30/2016
importCollision[x][y] = index;
if (tileSetTileData) {
const
transform = entityTransformCheck(index);
if (tileSetTileData.has(transform.id)) {
const
{objectgroup, properties} = tileSetTileData.get(transform.id);
if (objectgroup && !layerDefinitionProperties.ignoreTileCollision) {
const
offsetX = mapOffsetX + tileWidth * x,
offsetY = mapOffsetY + tileHeight * y;
this.setUpEntities(objectgroup.objects.map((object) => getObjectCentered(object)).map((object) => getObjectTransformed(object, transform, tileWidth, tileHeight)), objectgroup, offsetX, offsetY, tilesets, progress, entityLinker);
}
if (properties && getProperty(properties, 'hide')) {
importRender[x][y] = 'tile-1'; // hide this tile's art.
}
}
}
}
}
for (let x = 0; x < components.length; x++) {
if (components[x].type === 'RenderTiles') {
renderTiles = components[x];
}
if (components[x].type === 'CollisionTiles') {
collisionTiles = components[x];
}
if (components[x].spriteSheet === 'import') {
components[x].spriteSheet = importSpriteSheet;
} else if (components[x].spriteSheet) {
if (typeof components[x].spriteSheet === 'string' && platypus.game.settings.spriteSheets[components[x].spriteSheet]) {
components[x].spriteSheet = platypus.game.settings.spriteSheets[components[x].spriteSheet];
}
if (!components[x].spriteSheet.animations) {
components[x].spriteSheet.animations = importAnimation;
}
}
if (components[x].collisionMap === 'import') {
components[x].collisionMap = [importCollision];
}
if (components[x].imageMap === 'import') {
components[x].imageMap = importRender;
}
}
if ((!this.separateTiles) && (combineRenderLayer?.tileHeight === tHeight) && (combineRenderLayer?.tileWidth === tWidth) && (combineRenderLayer?.columns === width) && (combineRenderLayer?.rows === height)) {
combineRenderLayer.triggerEvent('add-tiles', renderTiles);
combineRenderLayer.triggerEvent('add-collision-tiles', collisionTiles);
this.updateLoadingProgress(progress);
return combineRenderLayer;
} else {
const
properties = {};
if (this.spriteSheet) {
if (typeof this.spriteSheet === 'string') {
properties.spriteSheet = platypus.game.settings.spriteSheets[this.spriteSheet];
} else {
properties.spriteSheet = this.spriteSheet;
}
if (!properties.spriteSheet.animations) {
properties.spriteSheet.animations = importAnimation;
}
}
return entityLinker.linkEntity(this.owner.addEntity(new Entity(layerDefinition, {
properties
}, this.updateLoadingProgress.bind(this, progress), this.owner)));
}
},
convertImageLayer: function (imageLayer) {
const
{image, name, properties} = imageLayer,
namedAsset = this.assetCache.get(name),
imageId = namedAsset ? name : getKey(image),
asset = namedAsset ?? this.assetCache.get(imageId) ?? null,
repeat = getProperty(properties, 'repeat'),
repeatX = getProperty(properties, 'repeat-x'),
repeatY = getProperty(properties, 'repeat-y'),
height = +(repeatY ?? repeat ?? 1),
width = +(repeatX ?? repeat ?? 1),
dataCells = width * height,
tileheight = asset?.height ?? 1,
tilewidth = asset?.width ?? 1,
tileLayer = {
...imageLayer,
data: Array(dataCells).fill(1),
image: asset ? imageId : image,
height,
type: 'tilelayer',
width,
tileheight,
tilewidth,
properties
};
if (!asset) { // Prefer to have name in tiled match image id in game
platypus.debug.warn(`Component TiledLoader: Cannot find the "${name}" sprite sheet. Add it to the list of assets in config.json and give it the id "${name}".`);
}
tileLayer.tileset = {
columns: 1,
image: tileLayer.image,
imageheight: tileheight,
imagewidth: tilewidth,
margin: 0,
name,
spacing: 0,
tilecount: 1,
tileheight,
tilewidth,
type: "tileset"
};
return tileLayer;
},
loadLevel: function (levelData, callback) {
const
{assetCache, offsetMap, owner} = this,
entityLinker = EntityLinker.setUp(),
images = this.images ? greenSlice(this.images) : arrayCache.setUp(),
level = (typeof levelData.level === 'string') ? platypus.game.settings.levels[levelData.level] : levelData.level, //format level appropriately
layers = level.layers,
progress = Data.setUp('count', 0, 'progress', 0, 'total', 0),
tileSetTileData = DataMap.setUp(),
tilesets = importTilesetData(level.tilesets),
tileWidth = this.tileWidth = level.tilewidth,
tileHeight = this.tileHeight = level.tileheight,
height = level.height * tileHeight,
width = level.width * tileWidth,
x = offsetMap ? getPowerOfTen(width) : 0,
y = offsetMap ? getPowerOfTen(height) : 0;
let layer = null;
createTilesetObjectGroupReference(tileSetTileData, tilesets);
if (level.properties) {
entityLinker.linkObject(owner.tiledId = 0); // Level
mergeAndFormatProperties(level.properties, owner, entityLinker);
entityLinker.linkEntity(owner);
}
if (images.length === 0) {
const
addImage = ({image}) => {
const
imageKey = getKey(image),
asset = assetCache.get(imageKey);
if (asset) { // Prefer to have name in tiled match image id in game
images.push(imageKey);
} else {
platypus.debug.warn('Component TiledLoader: Cannot find the "' + imageKey + '" sprite sheet. Add it to the list of assets in config.json and give it the id "' + imageKey + '".');
images.push(image);
}
};
for (let i = 0; i < tilesets.length; i++) {
const
tileset = tilesets[i];
if (tileset.image) {
addImage(tileset);
} else {
tileset.tiles.forEach(addImage);
}
}
}
progress.total = layers.length;
this.finishedLoading = () => {
const
{lazyLoads} = this,
message = Data.setUp(
"level", null,
"world", AABB.setUp(),
"tile", AABB.setUp(),
"camera", null,
lazyLoads
),
lazyLoad = (entity) => {
const
aabb = entity.aabb,
entityLinker = entity.entityLinker;
aabb.recycle();
entity.aabb = null;
entity.entityLinker = null;
entityLinker.linkEntity(owner.addEntity(entity));
};
/**
* Once finished loading the map, this message is triggered on the entity to notify other components of completion.
*
* @event platypus.Entity#world-loaded
* @param message {platypus.Data} World data.
* @param message.level {Object} The Tiled level data used to load the level.
* @param message.width {number} The width of the world in world units.
* @param message.height {number} The height of the world in world units.
* @param message.tile {platypus.AABB} Dimensions of the world tiles.
* @param message.world {platypus.AABB} Dimensions of the world.
* @param message.camera {platypus.Entity} If a camera property is found on one of the loaded entities, this property will point to the entity on load that a world camera should focus on.
* @param message.lazyLoads {Array} List of objects representing entity definitions that will await camera focus before generating actual entities.
*/
message.level = level;
message.camera = this.followEntity; // TODO: in 0.9.0 this should probably be removed, using something like "child-entity-added" instead. Currently this is particular to TiledLoader and Camera and should be generalized. - DDD 3/15/2016
message.width = width;
message.height = height;
message.world.setBounds(x, y, x + width, y + height);
message.tile.setBounds(0, 0, tileWidth, tileHeight);
owner.triggerEvent('world-loaded', message);
message.world.recycle();
message.tile.recycle();
message.recycle();
if (lazyLoads.length) {
this.addEventListener("camera-update", (camera) => {
const
viewport = AABB.setUp(camera.viewport);
let i = lazyLoads.length;
viewport.resize(viewport.width * 1.5, viewport.height * 1.5);
while (i--) {
const entity = lazyLoads[i],
aabb = entity.aabb;
if (viewport.intersects(aabb)) {
lazyLoad(entity);
for (let j = i + 1; j < lazyLoads.length; j++) {
lazyLoads[j - 1] = lazyLoads[j];
}
lazyLoads.length -= 1;
}
}
});
if (this.backgroundLoad) {
this.addEventListener('tick', function () {
const
i = lazyLoads.length;
if (i) {
lazyLoad(lazyLoads.pop());
}
});
}
} else {
owner.removeComponent(this);
}
if (callback) {
callback();
}
};
for (let i = 0; i < layers.length; i++) {
const
layerDefinition = layers[i];
switch (layerDefinition.type) {
case 'imagelayer':
layer = this.convertImageLayer(layerDefinition);
layer = this.createLayer(getProperty(layer.properties, 'entity') || 'image-layer', layer, x, y, layer.tilewidth, layer.tileheight, [layer.tileset], null, images, layer, progress, entityLinker);
break;
case 'objectgroup':
this.setUpEntities(layerDefinition.objects.map((object) => getObjectCentered(object)), layerDefinition, x, y, tilesets, progress, entityLinker);
layer = null;
this.updateLoadingProgress(progress);
break;
case 'tilelayer':
layer = this.setupLayer(layerDefinition, layer, x, y, tileWidth, tileHeight, tilesets, tileSetTileData, images, progress, entityLinker);
break;
default:
platypus.debug.warn('Component TiledLoader: Platypus does not support Tiled layers of type "' + layerDefinition.type + '". This layer will not be loaded.');
this.updateLoadingProgress(progress);
}
this.layerZ += this.layerIncrement;
}
tileSetTileData.recycle();
},
setUpEntities: (function () {
const
tBoth = function (point) {
return Data.setUp('x', -point.x, 'y', -point.y);
},
tNone = function (point) {
return Data.setUp('x', point.x, 'y', point.y);
},
tX = function (point) {
return Data.setUp('x', -point.x, 'y', point.y);
},
tY = function (point) {
return Data.setUp('x', point.x, 'y', -point.y);
},
transformPoints = function (points, transformX, transformY) {
const
arr = arrayCache.setUp(),
reverseCycle = transformX ^ transformY,
transform = transformX ? transformY ? tBoth : tX : transformY ? tY : tNone;
if (reverseCycle) {
let i = points.length;
while (i--) {
arr.push(transform(points[i]));
}
arr.unshift(arr.pop()); // so the same point is at the beginning.
} else {
for (let i = 0; i < points.length; i++) {
arr.push(transform(points[i]));
}
}
return arr;
},
getPolyShape = function (type, points, transformX, transformY, decomposed) {
const
shape = {
type: type,
points: transformPoints(points, transformX, transformY)
};
if (decomposed) {
const decomposedPoints = [];
let p = 0;
for (p = 0; p < decomposed.length; p++) {
decomposedPoints.push(transformPoints(decomposed[p], transformX, transformY));
}
shape.decomposedPolygon = decomposedPoints;
}
return shape;
};
return function (objects, layer, offsetX, offsetY, tilesets, progress, entityLinker) {
const
{offsetx: layerOffsetX = 0, offsety: layerOffsetY = 0, properties: layerProperties} = layer,
mapOffsetX = offsetX + layerOffsetX,
mapOffsetY = offsetY + layerOffsetY,
len = objects.length;
progress.total += len;
objects.forEach((object) => {
const
entityData = getEntityData(object, tilesets, entityLinker);
if (entityData) {
const
entityPositionX = getProperty(layerProperties, 'entityPositionX') ?? this.entityPositionX,
entityPositionY = getProperty(layerProperties, 'entityPositionY') ?? this.entityPositionY,
{flipX = 1, flipY = 1, height, point, polygon, polyline, rotation, width, x, y} = object,
angle = (rotation) / 180 * Math.PI,
tiledLoaderOffset = Vector.setUp(width / 2 * POSITIONS[entityPositionX] * flipX, height / 2 * POSITIONS[entityPositionY] * flipY).rotate(angle),
entityLocation = Vector.setUp(mapOffsetX + x, mapOffsetY + y).addVector(tiledLoaderOffset),
{type: entityType, properties} = entityData,
entityPackage = {
properties
},
entityDefinition = platypus.game.settings.entities[entityType],
entityDefProps = entityDefinition?.properties ?? null,
hh = (point ? (entityDefProps?.height ?? 0) * (1 + POSITIONS[entityPositionY]) : height) / 2,
hw = (point ? (entityDefProps?.width ?? 0) * (1 + POSITIONS[entityPositionX]) : width) / 2,
registrationPoint = Vector.setUp(tiledLoaderOffset).add(hw, hh),
addShape = (shape, properties) => {
if (!properties.shapes) {
properties.shapes = [];
}
// Platypus collision uses a shapes array.
shape.regX = registrationPoint.x;
shape.regY = registrationPoint.y;
properties.shapes.push(shape);
},
addCollisionShapes = ({ellipse, height, width}, properties) => {
if (ellipse) {
if (width === height) {
addShape({
type: 'circle',
radius: width / 2
}, properties);
} else {
addShape({
type: 'ellipse',
width,
height
}, properties);
}
} else if (width && height) {
addShape({
type: 'rectangle',
width,
height,
}, properties);
}
};
entityPackage[entityDefinition ? 'type' : 'id'] = entityType;
if (polygon || polyline) {
//Figuring out the width of the polygon and shifting the origin so it's in the top-left.
const
polyPoints = polygon ?? polyline;
let smallestX = Infinity,
largestX = -Infinity,
smallestY = Infinity,
largestY = -Infinity;
for (let x = 0; x < polyPoints.length; x++) {
if (polyPoints[x].x > largestX) {
largestX = polyPoints[x].x;
}
if (polyPoints[x].x < smallestX) {
smallestX = polyPoints[x].x;
}
if (polyPoints[x].y > largestY) {
largestY = polyPoints[x].y;
}
if (polyPoints[x].y < smallestY) {
smallestY = polyPoints[x].y;
}
}
properties.width = largestX - smallestX;
properties.height = largestY - smallestY;
properties.x = entityLocation.x;
properties.y = entityLocation.y;
if (polygon) {
addShape(getPolyShape('polygon', polyPoints, flipX === -1, flipY === -1, properties.decomposedPolygon), properties);
} else if (polyline) {
addShape(getPolyShape('polyline', polyPoints, flipX === -1, flipY === -1, null), properties);
}
if (rotation) {
properties.rotation = rotation;
}
} else {
if (!point) {
properties.width = width;
properties.height = height;
}
properties.x = entityLocation.x;
properties.y = entityLocation.y;
properties.rotation = object.rotation;
properties.regX = registrationPoint.x;
properties.regY = registrationPoint.y;
addCollisionShapes(object, properties);
}
if (entityDefProps) {
properties.scaleX *= (entityDefProps.scaleX || 1);
properties.scaleY *= (entityDefProps.scaleY || 1);
}
properties.scaleX *= flipX;
properties.scaleY *= flipY;
properties.layerZ = this.layerZ;
//Setting the z value. All values are getting added to the layerZ value.
if (properties.z) {
properties.z += this.layerZ;
} else if (entityDefProps && (typeof entityDefProps.z === 'number')) {
properties.z = this.layerZ + entityDefProps.z;
} else {
properties.z = this.layerZ;
}
{
const
proofedPackage = this.entityModifier(entityPackage);
if (proofedPackage) {
if (properties.lazyLoad || (entityDefProps && entityDefProps.lazyLoad)) {
const
{height: h = 1, regX = 0, regY = 0, width: w = 1, x = 0, y = 0} = properties;
proofedPackage.aabb = AABB.setUp(x + w / 2 - regX, y + h / 2 - regY, w, h);
proofedPackage.entityLinker = entityLinker;
this.lazyLoads.push(proofedPackage);
this.updateLoadingProgress(progress);
} else {
const
createdEntity = this.owner.addEntity(proofedPackage, this.updateLoadingProgress.bind(this, progress));
entityLinker.linkEntity(createdEntity);
if (createdEntity && createdEntity.camera) {
this.followEntity = {
entity: createdEntity,
mode: createdEntity.camera
}; //used by camera
}
}
} else {
this.updateLoadingProgress(progress);
}
}
} else {
this.updateLoadingProgress(progress);
}
});
};
}()),
setupLayer: function (layer, combineRenderLayer, mapOffsetX, mapOffsetY, tileWidth, tileHeight, tilesets, tileSetTileData, images, progress, entityLinker) {
const
entity = getProperty(layer.properties, 'entity') ?? 'render-layer', // default
entityDefinition = platypus.game.settings.entities[entity] ?? standardEntityLayers[entity];
let canCombine = false;
// Need to check whether the entity can be combined for optimization. This combining of tile layers might be a nice addition to the compilation tools so it's not happening here.
if (entityDefinition) {
let i = entityDefinition.components.length;
while (i--) {
if (entityDefinition.components[i].type === "RenderTiles") {
canCombine = true;
break;
}
}
}
if (canCombine) {
return this.createLayer(entity, layer, mapOffsetX, mapOffsetY, tileWidth, tileHeight, tilesets, tileSetTileData, images, combineRenderLayer, progress, entityLinker);
} else {
this.createLayer(entity, layer, mapOffsetX, mapOffsetY, tileWidth, tileHeight, tilesets, tileSetTileData, images, combineRenderLayer, progress, entityLinker);
return null;
}
},
updateLoadingProgress: function (progress) {
progress.count += 1;
progress.progress = progress.count / progress.total;
/**
* As a level is loaded, this event is triggered to show progress.
*
* @event platypus.Entity#level-loading-progress
* @param message {platypus.Data} Contains progress data.
* @param message.count {Number} The number of loaded entities.
* @param message.progress {Number} A fraction of count / total.
* @param message.total {Number} The total number of entities being loaded by this component.
*/
this.owner.triggerEvent('level-loading-progress', progress);
if (progress.count === progress.total) {
progress.recycle();
this.finishedLoading();
}
},
destroy: function () {
arrayCache.recycle(this.lazyLoads);
this.lazyLoads = null;
}
},
getAssetList: function (definition, props, defaultProps, data) {
const
ss = definition?.spriteSheet ?? props?.spriteSheet ?? defaultProps?.spriteSheet,
images = definition?.images ?? props?.images ?? defaultProps?.images,
assets = checkLevel(data?.level ?? definition?.level ?? props?.level ?? defaultProps?.level, ss);
if (ss) {
if (typeof ss === 'string') {
union(assets, platypus.game.settings.spriteSheets[ss].images);
} else {
union(assets, ss.images);
}
}
if (images) {
union(assets, images);
}
return assets;
}
});