littlejsengine 1.23.1 → 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.
@@ -84,8 +84,9 @@ function level3DAddMesh(name, mesh, tileInfo, color=WHITE)
84
84
  * are added before its objects are made, one the game added itself keeps its place
85
85
  * - A level may set the scene too, in a scene block beside its objects: sky, three colors for straight up, the
86
86
  * horizon and straight down, ambient, how much of them lights the scene, .5 when not given, sunDirection and
87
- * sunColor, fog, its start and end, fogColor, the horizon color when not given, and shadows; what the block
88
- * leaves out stays as the game set it, and a level with no block changes nothing
87
+ * sunColor, fog, its start and end, fogColor, the horizon color when not given, shadows, and lensFlare, the
88
+ * sun's lens flare; what the block leaves out stays as the game set it, and a level with no block changes
89
+ * nothing
89
90
  * @param {Object} level - The level, the level editor edits this same object
90
91
  * @return {Array<any>} - What each object's type made, a function that made nothing is left out
91
92
  * @memberof Level3D */
@@ -310,8 +311,25 @@ function level3DSceneApply(scene)
310
311
  r.fogColor = color(scene.fogColor) || r.fogColor;
311
312
  if (typeof scene.shadows === 'boolean')
312
313
  r.shadows = scene.shadows;
314
+ if (typeof scene.lensFlare === 'boolean')
315
+ level3DSceneFlare(scene.lensFlare);
313
316
  }
314
317
 
318
+ // the sun's lens flare as a scene sets it: on makes one unless the sun has a flare already, the game's own or an
319
+ // earlier level's, and off takes away the one a scene made, never the game's
320
+ let level3DSunFlare;
321
+ function level3DSceneFlare(on)
322
+ {
323
+ if (!on)
324
+ {
325
+ level3DSunFlare && !level3DSunFlare.destroyed && level3DSunFlare.destroy();
326
+ level3DSunFlare = undefined;
327
+ }
328
+ else if (!level3DSunHasFlare())
329
+ level3DSunFlare = new LensFlare3D;
330
+ }
331
+ const level3DSunHasFlare = ()=> engineObjects.some((o)=> o instanceof LensFlare3D && !o.destroyed && !o.light);
332
+
315
333
  // a vec3 of an array of three numbers, as the file has them, or the fallback
316
334
  function level3DVector(value, fallback)
317
335
  {
@@ -660,7 +678,8 @@ level3DAddType('Cylinder', function(pos, properties)
660
678
  { return level3DMakeShape(pos, properties, level3DCylinderMesh ||= buildCylinder()); },
661
679
  {color: WHITE, tile: -1, solid: true});
662
680
  // a light, a spotlight with a cone: cone is the angle in degrees from its forward out to the edge of its beam, the
663
- // object's rotation aims it, softness is how much of the cone fades, and shadows makes it the one that casts them
681
+ // object's rotation aims it, softness is how much of the cone fades, shadows makes it the one that casts them, and
682
+ // lensFlare gives it a lens flare; not flare, since a property is also set on the object, where flare is the flare
664
683
  level3DAddType('Light', function(pos, properties)
665
684
  {
666
685
  const light = new Light3D(pos, properties.radius, properties.color, properties.intensity);
@@ -668,5 +687,6 @@ level3DAddType('Light', function(pos, properties)
668
687
  light.coneSoftness = properties.softness;
669
688
  if (properties.shadows && render3D)
670
689
  render3D.shadowLight = light;
690
+ properties.lensFlare && light.addFlare();
671
691
  return light;
672
- }, {color: WHITE, radius: 5, intensity: 1, cone: 0, softness: .2, shadows: false});
692
+ }, {color: WHITE, radius: 5, intensity: 1, cone: 0, softness: .2, shadows: false, lensFlare: false});
@@ -89,7 +89,7 @@ class VoxelMap extends EngineObject3D
89
89
  /** @type {Array<Vector3>} */
90
90
  this.chunkCenters = []; // a chunk mesh's points are around its center, which it is drawn at
91
91
  this.chunksChanged = new Set;
92
- /** @type {Array<{faces: Array<number>, seeThrough: boolean, transparent: boolean}|undefined>} */
92
+ /** @type {Array<{faces: Array<number>, seeThrough: boolean, transparent: boolean, doubleSided: boolean}|undefined>} */
93
93
  this.blockTypes = [];
