littlejsengine 1.23.1 → 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.
@@ -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});
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.0';
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`,
@@ -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,
@@ -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
 
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
- }