littlejsengine 1.24.0 → 1.24.1

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.
package/dist/littlejs.js CHANGED
@@ -35,7 +35,7 @@ const engineName = 'LittleJS';
35
35
  * @type {string}
36
36
  * @default
37
37
  * @memberof Engine */
38
- const engineVersion = '1.24.0';
38
+ const engineVersion = '1.24.1';
39
39
 
40
40
  /** Frames per second to update
41
41
  * @type {number}
@@ -4084,7 +4084,11 @@ function setGLEnable(enable)
4084
4084
  /** Set how many sided polygons to use when drawing circles and ellipses with WebGL
4085
4085
  * @param {number} sides
4086
4086
  * @memberof Settings */
4087
- function setGLCircleSides(sides) { glCircleSides = sides; }
4087
+ function setGLCircleSides(sides)
4088
+ {
4089
+ ASSERT(isNumber(sides) && sides >= 3, 'circles need at least 3 sides');
4090
+ glCircleSides = sides;
4091
+ }
4088
4092
 
4089
4093
  /** Set default size of tiles in pixels
4090
4094
  * @param {Vector2} size
@@ -9606,7 +9610,7 @@ const tileLayersTiledFlips = [[0,0], [3,1], [2,1], [3,0], [0,1], [1,0], [2,0], [
9606
9610
  * @param {Object} tileMapData - Level data from exported data
9607
9611
  * @param {TileInfo} [tileInfo] - Default tile info (used for size and texture), tile() by default, none when no image is loaded
9608
9612
  * @param {number} [renderOrder] - Render order of the top layer
9609
- * @param {number} [collisionLayer] - Layer to use for collision if any
9613
+ * @param {number|string} [collisionLayer] - Layer to use for collision if any, by its index or its name
9610
9614
  * @param {boolean} [draw] - Should the layer be drawn automatically
9611
9615
  * @return {Array<TileCollisionLayer>}
9612
9616
  * @memberof TileLayers */
@@ -9638,7 +9642,8 @@ function tileLayersLoad(tileMapData, tileInfo=tileLayerDefaultTile(), renderOrde
9638
9642
  const margin = tileset.margin || 0, spacing = tileset.spacing || 0, size = tileInfo.size;
9639
9643
  const width = tileInfo.textureInfo?.size.x || 0;
9640
9644
  const columns = tileset.columns || max(1, floor((width - margin*2 + spacing) / (size.x + spacing)));
9641
- tileInfo = new TileInfo(vec2(margin), size, tileInfo.textureInfo, spacing / 2, tileInfo.bleed, columns);
9645
+ const origin = tileInfo.pos.subtract(vec2(tileInfo.padding)); // where the sheet starts, in an atlas too
9646
+ tileInfo = new TileInfo(origin.add(vec2(margin)), size, tileInfo.textureInfo, spacing / 2, tileInfo.bleed, columns);
9642
9647
  }
9643
9648
 
9644
9649
  // flatten group layers in order, a group's color and visibility carry to the layers inside it
@@ -9669,6 +9674,14 @@ function tileLayersLoad(tileMapData, tileInfo=tileLayerDefaultTile(), renderOrde
9669
9674
  };
9670
9675
  addLayers(tileMapData.layers, WHITE, true);
9671
9676
 
9677
+ // a layer named for collision is the first tile layer of that name, which stays the same when layers are added
9678
+ if (typeof collisionLayer === 'string')
9679
+ {
9680
+ const name = collisionLayer;
9681
+ collisionLayer = layers.findIndex((l)=> l.color && l.dataLayer.name === name);
9682
+ ASSERT(collisionLayer >= 0, 'no tile layer named ' + name);
9683
+ }
9684
+
9672
9685
  // create tile layers and fill with data
9673
9686
  const tileLayers = [];
9674
9687
  const levelSize = vec2(tileMapData.width, tileMapData.height);