94
94
  this.tiles = new Map; // tile index to the uvs of its corners
95
95
 
@@ -161,27 +161,29 @@ class VoxelMap extends EngineObject3D
161
161
  * @param {number} type - 1 to 255
162
162
  * @param {number|Array<number>|{top?: number, side: number, bottom?: number}} faces - A tile index for every face,
163
163
  * 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
164
- * @param {{seeThrough?: boolean, transparent?: boolean}} [options] - seeThrough for holes in its texture, like
165
- * leaves, so the blocks beside it keep their faces; transparent to blend, like glass or water, drawn in the
166
- * transparent stage, and see-through too */
167
- setBlockType(type, faces, {seeThrough=false, transparent=false}={})
164
+ * @param {{seeThrough?: boolean, transparent?: boolean, doubleSided?: boolean}} [options] - seeThrough for holes
165
+ * in its texture, like leaves, so the blocks beside it keep their faces; transparent to blend, like glass or
166
+ * water, drawn in the transparent stage, and see-through too; doubleSided for faces seen from inside the
167
+ * block as well, like water, whose surface then shows from under it */
168
+ setBlockType(type, faces, {seeThrough=false, transparent=false, doubleSided=false}={})
168
169
  {
169
170
  ASSERT(type >= 1 && type <= 255 && type % 1 === 0, 'a block type is a whole number from 1 to 255', type);
170
171
  const f = /** @type {any} */ (faces);
171
172
  const list = isNumber(f) ? [f, f, f, f, f, f] : isArray(f) ? f :
172
173
  [f.side, f.side, f.top ?? f.side, f.bottom ?? f.side, f.side, f.side];
173
174
  ASSERT(list.length === 6 && list.every(isNumber), 'faces is a tile index, six of them, or {top, side, bottom}');
174
- this.blockTypes[type] = {faces: list, seeThrough: seeThrough || transparent, transparent};
175
+ this.blockTypes[type] = {faces: list, seeThrough: seeThrough || transparent, transparent, doubleSided};
175
176
  this.rebuild();
176
177
  }
177
178
 
178
179
  /** A block type's faces and how it is seen through
179
180
  * @param {number} type
180
- * @return {{faces: Array<number>, seeThrough: boolean, transparent: boolean}}
181
+ * @return {{faces: Array<number>, seeThrough: boolean, transparent: boolean, doubleSided: boolean}}
181
182
  * @ignore */
182
183
  blockType(type)
183
184
  {
184
- return this.blockTypes[type] ||= {faces: [type, type, type, type, type, type], seeThrough: false, transparent: false};
185
+ return this.blockTypes[type] ||= {faces: [type, type, type, type, type, type], seeThrough: false, transparent: false,
186
+ doubleSided: false};
185
187
  }
186
188
 
187
189
  /** Build every chunk again, after changing data directly or ambientOcclusion */
@@ -251,6 +253,11 @@ class VoxelMap extends EngineObject3D
251
253
  colors[k] = ao ? RENDER3D_VOXEL_SHADES[shade[i]] : WHITE;
252
254
  }
253
255
  mesh.addStrip(points, normal, uvs, colors);
256
+
257
+ // a double sided block shows the face from inside too, its corners the other way round
258
+ if (block.doubleSided)
259
+ mesh.addStrip([points[1], points[0], points[3], points[2]], normal.scale(-1),
260
+ [uvs[1], uvs[0], uvs[3], uvs[2]], [colors[1], colors[0], colors[3], colors[2]]);
254
261
  }
255
262
  }
256
263
  this.chunkMeshes[index]?.dispose();
package/src/engine.js CHANGED
@@ -32,7 +32,7 @@ const engineName = 'LittleJS';
32
32
  * @type {string}
33
33
  * @default
34
34
  * @memberof Engine */
35
- const engineVersion = '1.23.1';
35
+ const engineVersion = '1.24.1';
36
36
 
