littlejsengine 1.23.0 → 1.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -54,7 +54,11 @@ function tweakDivider(){}
54
54
  function tweakEngineDefaults(){}
55
55
  class LevelEditor
56
56
  {
57
- constructor() { this.paletteTiles = this.use3D = this.tool = undefined; }
57
+ constructor()
58
+ {
59
+ this.paletteTiles = this.use3D = this.tool = undefined;
60
+ this.keys = {}, this.buttons = [], this.tools = {};
61
+ }
58
62
  get isOpen() { return false; }
59
63
  get is3D() { return false; }
60
64
  get edit2D() { return undefined; }
@@ -128,6 +128,10 @@ const tileLayersTiledFlips = [[0,0], [3,1], [2,1], [3,0], [0,1], [1,0], [2,0], [
128
128
  * Load tile layers from exported data
129
129
  * - Tiled maps come in as they are, flipped and turned tiles included, from one tileset image (a second tileset's
130
130
  * tiles continue its numbering), finite maps in the CSV or array layer format; layer offsets and parallax are not read
131
+ * - A tileset kept in the map with a margin or a spacing, a sheet with gaps between its tiles, is read where its
132
+ * tiles are, whatever padding the tile info has; one in a file of its own (a tsx) is not read, pass a tile info
133
+ * with its padding
134
+ * - An LDtk level loads through tileLayersFromLDtk, which makes it a map like these
131
135
  * - Group layers are flattened in order, each replaced by the layers inside it, so the layer indices
132
136
  * (collisionLayer and the returned array) count that flattened list; a group's tint, opacity and
133
137
  * visibility carry to the layers inside it
@@ -160,6 +164,18 @@ function tileLayersLoad(tileMapData, tileInfo=tileLayerDefaultTile(), renderOrde
160
164
  ASSERT(tileMapData.width && tileMapData.height);
161
165
  ASSERT(tileMapData.layers && tileMapData.layers.length);
162
166
 
167
+ // a sheet with a margin around its tiles or a spacing between them: the first tile is at the margin and each
168
+ // cell is a tile and a spacing, which is a padding of half the spacing counted from there, by the tileset's
169
+ // own count of columns, since the image's width does not say with a spacing that is not all around
170
+ const tileset = tileMapData.tilesets?.[0];
171
+ if (tileInfo && tileset && (tileset.margin > 0 || tileset.spacing > 0))
172
+ {
173
+ const margin = tileset.margin || 0, spacing = tileset.spacing || 0, size = tileInfo.size;
174
+ const width = tileInfo.textureInfo?.size.x || 0;
175
+ const columns = tileset.columns || max(1, floor((width - margin*2 + spacing) / (size.x + spacing)));
176
+ tileInfo = new TileInfo(vec2(margin), size, tileInfo.textureInfo, spacing / 2, tileInfo.bleed, columns);
177
+ }
178
+
163
179
  // flatten group layers in order, a group's color and visibility carry to the layers inside it
164
180
  /** @type {Array<{dataLayer: Object, color?: Color, visible?: boolean}>} */
165
181
  const layers = [];
@@ -237,6 +253,107 @@ function tileLayersLoad(tileMapData, tileInfo=tileLayerDefaultTile(), renderOrde
237
253
  function tileLayersColor(hex)
238
254
  { return new Color().setHex(hex.length === 9 ? '#' + hex.slice(3) + hex.slice(1, 3) : hex); }
239
255
 
256
+ // the Tiled property type of an LDtk field type, for the ones Tiled has a property for
257
+ const tileLayersLDtkTypes = {Int: 'int', Float: 'float', Bool: 'bool', String: 'string', Multilines: 'string',
258
+ Color: 'color', FilePath: 'file'};
259
+
260
+ /**
261
+ * Make a Tiled map of a level of an LDtk project, to load with tileLayersLoad and objectLayersLoad
262
+ * - Each Tiles, AutoLayer and IntGrid layer is a tile layer, the bottom one first as in Tiled, so the last layer
263
+ * of the LDtk file is layer 0; where LDtk stacks tiles in a cell, an edge over a fill, each tile over another
264
+ * goes in a layer of its own just above, named with (2), (3) and so on, so a layer of the first tiles has one
265
+ * wherever the LDtk layer has any, which makes it the one for collision
266
+ * - An IntGrid layer with no tiles is a hidden layer of its values, for collision: pass its index as collisionLayer
267
+ * - An Entities layer is an object layer: an entity's name is its type for objectLayersAddType, it is placed at
268
+ * its middle, and its Int, Float, Bool, String, Color and FilePath fields are its properties (an enum is a string)
269
+ * - The tileset is the first tile layer's, with its padding and spacing; give tileLayersLoad a tile info of its image
270
+ * - The level is in the project file (not saved as separate level files), its layers of one grid size; a layer of
271
+ * another grid size or another tileset is left out, with a warning in debug builds
272
+ * - The level editor edits the map this returns, and saves it as a Tiled map
273
+ * @param {Object} ldtk - The LDtk project, its JSON
274
+ * @param {number|string} [level] - Which level, by its index or its identifier
275
+ * @return {Object} - A Tiled map: width, height, tilewidth, tileheight, tilesets and layers
276
+ * @example
277
+ * const map = tileLayersFromLDtk(await fetchJSON('world.ldtk'), 'Level_0');
278
+ * const layers = tileLayersLoad(map, tile(0, 16), 0, 1); // layer 1 is solid
279
+ * objectLayersLoad(map);
280
+ * @memberof TileLayers */
281
+ function tileLayersFromLDtk(ldtk, level=0)
282
+ {
283
+ const levels = ldtk?.levels || [];
284
+ const data = typeof level === 'string' ? levels.find((l)=> l.identifier === level) : levels[level];
285
+ ASSERT(data, 'LDtk level not found', level);
286
+ ASSERT(isArray(data.layerInstances), 'LDtk level has no layers: levels saved as separate files are not read');
287
+ const instances = data.layerInstances;
288
+ const tiled = (l)=> l.__type !== 'Entities';
289
+ const first = instances.find((l)=> tiled(l) && l.__tilesetDefUid != undefined) || instances.find(tiled) || instances[0];
290
+ const grid = first?.__gridSize || 16;
291
+ const width = first?.__cWid || ceil(data.pxWid / grid), height = first?.__cHei || ceil(data.pxHei / grid);
292
+ const map = {width, height, tilewidth: grid, tileheight: grid, orientation: 'orthogonal', renderorder: 'right-down',
293
+ infinite: false, layers: [], nextlayerid: 1, nextobjectid: 1};
294
+
295
+ // the tileset of the first layer that has one, where its tiles are in its image
296
+ const tileset = (ldtk?.defs?.tilesets || []).find((t)=> t.uid === first?.__tilesetDefUid);
297
+ if (tileset)
298
+ map.tilesets = [{firstgid: 1, name: String(tileset.relPath || '').replace(/^.*[\\/]/, '').replace(/\.\w+$/, ''),
299
+ image: tileset.relPath, imagewidth: tileset.pxWid, imageheight: tileset.pxHei,
300
+ tilewidth: tileset.tileGridSize, tileheight: tileset.tileGridSize, margin: tileset.padding || 0,
301
+ spacing: tileset.spacing || 0, columns: tileset.__cWid, tilecount: tileset.__cWid * tileset.__cHei}];
302
+
303
+ // LDtk lists the top layer first, Tiled the bottom one
304
+ for (let i = instances.length; i--;)
305
+ {
306
+ const instance = instances[i], name = instance.__identifier, id = map.nextlayerid++;
307
+ if (!tiled(instance))
308
+ {
309
+ const objects = (instance.entityInstances || []).map((entity)=>
310
+ {
311
+ // an entity is placed by its pivot, an object here by its middle
312
+ const [pivotX=0, pivotY=0] = entity.__pivot || [], w = entity.width || 0, h = entity.height || 0;
313
+ const properties = [];
314
+ for (const field of entity.fieldInstances || [])
315
+ {
316
+ const type = tileLayersLDtkTypes[field.__type] || (/^(Local|Extern)Enum\./.test(field.__type) && 'string');
317
+ type && field.__value != undefined && properties.push({name: field.__identifier, type, value: field.__value});
318
+ }
319
+ return {id: map.nextobjectid++, name: '', type: entity.__identifier, x: entity.px[0] + (.5 - pivotX) * w,
320
+ y: entity.px[1] + (.5 - pivotY) * h, width: w, height: h, rotation: 0, visible: true, properties};
321
+ });
322
+ map.layers.push({id, name, type: 'objectgroup', objects, opacity: 1, visible: true, x: 0, y: 0});
323
+ continue;
324
+ }
325
+ const tiles = [...(instance.autoLayerTiles || []), ...(instance.gridTiles || [])];
326
+ const usable = instance.__gridSize === grid && instance.__cWid === width && instance.__cHei === height &&
327
+ (!tiles.length || instance.__tilesetDefUid === tileset?.uid);
328
+ if (!usable)
329
+ {
330
+ debug && console.warn(`tileLayersFromLDtk: layer ${name} has another grid or tileset, left out`);
331
+ continue;
332
+ }
333
+ const empty = ()=> new Array(width * height).fill(0), stacks = [empty()];
334
+ if (tiles.length)
335
+ {
336
+ // a tile's place in pixels, its tile in the sheet, and its flips: bit 0 across, bit 1 down, which are
337
+ // Tiled's two top bits; LDtk lists them in the order it draws them, so the first in a cell is the
338
+ // bottom one and each after it goes a layer higher
339
+ const depth = new Uint16Array(width * height);
340
+ for (const t of tiles)
341
+ {
342
+ const x = floor(t.px[0] / grid), y = floor(t.px[1] / grid), cell = x + y * width;
343
+ if (x >= 0 && x < width && y >= 0 && y < height)
344
+ (stacks[depth[cell]++] ||= empty())[cell] =
345
+ (t.t + 1 | (t.f & 1 ? 0x80000000 : 0) | (t.f & 2 ? 0x40000000 : 0)) >>> 0;
346
+ }
347
+ }
348
+ else
349
+ (instance.intGridCsv || []).forEach((value, k)=> k < stacks[0].length && (stacks[0][k] = value));
350
+ const visible = instance.__type === 'IntGrid' && !tiles.length ? false : instance.visible !== false;
351
+ stacks.forEach((data, k)=> map.layers.push({id: k ? map.nextlayerid++ : id, name: k ? `${name} (${k + 1})` : name,
352
+ type: 'tilelayer', width, height, data, opacity: instance.__opacity ?? 1, visible, x: 0, y: 0}));
353
+ }
354
+ return map;
355
+ }
356
+
240
357
  ///////////////////////////////////////////////////////////////////////////////
241
358
  // Object layers
242
359
 
@@ -331,7 +448,8 @@ function objectLayersMake(tileMapData, object)
331
448
  const name = object.type || object.class, type = objectLayersTypes.get(name);
332
449
  if (!type)
333
450
  {
334
- debug && console.warn(`objectLayersLoad: no type added for ${name}, skipped`);
451
+ // an object with no type, a shape or a text someone drew in Tiled, is not one a game makes
452
+ debug && name && console.warn(`objectLayersLoad: no type added for ${name}, skipped`);
335
453
  return;
336
454
  }
337
455
  const {height=0, tilewidth=1, tileheight=1} = tileMapData;