@@ -9724,31 +9737,38 @@ const tileLayersLDtkTypes = {Int: 'int', Float: 'float', Bool: 'bool', String: '
9724
9737
 
9725
9738
  /**
9726
9739
  * Make a Tiled map of a level of an LDtk project, to load with tileLayersLoad and objectLayersLoad
9727
- * - Each Tiles, AutoLayer and IntGrid layer is a tile layer, the bottom one first as in Tiled, so the last layer
9728
- * of the LDtk file is layer 0; where LDtk stacks tiles in a cell, an edge over a fill, each tile over another
9729
- * 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
9730
- * wherever the LDtk layer has any, which makes it the one for collision
9731
- * - An IntGrid layer with no tiles is a hidden layer of its values, for collision: pass its index as collisionLayer
9740
+ * - Each Tiles, AutoLayer and IntGrid layer is a tile layer, the bottom one first as in Tiled; where LDtk stacks
9741
+ * tiles in a cell, an edge over a fill, each tile over another goes in a layer of its own just above, named with
9742
+ * (2), (3) and so on, and a tile LDtk draws see-through goes in one of its own with that opacity, times the
9743
+ * layer's, named with it, like Ground .25
9744
+ * - An IntGrid layer is a hidden layer of its values under its own name, for collision, whatever tiles its rules
9745
+ * make, which are layers over it named with tiles, like Collisions tiles; pass the name as collisionLayer, since
9746
+ * the stacked and see-through layers change the indices from level to level:
9747
+ * tileLayersLoad(map, tile(0, 16), 0, 'Collisions')
9732
9748
  * - An Entities layer is an object layer: an entity's name is its type for objectLayersAddType, it is placed at
9733
9749
  * its middle, and its Int, Float, Bool, String, Color and FilePath fields are its properties (an enum is a string)
9734
9750
  * - The tileset is the first tile layer's, with its padding and spacing; give tileLayersLoad a tile info of its image
9735
9751
  * - The level is in the project file (not saved as separate level files), its layers of one grid size; a layer of
9736
- * another grid size or another tileset is left out, with a warning in debug builds
9752
+ * another grid size or another tileset is left out, with a warning in debug builds, and a project with no tileset
9753
+ * keeps its tiles for the tile info the game gives; a project of several worlds has its levels counted in order
9754
+ * - Layer offsets are not read
9737
9755
  * - The level editor edits the map this returns, and saves it as a Tiled map
9738
9756
  * @param {Object} ldtk - The LDtk project, its JSON
9739
9757
  * @param {number|string} [level] - Which level, by its index or its identifier
9740
9758
  * @return {Object} - A Tiled map: width, height, tilewidth, tileheight, tilesets and layers
9741
9759
  * @example
9742
9760
  * const map = tileLayersFromLDtk(await fetchJSON('world.ldtk'), 'Level_0');
9743
- * const layers = tileLayersLoad(map, tile(0, 16), 0, 1); // layer 1 is solid
9761
+ * const layers = tileLayersLoad(map, tile(0, 16), 0, 'Collisions'); // its IntGrid layer is solid
9744
9762
  * objectLayersLoad(map);
9745
9763
  * @memberof TileLayers */
9746
9764
  function tileLayersFromLDtk(ldtk, level=0)
9747
9765
  {
9748
- const levels = ldtk?.levels || [];
9766
+ const levels = ldtk?.levels?.length ? ldtk.levels : (ldtk?.worlds || []).flatMap((w)=> w.levels || []);
9749
9767
  const data = typeof level === 'string' ? levels.find((l)=> l.identifier === level) : levels[level];
9750
- ASSERT(data, 'LDtk level not found', level);
9751
- ASSERT(isArray(data.layerInstances), 'LDtk level has no layers: levels saved as separate files are not read');
9768
+ if (!data)
9769
+ throw new Error('tileLayersFromLDtk: no level ' + level);
9770
+ if (!isArray(data.layerInstances))
9771
+ throw new Error('tileLayersFromLDtk: level ' + data.identifier + ' has no layers, levels saved as separate files are not read');
9752
9772
  const instances = data.layerInstances;
9753
9773
  const tiled = (l)=> l.__type !== 'Entities';
9754
9774
  const first = instances.find((l)=> tiled(l) && l.__tilesetDefUid != undefined) || instances.find(tiled) || instances[0];
@@ -9788,33 +9808,53 @@ function tileLayersFromLDtk(ldtk, level=0)
9788
9808
  continue;
9789
9809
  }
9790
9810
  const tiles = [...(instance.autoLayerTiles || []), ...(instance.gridTiles || [])];
9791
- const usable = instance.__gridSize === grid && instance.__cWid === width && instance.__cHei === height &&
9792
- (!tiles.length || instance.__tilesetDefUid === tileset?.uid);
9793
- if (!usable)
9811
+ const ownTiles = !tiles.length || !tileset ||
9812
+ instance.__tilesetDefUid === tileset.uid && tileset.tileGridSize === grid;
9813
+ if (instance.__gridSize !== grid || instance.__cWid !== width || instance.__cHei !== height || !ownTiles)
9794
9814
  {
9795
9815
  debug && console.warn(`tileLayersFromLDtk: layer ${name} has another grid or tileset, left out`);
9796
9816
  continue;
9797
9817
  }
9798
- const empty = ()=> new Array(width * height).fill(0), stacks = [empty()];
9799
- if (tiles.length)
9818
+ // an IntGrid layer's values, hidden, under its own name, for collision whatever its tiles do
9819
+ const empty = ()=> new Array(width * height).fill(0), opacity = instance.__opacity ?? 1;
9820
+ const values = instance.__type === 'IntGrid';
9821
+ if (values)
9822
+ {
9823
+ const data = empty();
9824
+ (instance.intGridCsv || []).forEach((value, k)=> k < data.length && (data[k] = value));
9825
+ map.layers.push({id, name, type: 'tilelayer', width, height, data, opacity, visible: false, x: 0, y: 0});
9826
+ if (!tiles.length) continue;
9827
+ }
9828
+
9829
+ // a layer for each depth in a cell and each opacity, the first tiles drawn solid first
9830
+ const tilesName = values ? name + ' tiles' : name;
9831
+ const stacks = [{depth: 0, alpha: 1, data: empty()}], stack = (depth, alpha)=>
9832
+ {
9833
+ let s = stacks.find((s)=> s.depth === depth && s.alpha === alpha);
9834
+ s || stacks.push(s = {depth, alpha, data: empty()});
9835
+ return s.data;
9836
+ };
9800
9837
  {
9801
- // a tile's place in pixels, its tile in the sheet, and its flips: bit 0 across, bit 1 down, which are
9802
- // Tiled's two top bits; LDtk lists them in the order it draws them, so the first in a cell is the
9803
- // bottom one and each after it goes a layer higher
9838
+ // a tile's place in pixels, its tile in the sheet, its flips, bit 0 across and bit 1 down, which are
9839
+ // Tiled's two top bits, and its opacity; LDtk lists them in the order it draws them, so the first in a
9840
+ // cell is the bottom one and each after it goes a layer higher
9804
9841
  const depth = new Uint16Array(width * height);
9805
9842
  for (const t of tiles)
9806
9843
  {
9807
9844
  const x = floor(t.px[0] / grid), y = floor(t.px[1] / grid), cell = x + y * width;
9808
9845
  if (x >= 0 && x < width && y >= 0 && y < height)
9809
- (stacks[depth[cell]++] ||= empty())[cell] =
9846
+ stack(depth[cell]++, round(clamp(t.a ?? 1) * 1e3) / 1e3)[cell] =
9810
9847
  (t.t + 1 | (t.f & 1 ? 0x80000000 : 0) | (t.f & 2 ? 0x40000000 : 0)) >>> 0;
9811
9848
  }
9812
9849
  }
9813
- else
9814
- (instance.intGridCsv || []).forEach((value, k)=> k < stacks[0].length && (stacks[0][k] = value));
9815
- const visible = instance.__type === 'IntGrid' && !tiles.length ? false : instance.visible !== false;
9816
- stacks.forEach((data, k)=> map.layers.push({id: k ? map.nextlayerid++ : id, name: k ? `${name} (${k + 1})` : name,
9817
- type: 'tilelayer', width, height, data, opacity: instance.__opacity ?? 1, visible, x: 0, y: 0}));
9850
+ const visible = instance.visible !== false;
9851
+ stacks.sort((a, b)=> a.depth - b.depth || b.alpha - a.alpha);
9852
+ for (const {depth, alpha, data} of stacks)
9853
+ {
9854
+ const layerName = tilesName + (depth ? ` (${depth + 1})` : '') + (alpha < 1 ? ' ' + String(alpha).replace(/^0/, '') : '');
9855
+ map.layers.push({id: values || depth || alpha < 1 ? map.nextlayerid++ : id, name: layerName, type: 'tilelayer',
9856
+ width, height, data, opacity: opacity * alpha, visible, x: 0, y: 0});
9857
+ }
9818
9858
  }