37
37
  /** Frames per second to update
38
38
  * @type {number}
@@ -7,7 +7,7 @@
7
7
  * - 2D spatial audio based on camera position with distance-based falloff
8
8
  * - Sound instance management (pause, resume, stop)
9
9
  * - Speech synthesis for text-to-speech
10
- * - Music playback with ZzFXM support
10
+ * - Music playback from audio files, looping by default
11
11
  * - Web Audio API integration with master gain control
12
12
  * - Sounds and the master bus can route through effects, see the audio effects plugin
13
13
  * @namespace Audio
@@ -887,7 +887,7 @@ function playAudioBuffer(buffer, volume=1, rate=1, pan=0, loop=false, gainNode,
887
887
  }
888
888
 
889
889
  ///////////////////////////////////////////////////////////////////////////////
890
- // ZzFXMicro - Zuper Zmall Zound Zynth - v1.4.0 by Frank Force
890
+ // ZzFX - Zuper Zmall Zound Zynth - v1.4.0 by Frank Force
891
891
 
892
892
  /** Generate and play a ZzFX sound
893
893
  *
@@ -44,7 +44,6 @@ const enginePluginFiles =
44
44
  `${PLUGIN_FOLDER}/newgrounds.js`,
45
45
  `${PLUGIN_FOLDER}/postProcess.js`,
46
46
  `${PLUGIN_FOLDER}/lightSystem.js`,
47
- `${PLUGIN_FOLDER}/zzfxm.js`,
48
47
  `${PLUGIN_FOLDER}/audioEffects.js`,
49
48
  `${PLUGIN_FOLDER}/uiSystem.js`,
50
49
  `${PLUGIN_FOLDER}/box2d.js`,
@@ -570,8 +570,8 @@ function editorMapRestore(map)
570
570
  if ((saved.width ?? map.width) === map.width && (saved.height ?? map.height) === map.height &&
571
571
  editorSameData(saved.layers, data) && editorSameData(saved.objects ?? [], objects))
572
572
  editorDiscardPending(record);
573
- else if (!sameFile || !editorResizeMap(map, saved.width ?? map.width, saved.height ?? map.height) ||
574
- !editorCopyData(data, saved.layers))
573
+ else if (!sameFile || !editorSavedFits(saved, data.length, saved.width ?? map.width, saved.height ?? map.height) ||
574
+ !editorResizeMap(map, saved.width ?? map.width, saved.height ?? map.height) || !editorCopyData(data, saved.layers))
575
575
  {
576
576
  record.pending = saved;
577
577
  console.warn(`LittleJS editor: ${record.fileName} changed since its autosaved edits, ` +
@@ -870,6 +870,11 @@ function editorCopyData(data, saved)
870
870
  return true;
871
871
  }
872
872
 
873
+ // 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
874
+ // that fit it
875
+ const editorSavedFits = (saved, count, width, height)=> isArray(saved.layers) && saved.layers.length === count &&
876
+ saved.layers.every((layer)=> isArray(layer) && layer.length === width * height);
877
+
873
878
  // remember a map's tile data and objects, or forget them when they are back to the file
874
879
  function editorAutosave(record)
875
880
  {
@@ -898,7 +903,7 @@ function editorPaintData(record, data, objects, width=record.map.width, height=r
898
903
  {
899
904
  if (!editorHas('onRestart')) return;
900
905
  const before = editorMapSnapshot(record);
901
- editorSetMapSnapshot(record, {width, height, layers: data, objects});
906
+ editorSetMapSnapshot(record, {width, height, layers: data, objects}, keepUnknown);
902
907
  editorMapChanged(record, before);
903
908
  return;
904
909
  }
@@ -1015,13 +1020,15 @@ function editorMapSnapshot(record)
1015
1020
  return editorObjectsCopy({width: map.width, height: map.height, layers: editorTileLayerData(map.layers), objects});
1016
1021
  }
1017
1022
 
1018
- // give a map a size, tiles and objects, the game's Restart hook makes its layers again
1019
- function editorSetMapSnapshot(record, snapshot)
1023
+ // give a map a size, tiles and objects, the game's Restart hook makes its layers again; keepUnknown leaves an object
1024
+ // layer past the snapshot's as it is, as applying an autosave from before the file had it does
1025
+ function editorSetMapSnapshot(record, snapshot, keepUnknown=false)
1020
1026
  {
1021
1027
  const map = record.map;
1022
1028
  editorResizeMap(map, snapshot.width, snapshot.height);
1023
1029
  editorCopyData(editorTileLayerData(map.layers), snapshot.layers);
1024
1030
  editorRestoreObjects(map, snapshot);
1031
+ if (!keepUnknown)
1025
1032
  for (const group of editorObjectGroups(map.layers).slice(snapshot.objects.length))
1026
1033
  group.objects = []; // an Objects layer the editor made since
1027
1034
  editorClearSelections();
@@ -2181,9 +2188,13 @@ function editorPaletteTiles(layer)
2181
2188
  if (!image) return [];
2182
2189
  if (layer.palette?.image === image) return layer.palette.tiles;
2183
2190
 
2184
- const tiles = [];
2191
+ // a sheet read by its columns, a tileset with a margin or a spacing, is read with frame(), which asserts the
2192
+ // frame is inside the image, so a row past the image's end is found before its frame is made
2193
+ const tiles = [], base = live.tileInfo, columns = base.columns;
2185
2194
  for (let i = 0; i < 4096; ++i)
2186
2195
  {
2196
+ if (columns && base.pos.y + (i / columns | 0) * (base.size.y + base.padding*2) + base.size.y > image.height)
2197
+ break;
2187
2198
  const t = editorTileInfo(live, i);
2188
2199
  if (t.pos.x + t.size.x > image.width || t.pos.y + t.size.y > image.height) break;
2189
2200
  tiles.push({tile: i, tileInfo: t});
@@ -419,6 +419,7 @@ export
419
419
  tileCollisionTest,
420
420
  tileCollisionRaycast,
421
421
  tileLayersLoad,
422
+ tileLayersFromLDtk,
422
423
  objectLayersAddType,
423
424
  objectLayersLoad,
424
425
  TileLayerData,
@@ -625,7 +625,11 @@ function setGLEnable(enable)
625
625
  /** Set how many sided polygons to use when drawing circles and ellipses with WebGL
626
626
  * @param {number} sides
627
627
  * @memberof Settings */
628
- function setGLCircleSides(sides) { glCircleSides = sides; }
628
+ function setGLCircleSides(sides)
629
+ {
630
+ ASSERT(isNumber(sides) && sides >= 3, 'circles need at least 3 sides');
631
+ glCircleSides = sides;
632
+ }
629
633
 
630
634
  /** Set default size of tiles in pixels
631
635
  * @param {Vector2} size
@@ -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
@@ -137,7 +141,7 @@ const tileLayersTiledFlips = [[0,0], [3,1], [2,1], [3,0], [0,1], [1,0], [2,0], [
137
141
  * @param {Object} tileMapData - Level data from exported data
138
142
  * @param {TileInfo} [tileInfo] - Default tile info (used for size and texture), tile() by default, none when no image is loaded
139
143
  * @param {number} [renderOrder] - Render order of the top layer
140
- * @param {number} [collisionLayer] - Layer to use for collision if any
144
+ * @param {number|string} [collisionLayer] - Layer to use for collision if any, by its index or its name
141
145
  * @param {boolean} [draw] - Should the layer be drawn automatically
142
146
  * @return {Array<TileCollisionLayer>}
143
147
  * @memberof TileLayers */
@@ -160,6 +164,19 @@ 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
+ const origin = tileInfo.pos.subtract(vec2(tileInfo.padding)); // where the sheet starts, in an atlas too
177
+ tileInfo = new TileInfo(origin.add(vec2(margin)), size, tileInfo.textureInfo, spacing / 2, tileInfo.bleed, columns);
178
+ }
179
+
163
180
  // flatten group layers in order, a group's color and visibility carry to the layers inside it
164
181
  /** @type {Array<{dataLayer: Object, color?: Color, visible?: boolean}>} */
165
182
  const layers = [];
@@ -188,6 +205,14 @@ function tileLayersLoad(tileMapData, tileInfo=tileLayerDefaultTile(), renderOrde
188
205
  };
189
206
  addLayers(tileMapData.layers, WHITE, true);
190
207
 
208
+ // a layer named for collision is the first tile layer of that name, which stays the same when layers are added
209
+ if (typeof collisionLayer === 'string')
210
+ {
211
+ const name = collisionLayer;
212
+ collisionLayer = layers.findIndex((l)=> l.color && l.dataLayer.name === name);
213
+ ASSERT(collisionLayer >= 0, 'no tile layer named ' + name);
214
+ }
215
+
191
216
  // create tile layers and fill with data