9819
9859
  return map;
9820
9860
  }
@@ -17375,7 +17415,7 @@ class Box2dObject extends EngineObject
17375
17415
 
17376
17416
  const points = [];
17377
17417
  const radius = diameter/2;
17378
- for (let i=sides; i--;)
17418
+ for (let i=sides; i-- > 0;) // a count that is not a whole number still ends
17379
17419
  points.push(vec2(radius,0).rotate((i+.5)/sides*PI*2));
17380
17420
  return this.addPoly(points, density, friction, restitution, isSensor);
17381
17421
  }
@@ -27843,10 +27883,11 @@ class Light3D extends EngineObject3D
27843
27883
  * @param {number} [count] - How many ghosts there are, besides the glow at the light
27844
27884
  * @param {number} [intensity] - How bright it is
27845
27885
  * @param {number} [saturation] - How colorful the ghosts are
27886
+ * @param {Color} [color] - Tints the flare, with the light's own color
27846
27887
  * @return {LensFlare3D} - The flare, to change: light.addFlare().shapes = ['hex'] */
27847
- addFlare(size, count, intensity, saturation)
27888
+ addFlare(size, count, intensity, saturation, color)
27848
27889
  {
27849
- const flare = new LensFlare3D(size, count, intensity, saturation);
27890
+ const flare = new LensFlare3D(size, count, intensity, saturation, color);
27850
27891
  this.flare = flare;
27851
27892
  return flare;
27852
27893
  }
@@ -29494,7 +29535,7 @@ function render3DFlareTile(shape)
29494
29535
  * with the sun's own color; shapes says what its ghosts are, glowSize and ghostSize how big its parts are; or
29495
29536
  * give it elements of your own, which may be tiles of the game's
29496
29537
  * - visible is how much of the sun shows, 0 to 1, eased over fadeTime, there for a game to read; it is 0 while the
29497
- * sun is off the screen, where nothing is tested
29538
+ * sun is too far off the screen for the flare to show, a third of the screen past its edge, where nothing is tested
29498
29539
  * - What hides the sun is found with a ray from the camera, against the level and every object that is not see
29499
29540
  * through, each on the triangles of its mesh, see render3D.pick; turn it off with occlusion
29500
29541
  * - It needs WebGL, and it draws nothing in the shadow of renderAfter2D
@@ -29541,7 +29582,8 @@ class LensFlare3D extends EngineObject3D
29541
29582
  this.elements = undefined;
29542
29583
  /** @property {Light3D|undefined} - A light the flare is of in place of the sun, a lamp or a spotlight: the
29543
29584
  * flare is at the light and in its color, smaller from farther than the light reaches, hidden by what is
29544
- * in front of the light, and a spotlight's shows from inside its beam only; a DirectionalLight3D's is far
29585
+ * in front of the light but its lamp, a mesh around it no wider than half its radius, and a spotlight's
29586
+ * shows from inside its beam only; a DirectionalLight3D's is far
29545
29587
  * away where it shines from, like the sun's; the flare is destroyed when its light is; light.addFlare
29546
29588
  * sets this
29547
29589
  * @type {Light3D|undefined} */