192
217
  const tileLayers = [];
193
218
  const levelSize = vec2(tileMapData.width, tileMapData.height);
@@ -237,6 +262,134 @@ function tileLayersLoad(tileMapData, tileInfo=tileLayerDefaultTile(), renderOrde
237
262
  function tileLayersColor(hex)
238
263
  { return new Color().setHex(hex.length === 9 ? '#' + hex.slice(3) + hex.slice(1, 3) : hex); }
239
264
 
265
+ // the Tiled property type of an LDtk field type, for the ones Tiled has a property for
266
+ const tileLayersLDtkTypes = {Int: 'int', Float: 'float', Bool: 'bool', String: 'string', Multilines: 'string',
267
+ Color: 'color', FilePath: 'file'};
268
+
269
+ /**
270
+ * Make a Tiled map of a level of an LDtk project, to load with tileLayersLoad and objectLayersLoad
271
+ * - Each Tiles, AutoLayer and IntGrid layer is a tile layer, the bottom one first as in Tiled; where LDtk stacks
272
+ * tiles in a cell, an edge over a fill, each tile over another goes in a layer of its own just above, named with
273
+ * (2), (3) and so on, and a tile LDtk draws see-through goes in one of its own with that opacity, times the
274
+ * layer's, named with it, like Ground .25
275
+ * - An IntGrid layer is a hidden layer of its values under its own name, for collision, whatever tiles its rules
276
+ * make, which are layers over it named with tiles, like Collisions tiles; pass the name as collisionLayer, since
277
+ * the stacked and see-through layers change the indices from level to level:
278
+ * tileLayersLoad(map, tile(0, 16), 0, 'Collisions')
279
+ * - An Entities layer is an object layer: an entity's name is its type for objectLayersAddType, it is placed at
280
+ * its middle, and its Int, Float, Bool, String, Color and FilePath fields are its properties (an enum is a string)
281
+ * - The tileset is the first tile layer's, with its padding and spacing; give tileLayersLoad a tile info of its image
282
+ * - The level is in the project file (not saved as separate level files), its layers of one grid size; a layer of
283
+ * another grid size or another tileset is left out, with a warning in debug builds, and a project with no tileset
284
+ * keeps its tiles for the tile info the game gives; a project of several worlds has its levels counted in order
285
+ * - Layer offsets are not read
286
+ * - The level editor edits the map this returns, and saves it as a Tiled map
287
+ * @param {Object} ldtk - The LDtk project, its JSON
288
+ * @param {number|string} [level] - Which level, by its index or its identifier
289
+ * @return {Object} - A Tiled map: width, height, tilewidth, tileheight, tilesets and layers
290
+ * @example
291
+ * const map = tileLayersFromLDtk(await fetchJSON('world.ldtk'), 'Level_0');
292
+ * const layers = tileLayersLoad(map, tile(0, 16), 0, 'Collisions'); // its IntGrid layer is solid
293
+ * objectLayersLoad(map);
294
+ * @memberof TileLayers */
295
+ function tileLayersFromLDtk(ldtk, level=0)
296
+ {
297
+ const levels = ldtk?.levels?.length ? ldtk.levels : (ldtk?.worlds || []).flatMap((w)=> w.levels || []);
298
+ const data = typeof level === 'string' ? levels.find((l)=> l.identifier === level) : levels[level];
299
+ if (!data)
300
+ throw new Error('tileLayersFromLDtk: no level ' + level);
301
+ if (!isArray(data.layerInstances))
302
+ throw new Error('tileLayersFromLDtk: level ' + data.identifier + ' has no layers, levels saved as separate files are not read');
303
+ const instances = data.layerInstances;
304
+ const tiled = (l)=> l.__type !== 'Entities';
305
+ const first = instances.find((l)=> tiled(l) && l.__tilesetDefUid != undefined) || instances.find(tiled) || instances[0];
306
+ const grid = first?.__gridSize || 16;
307
+ const width = first?.__cWid || ceil(data.pxWid / grid), height = first?.__cHei || ceil(data.pxHei / grid);
308
+ const map = {width, height, tilewidth: grid, tileheight: grid, orientation: 'orthogonal', renderorder: 'right-down',
309
+ infinite: false, layers: [], nextlayerid: 1, nextobjectid: 1};
310
+
311
+ // the tileset of the first layer that has one, where its tiles are in its image
312
+ const tileset = (ldtk?.defs?.tilesets || []).find((t)=> t.uid === first?.__tilesetDefUid);
313
+ if (tileset)
314
+ map.tilesets = [{firstgid: 1, name: String(tileset.relPath || '').replace(/^.*[\\/]/, '').replace(/\.\w+$/, ''),
315
+ image: tileset.relPath, imagewidth: tileset.pxWid, imageheight: tileset.pxHei,
316
+ tilewidth: tileset.tileGridSize, tileheight: tileset.tileGridSize, margin: tileset.padding || 0,
317
+ spacing: tileset.spacing || 0, columns: tileset.__cWid, tilecount: tileset.__cWid * tileset.__cHei}];
318
+
319
+ // LDtk lists the top layer first, Tiled the bottom one
320
+ for (let i = instances.length; i--;)
321
+ {
322
+ const instance = instances[i], name = instance.__identifier, id = map.nextlayerid++;
323
+ if (!tiled(instance))
324
+ {
325
+ const objects = (instance.entityInstances || []).map((entity)=>
326
+ {
327
+ // an entity is placed by its pivot, an object here by its middle
328
+ const [pivotX=0, pivotY=0] = entity.__pivot || [], w = entity.width || 0, h = entity.height || 0;
329
+ const properties = [];
330
+ for (const field of entity.fieldInstances || [])
331
+ {
332
+ const type = tileLayersLDtkTypes[field.__type] || (/^(Local|Extern)Enum\./.test(field.__type) && 'string');
333
+ type && field.__value != undefined && properties.push({name: field.__identifier, type, value: field.__value});
334
+ }
335
+ return {id: map.nextobjectid++, name: '', type: entity.__identifier, x: entity.px[0] + (.5 - pivotX) * w,
336
+ y: entity.px[1] + (.5 - pivotY) * h, width: w, height: h, rotation: 0, visible: true, properties};
337
+ });
338
+ map.layers.push({id, name, type: 'objectgroup', objects, opacity: 1, visible: true, x: 0, y: 0});
339
+ continue;
340
+ }
341
+ const tiles = [...(instance.autoLayerTiles || []), ...(instance.gridTiles || [])];
342
+ const ownTiles = !tiles.length || !tileset ||
343
+ instance.__tilesetDefUid === tileset.uid && tileset.tileGridSize === grid;
344
+ if (instance.__gridSize !== grid || instance.__cWid !== width || instance.__cHei !== height || !ownTiles)
345
+ {
346
+ debug && console.warn(`tileLayersFromLDtk: layer ${name} has another grid or tileset, left out`);
347
+ continue;
348
+ }
349
+ // an IntGrid layer's values, hidden, under its own name, for collision whatever its tiles do
350
+ const empty = ()=> new Array(width * height).fill(0), opacity = instance.__opacity ?? 1;
351
+ const values = instance.__type === 'IntGrid';
352
+ if (values)
353
+ {
354
+ const data = empty();
355
+ (instance.intGridCsv || []).forEach((value, k)=> k < data.length && (data[k] = value));
356
+ map.layers.push({id, name, type: 'tilelayer', width, height, data, opacity, visible: false, x: 0, y: 0});
357
+ if (!tiles.length) continue;
358
+ }
359
+
360
+ // a layer for each depth in a cell and each opacity, the first tiles drawn solid first
361
+ const tilesName = values ? name + ' tiles' : name;
362
+ const stacks = [{depth: 0, alpha: 1, data: empty()}], stack = (depth, alpha)=>
363
+ {
364
+ let s = stacks.find((s)=> s.depth === depth && s.alpha === alpha);
365
+ s || stacks.push(s = {depth, alpha, data: empty()});
366
+ return s.data;
367
+ };
368
+ {
369
+ // a tile's place in pixels, its tile in the sheet, its flips, bit 0 across and bit 1 down, which are
370
+ // Tiled's two top bits, and its opacity; LDtk lists them in the order it draws them, so the first in a
371
+ // cell is the bottom one and each after it goes a layer higher
372
+ const depth = new Uint16Array(width * height);
373
+ for (const t of tiles)
374
+ {
375
+ const x = floor(t.px[0] / grid), y = floor(t.px[1] / grid), cell = x + y * width;
376
+ if (x >= 0 && x < width && y >= 0 && y < height)
377
+ stack(depth[cell]++, round(clamp(t.a ?? 1) * 1e3) / 1e3)[cell] =
378
+ (t.t + 1 | (t.f & 1 ? 0x80000000 : 0) | (t.f & 2 ? 0x40000000 : 0)) >>> 0;
379
+ }
380
+ }
381
+ const visible = instance.visible !== false;
382
+ stacks.sort((a, b)=> a.depth - b.depth || b.alpha - a.alpha);
383
+ for (const {depth, alpha, data} of stacks)
384
+ {
385
+ const layerName = tilesName + (depth ? ` (${depth + 1})` : '') + (alpha < 1 ? ' ' + String(alpha).replace(/^0/, '') : '');
386
+ map.layers.push({id: values || depth || alpha < 1 ? map.nextlayerid++ : id, name: layerName, type: 'tilelayer',
387
+ width, height, data, opacity: opacity * alpha, visible, x: 0, y: 0});
388
+ }
389
+ }
390
+ return map;
391
+ }
392
+
240
393
  ///////////////////////////////////////////////////////////////////////////////
241
394
  // Object layers
242
395
 
package/plugins/zzfxm.js DELETED
@@ -1,162 +0,0 @@
1
- /**
2
- * LittleJS ZzFXM Plugin
3
- * - A port of ZzFXM, the Zuper Zmall Zound Zynth music player, by Keith Clark and Frank Force, MIT licensed,
4
- * https://github.com/keithclark/ZzFXM; its notice is in COPYRIGHT.txt
5
- * @namespace ZzFXM
6
- */
7
-
8
- 'use strict';
9
-
10
- /**
11
- * Music Object - Stores a zzfx music track for later use
12
- *
13
- * <a href=https://keithclark.github.io/ZzFXM/>Create music with the ZzFXM tracker.</a>
14
- * @extends Sound
15
- * @memberof ZzFXM
16
- * @example
17
- * // create some music
18
- * const music_example = new ZzFXMusic(
19
- * [
20
- * [ // instruments
21
- * [,0,400] // simple note
22
- * ],
23
- * [ // patterns
24
- * [ // pattern 1
25
- * [ // channel 0
26
- * 0, -1, // instrument 0, left speaker
27
- * 1, 0, 9, 1 // channel notes
28
- * ],
29
- * [ // channel 1
30
- * 0, 1, // instrument 0, right speaker
31
- * 0, 12, 17, -1 // channel notes
32
- * ]
33
- * ],
34
- * ],
35
- * [0, 0, 0, 0], // sequence, play pattern 0 four times
36
- * 90 // BPM
37
- * ]);
38
- *
39
- * // play the music on a loop
40
- * music_example.playMusic();
41
- */
42
- class ZzFXMusic extends Sound
43
- {
44
- /** Create a music object and cache the zzfx music samples for later use
45
- * @param {[Array, Array, Array, number?, ...any[]]} zzfxMusic - Array of zzfx music parameters: instruments, patterns, sequence, and an optional BPM; anything after it, like the tracker's metadata, is ignored
46
- */
47
- constructor(zzfxMusic)
48
- {
49
- super(undefined);
50
-
51
- if (!soundEnable || headlessMode) return;
52
- const [instruments, patterns, sequence, BPM] = zzfxMusic;
53
- super.sampleChannels = zzfxM(instruments, patterns, sequence, BPM); // the setter, without declaring a field that hides it in the typings
54
- this.buildSampleBuffer(); // hand the samples to an audio buffer now, like a zzfx sound, so the arrays are released
55
- this.loadedPercent = 1; // generated in place, so it is loaded like a zzfx sound
56
- }
57
- }
58
-
59
- ///////////////////////////////////////////////////////////////////////////////
60
- // ZzFX Music Renderer v2.0.3 by Keith Clark and Frank Force
61
-
62
- /** Generate samples for a ZzFM song with given parameters
63
- * @param {Array} instruments - Array of ZzFX sound parameters
64
- * @param {Array} patterns - Array of pattern data
65
- * @param {Array} sequence - Array of pattern indexes
66
- * @param {number} [BPM] - Playback speed of the song in BPM
67
- * @return {Array} - Left and right channel sample data
68
- * @memberof ZzFXM */
69
- function zzfxM(instruments, patterns, sequence, BPM = 125)
70
- {
71
- let i, j, k;
72
- let instrumentParameters;
73
- let note;
74
- let sample;
75
- let patternChannel;
76
- let notFirstBeat;
77
- let stop;
78
- let instrument;
79
- let attenuation;
80
- let outSampleOffset;
81
- let isSequenceEnd;
82
- let sampleOffset = 0;
83
- let nextSampleOffset;
84
- let sampleBuffer = [];
85
- let leftChannelBuffer = [];
86
- let rightChannelBuffer = [];
87
- let channelIndex = 0;
88
- let panning = 0;
89
- let hasMore = 1;
90
- let sampleCache = {};
91
- let beatLength = audioDefaultSampleRate / BPM * 60 >> 2;
92
-
93
- // for each channel in order until there are no more
94
- for (; hasMore; channelIndex++) {
95
-
96
- // reset current values
97
- sampleBuffer = [hasMore = notFirstBeat = outSampleOffset = 0];
98
-
99
- // for each pattern in sequence
100
- sequence.forEach((patternIndex, sequenceIndex)=> {
101
- // get pattern for current channel, use empty 1 note pattern if none found
102
- patternChannel = patterns[patternIndex][channelIndex] || [0, 0, 0];
103
-
104
- // check if there are more channels
105
- hasMore |= patterns[patternIndex][channelIndex]&&1;
106
-
107
- // get next offset, use the length of first channel
108
- nextSampleOffset = outSampleOffset + (patterns[patternIndex][0].length - 2 - (notFirstBeat?0:1)) * beatLength;
109
- // for each beat in pattern, plus one extra if end of sequence
110
- isSequenceEnd = sequenceIndex === sequence.length - 1;
111
- for (i = 2, k = outSampleOffset; i < patternChannel.length + isSequenceEnd; notFirstBeat = ++i) {
112
-
113
- // <channel-note>
114
- note = patternChannel[i];
115
-
116
- // stop if end, different instrument or new note
117
- stop = i === patternChannel.length + isSequenceEnd - 1 && isSequenceEnd ||
118
- instrument !== (patternChannel[0] || 0) || note | 0;
119
-
120
- // fill buffer with samples for previous beat, most cpu intensive part
121
- for (j = 0; j < beatLength && notFirstBeat;
122
-
123
- // fade off attenuation at end of beat if stopping note, prevents clicking
124
- j++ > beatLength - 99 && stop && attenuation < 1? attenuation += 1 / 99 : 0
125
- ) {
126
- // copy sample to stereo buffers with panning
127
- sample = (1 - attenuation) * sampleBuffer[sampleOffset++] / 2 || 0;
128
- leftChannelBuffer[k] = (leftChannelBuffer[k] || 0) - sample * panning + sample;
129
- rightChannelBuffer[k] = (rightChannelBuffer[k++] || 0) + sample * panning + sample;
130
- }
131
-
132
- // set up for next note
133
- if (note) {
134
- // set attenuation
135
- attenuation = note % 1;
136
- panning = patternChannel[1] || 0;
137
- if (note |= 0) {
138
- // get cached sample
139
- sampleBuffer = sampleCache[
140
- [
141
- instrument = patternChannel[sampleOffset = 0] || 0,
142
- note
143
- ]
144
- ] = sampleCache[[instrument, note]] || (
145
- // add sample to cache
146
- instrumentParameters = [...instruments[instrument]],
147
- instrumentParameters[2] = (instrumentParameters[2] || 220) * 2**(note / 12 - 1),
148
-
149
- // allow negative values to stop notes
150
- note > 0 ? zzfxG(...instrumentParameters) : []
151
- );
152
- }
153
- }
154
- }
155
-
156
- // update the sample offset
157
- outSampleOffset = nextSampleOffset;
158
- });
159
- }
160
-
161
- return [leftChannelBuffer, rightChannelBuffer];
162
- }