@@ -29681,13 +29723,15 @@ class LensFlare3D extends EngineObject3D
29681
29723
  if (map.raycast(ray, reach, (type)=> !map.blockType(type).seeThrough))
29682
29724
  return true;
29683
29725
  // a light's lamp, the mesh the light is inside, does not hide it: one whose box, in its own space, the light
29684
- // is in
29726
+ // is in, and no wider than half the light's reach, since a room or a whole level has a box around its lights
29727
+ // too, and its walls do hide them from outside
29685
29728
  const lamp = (o)=>
29686
29729
  {
29687
29730
  if (!this.light || reach === Infinity || !o.mesh || o instanceof HeightMap) return false;
29688
- const matrix = render3DObjectMatrix(o);
29689
- if (!matrix.determinant()) return false;
29690
- const p = matrix.copy().invert().transformPoint(source.pos), b = o.mesh.bounds || o.mesh.getBounds();
29731
+ const matrix = render3DObjectMatrix(o), b = o.mesh.bounds || o.mesh.getBounds();
29732
+ const side = max(b.max.x - b.min.x, b.max.y - b.min.y, b.max.z - b.min.z) * render3DMaxStretch(matrix.m);
29733
+ if (!matrix.determinant() || side > this.light.radius / 2) return false;
29734
+ const p = matrix.copy().invert().transformPoint(source.pos);
29691
29735
  return p.x >= b.min.x && p.x <= b.max.x && p.y >= b.min.y && p.y <= b.max.y &&
29692
29736
  p.z >= b.min.z && p.z <= b.max.z;
29693
29737
  };
@@ -30324,7 +30368,7 @@ class VoxelMap extends EngineObject3D
30324
30368
  /** @type {Array<Vector3>} */
30325
30369
  this.chunkCenters = []; // a chunk mesh's points are around its center, which it is drawn at
30326
30370
  this.chunksChanged = new Set;
30327
- /** @type {Array<{faces: Array<number>, seeThrough: boolean, transparent: boolean}|undefined>} */
30371
+ /** @type {Array<{faces: Array<number>, seeThrough: boolean, transparent: boolean, doubleSided: boolean}|undefined>} */
30328
30372
  this.blockTypes = [];
30329
30373
  this.tiles = new Map; // tile index to the uvs of its corners
30330
30374
 
@@ -30396,27 +30440,29 @@ class VoxelMap extends EngineObject3D
30396
30440
  * @param {number} type - 1 to 255
30397
30441
  * @param {number|Array<number>|{top?: number, side: number, bottom?: number}} faces - A tile index for every face,
30398
30442
  * six in the order +x, -x, +y, -y, +z, -z, or the side's with the top and bottom's, which default to the side's
30399
- * @param {{seeThrough?: boolean, transparent?: boolean}} [options] - seeThrough for holes in its texture, like
30400
- * leaves, so the blocks beside it keep their faces; transparent to blend, like glass or water, drawn in the
30401
- * transparent stage, and see-through too */
30402
- setBlockType(type, faces, {seeThrough=false, transparent=false}={})
30443
+ * @param {{seeThrough?: boolean, transparent?: boolean, doubleSided?: boolean}} [options] - seeThrough for holes
30444
+ * in its texture, like leaves, so the blocks beside it keep their faces; transparent to blend, like glass or
30445
+ * water, drawn in the transparent stage, and see-through too; doubleSided for faces seen from inside the
30446
+ * block as well, like water, whose surface then shows from under it */
30447
+ setBlockType(type, faces, {seeThrough=false, transparent=false, doubleSided=false}={})
30403
30448
  {
30404
30449
  ASSERT(type >= 1 && type <= 255 && type % 1 === 0, 'a block type is a whole number from 1 to 255', type);
30405
30450
  const f = /** @type {any} */ (faces);
30406
30451
  const list = isNumber(f) ? [f, f, f, f, f, f] : isArray(f) ? f :
30407
30452
  [f.side, f.side, f.top ?? f.side, f.bottom ?? f.side, f.side, f.side];
30408
30453
  ASSERT(list.length === 6 && list.every(isNumber), 'faces is a tile index, six of them, or {top, side, bottom}');
30409
- this.blockTypes[type] = {faces: list, seeThrough: seeThrough || transparent, transparent};
30454
+ this.blockTypes[type] = {faces: list, seeThrough: seeThrough || transparent, transparent, doubleSided};
30410
30455
  this.rebuild();
30411
30456
  }
30412
30457
 
30413
30458
  /** A block type's faces and how it is seen through
30414
30459
  * @param {number} type
30415
- * @return {{faces: Array<number>, seeThrough: boolean, transparent: boolean}}
30460
+ * @return {{faces: Array<number>, seeThrough: boolean, transparent: boolean, doubleSided: boolean}}
30416
30461
  * @ignore */
30417
30462
  blockType(type)
30418
30463
  {
30419
- return this.blockTypes[type] ||= {faces: [type, type, type, type, type, type], seeThrough: false, transparent: false};
30464
+ return this.blockTypes[type] ||= {faces: [type, type, type, type, type, type], seeThrough: false, transparent: false,
30465
+ doubleSided: false};
30420
30466
  }
30421
30467
 
30422
30468
  /** Build every chunk again, after changing data directly or ambientOcclusion */
@@ -30486,6 +30532,11 @@ class VoxelMap extends EngineObject3D
30486
30532
  colors[k] = ao ? RENDER3D_VOXEL_SHADES[shade[i]] : WHITE;
30487
30533
  }
30488
30534
  mesh.addStrip(points, normal, uvs, colors);
30535
+
30536
+ // a double sided block shows the face from inside too, its corners the other way round
30537
+ if (block.doubleSided)
30538
+ mesh.addStrip([points[1], points[0], points[3], points[2]], normal.scale(-1),
30539
+ [uvs[1], uvs[0], uvs[3], uvs[2]], [colors[1], colors[0], colors[3], colors[2]]);
30489
30540
  }
30490
30541
  }
30491
30542
  this.chunkMeshes[index]?.dispose();
@@ -34276,8 +34327,8 @@ function editorMapRestore(map)
34276
34327
  if ((saved.width ?? map.width) === map.width && (saved.height ?? map.height) === map.height &&
34277
34328
  editorSameData(saved.layers, data) && editorSameData(saved.objects ?? [], objects))
34278
34329
  editorDiscardPending(record);
34279
- else if (!sameFile || !editorResizeMap(map, saved.width ?? map.width, saved.height ?? map.height) ||
34280
- !editorCopyData(data, saved.layers))
34330
+ else if (!sameFile || !editorSavedFits(saved, data.length, saved.width ?? map.width, saved.height ?? map.height) ||
34331
+ !editorResizeMap(map, saved.width ?? map.width, saved.height ?? map.height) || !editorCopyData(data, saved.layers))
34281
34332
  {
34282
34333
  record.pending = saved;
34283
34334
  console.warn(`LittleJS editor: ${record.fileName} changed since its autosaved edits, ` +
@@ -34576,6 +34627,11 @@ function editorCopyData(data, saved)
34576
34627
  return true;
34577
34628
  }
34578
34629
 
34630
+ // are an autosave's tiles a layer for each of the map's, each of its own size, so the map is resized only for tiles
34631
+ // that fit it
34632
+ const editorSavedFits = (saved, count, width, height)=> isArray(saved.layers) && saved.layers.length === count &&
34633
+ saved.layers.every((layer)=> isArray(layer) && layer.length === width * height);
34634
+
34579
34635
  // remember a map's tile data and objects, or forget them when they are back to the file
34580
34636
  function editorAutosave(record)
34581
34637
  {
@@ -34604,7 +34660,7 @@ function editorPaintData(record, data, objects, width=record.map.width, height=r
34604
34660
  {
34605
34661
  if (!editorHas('onRestart')) return;
34606
34662
  const before = editorMapSnapshot(record);
34607
- editorSetMapSnapshot(record, {width, height, layers: data, objects});
34663
+ editorSetMapSnapshot(record, {width, height, layers: data, objects}, keepUnknown);
34608
34664
  editorMapChanged(record, before);
34609
34665
  return;
34610
34666
  }
@@ -34721,13 +34777,15 @@ function editorMapSnapshot(record)
34721
34777
  return editorObjectsCopy({width: map.width, height: map.height, layers: editorTileLayerData(map.layers), objects});
34722
34778
  }
34723
34779
 
34724
- // give a map a size, tiles and objects, the game's Restart hook makes its layers again
34725
- function editorSetMapSnapshot(record, snapshot)
34780
+ // give a map a size, tiles and objects, the game's Restart hook makes its layers again; keepUnknown leaves an object
34781
+ // layer past the snapshot's as it is, as applying an autosave from before the file had it does
34782
+ function editorSetMapSnapshot(record, snapshot, keepUnknown=false)
34726
34783
  {
34727
34784
  const map = record.map;
34728
34785
  editorResizeMap(map, snapshot.width, snapshot.height);
34729
34786
  editorCopyData(editorTileLayerData(map.layers), snapshot.layers);
34730
34787
  editorRestoreObjects(map, snapshot);
34788
+ if (!keepUnknown)
34731
34789
  for (const group of editorObjectGroups(map.layers).slice(snapshot.objects.length))
34732
34790
  group.objects = []; // an Objects layer the editor made since
34733
34791
  editorClearSelections();
@@ -35887,9 +35945,13 @@ function editorPaletteTiles(layer)
35887
35945
  if (!image) return [];
35888
35946
  if (layer.palette?.image === image) return layer.palette.tiles;
35889
35947
 
35890
- const tiles = [];
35948
+ // a sheet read by its columns, a tileset with a margin or a spacing, is read with frame(), which asserts the
35949
+ // frame is inside the image, so a row past the image's end is found before its frame is made
35950
+ const tiles = [], base = live.tileInfo, columns = base.columns;
35891
35951
  for (let i = 0; i < 4096; ++i)
35892
35952
  {
35953
+ if (columns && base.pos.y + (i / columns | 0) * (base.size.y + base.padding*2) + base.size.y > image.height)
35954
+ break;
35893
35955
  const t = editorTileInfo(live, i);
35894
35956
  if (t.pos.x + t.size.x > image.width || t.pos.y + t.size.y > image.height) break;
35895
35957
  tiles.push({tile: i, tileInfo: t});
@@ -37178,9 +37240,11 @@ function editor3DLevelLoaded(level)
37178
37240
  level.objects = editor3DCopy(saved.objects);
37179
37241
  for (const name of editor3DLevelPartNames)
37180
37242
  parts[name] ? level[name] = editor3DCopy(parts[name]) : delete level[name];
37243
+ saved.stale && console.warn(`LittleJS editor: the edits brought back to ${record.fileName} are older than ` +
37244
+ 'the last ones, which storage had no room for');
37181
37245
  }
37182
- else if (saved)
37183
- record.pending = saved;
37246
+ else if (saved && isArray(saved.objects))
37247
+ record.pending = saved; // one that is broken is passed by, and the next autosave writes over it
37184
37248
  }
37185
37249
 
37186
37250
  // called by level3DLoad for each object, with what its type made