littlejsengine 1.24.2 → 1.25.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/EDITOR.md +2 -2
  3. package/FAQ.md +6 -0
  4. package/REFERENCE.md +86 -41
  5. package/dist/littlejs.d.ts +600 -323
  6. package/dist/littlejs.esm.js +2412 -1797
  7. package/dist/littlejs.esm.min.js +38 -24
  8. package/dist/littlejs.js +2410 -1797
  9. package/dist/littlejs.min.js +37 -23
  10. package/dist/littlejs.release.js +2393 -1826
  11. package/package.json +1 -1
  12. package/plugins/box2d.js +41 -28
  13. package/plugins/drawUtilities.js +374 -380
  14. package/plugins/gltf.js +331 -55
  15. package/plugins/lightSystem.js +2 -2
  16. package/plugins/math3d.js +1013 -1014
  17. package/plugins/newgrounds.js +1 -4
  18. package/plugins/particleEffects.js +3 -4
  19. package/plugins/pathFinder.js +52 -28
  20. package/plugins/pluginExport.js +2 -0
  21. package/plugins/postProcess.js +196 -65
  22. package/plugins/render3d.js +8 -6
  23. package/plugins/render3dEditor.js +3 -3
  24. package/plugins/render3dExtras.js +31 -10
  25. package/plugins/render3dLevel.js +15 -4
  26. package/plugins/textureSheet.js +15 -7
  27. package/plugins/tweenSystem.js +43 -38
  28. package/plugins/uiSystem.js +25 -20
  29. package/src/engine.js +39 -21
  30. package/src/engineAudio.js +13 -9
  31. package/src/engineBuild.mjs +49 -2
  32. package/src/engineDebug.js +3 -2
  33. package/src/engineDraw.js +20 -18
  34. package/src/engineEditor.js +75 -23
  35. package/src/engineInput.js +10 -10
  36. package/src/engineMath.js +3 -6
  37. package/src/engineObject.js +8 -9
  38. package/src/engineParticles.js +8 -7
  39. package/src/engineRelease.js +17 -10
  40. package/src/engineTileLayer.js +33 -14
  41. package/src/engineUtilities.js +15 -2
  42. package/src/engineWebGL.js +34 -12
@@ -15,6 +15,16 @@
15
15
 
16
16
  const LEVEL3D_VERSION = 1; // the format a level's littlejs3D names
17
17
 
18
+ /** A 3D level as it is saved, what level3DLoad takes and the 3D level editor edits
19
+ * @typedef {Object} Level3D
20
+ * @property {number} [littlejs3D] - The format's version, 1
21
+ * @property {Array<Object>} objects - Each {id, type, pos, rotation, scale, properties}, all but type optional
22
+ * @property {Object} [scene] - The scene block: sky, ambient, sunDirection, sunColor, fog, fogColor, shadows, lensFlare
23
+ * @property {Object} [voxels] - A block map: {pos, size, blocks}
24
+ * @property {Object} [terrain] - A height map: {pos, size, height, color, heights, paint}
25
+ * @property {Object} [prefabs] - The level's own prefabs by name, each {objects, attached}
26
+ * @memberof Level3D */
27
+
18
28
  // the types a level's objects are made from, by name
19
29
  const level3DTypes = new Map;
20
30
 
@@ -33,7 +43,7 @@ const level3DBaseScale = new WeakMap;
33
43
  * - Adding a name again replaces it, Box, Sphere, Cylinder and Light too
34
44
  * @param {string} name - The type the objects have in the level
35
45
  * @param {Function} make - A class made at each object's position, or a function called with it
36
- * @param {Object} [defaults] - Properties of each one made, the level editor shows inputs for them
46
+ * @param {Object<string, any>} [defaults] - Properties of each one made, the level editor shows inputs for them
37
47
  * @param {TileInfo} [tileInfo] - An icon for the level editor
38
48
  * @memberof Level3D
39
49
  * @example
@@ -87,7 +97,7 @@ function level3DAddMesh(name, mesh, tileInfo, color=WHITE)
87
97
  * sunColor, fog, its start and end, fogColor, the horizon color when not given, shadows, and lensFlare, the
88
98
  * sun's lens flare; what the block leaves out stays as the game set it, and a level with no block changes
89
99
  * nothing
90
- * @param {Object} level - The level, the level editor edits this same object
100
+ * @param {Level3D} level - The level, the level editor edits this same object
91
101
  * @return {Array<any>} - What each object's type made, a function that made nothing is left out
92
102
  * @memberof Level3D */
93
103
  function level3DLoad(level)
@@ -420,7 +430,7 @@ function level3DMakeAt(object, pos, rotation, scale)
420
430
  * @param {Vector3} [pos3D]
421
431
  * @param {Vector3} [rotation3D] - In radians, as an object has it
422
432
  * @param {Vector3} [scale3D] - Times the scale the type makes it with
423
- * @param {Object} [properties] - Over the type's defaults
433
+ * @param {Object<string, any>} [properties] - Over the type's defaults
424
434
  * @return {any} - What the type made, a Prefab3D for a prefab, undefined when there is no such type
425
435
  * @memberof Level3D
426
436
  * @example
@@ -459,7 +469,8 @@ const level3DPrefabMaking = [];
459
469
  * by hand does
460
470
  * - Adding a name again replaces it
461
471
  * @param {string} name - The type its instances have in a level
462
- * @param {Object} prefab - {objects, attached}
472
+ * @param {{objects: Array<Object>, attached?: boolean}} prefab - Its objects, about its own origin, and whether
473
+ * they are attached
463
474
  * @memberof Level3D
464
475
  * @example
465
476
  * level3DAddPrefab('Tower', {objects: [{type: 'Box', pos: [0, 1, 0], scale: [2, 2, 2]},
@@ -37,6 +37,8 @@ let textureSheetPendingCount = 0;
37
37
  const textureSheetLoaded = new Map;
38
38
  // a load's key: what it was given, a vector by its numbers
39
39
  const textureSheetLoadKey = (...parts)=> parts.map((p)=> isVector2(p) ? p.x + ',' + p.y : String(p)).join('|');
40
+ // a load whose file failed is forgotten, so loading it again tries again, as when the server comes back
41
+ const textureSheetLoadFailed = (key, loaded)=> textureSheetLoaded.get(key) === loaded && textureSheetLoaded.delete(key);
40
42
 
41
43
  /**
42
44
  * Texture Sheet - A texture that images are packed into as they load
@@ -193,7 +195,8 @@ class TextureSheet
193
195
  * - Pass frameSize for animations, then step through them with TileInfo.frame
194
196
  * - Grid images keep their layout and frames wrap down to the next row
195
197
  * - Pass sourcePadding if the source image has padding baked in around frames
196
- * - The same image loaded again with the same settings gives back what the first load did, packed once
198
+ * - The same image loaded again with the same settings gives back what the first load did, packed once, unless
199
+ * that load failed
197
200
  * @param {string} src - Image source path
198
201
  * @param {Vector2|number} [frameSize] - Size of each animation frame in pixels, or the whole image less its
199
202
  * source padding if not passed
@@ -248,7 +251,7 @@ function loadSprite(src, frameSize, padding=textureSheetPadding, sourcePadding=0
248
251
  if (!added)
249
252
  {
250
253
  // leave the tile empty, no sheet can hold it
251
- LOG('loadSprite image is too large to fit on a texture sheet:', src);
254
+ console.warn('loadSprite image is too large to fit on a texture sheet:', src);
252
255
  return;
253
256
  }
254
257
  Object.assign(tileInfo, added.tile);
@@ -257,7 +260,8 @@ function loadSprite(src, frameSize, padding=textureSheetPadding, sourcePadding=0
257
260
  else
258
261
  {
259
262
  // leave the tile empty if the image failed to load
260
- LOG('loadSprite failed to load image:', src);
263
+ console.warn('loadSprite failed to load image:', src);
264
+ textureSheetLoadFailed(key, tileInfo);
261
265
  }
262
266
  });
263
267
 
@@ -275,7 +279,8 @@ function loadSprite(src, frameSize, padding=textureSheetPadding, sourcePadding=0
275
279
  * is not read, cut it into its tiles first
276
280
  * - An image that fails to load, or that no sheet can hold, adds no tiles and says so in the console, so the tiles
277
281
  * of the images after it move up
278
- * - The same images loaded again with the same settings give back what the first load did, packed once
282
+ * - The same images loaded again with the same settings give back what the first load did, packed once, unless
283
+ * an image of it failed
279
284
  * @param {Array<string>} sources - Image source paths
280
285
  * @param {Vector2|number} [tileSize] - Size of a tile in pixels
281
286
  * @param {number} [padding] - How many pixels padding around each tile on the sheet
@@ -317,6 +322,7 @@ function loadTiles(sources, tileSize=tileDefaultSize, padding=textureSheetPaddin
317
322
  // the whole tiles of it, an edge past the last one left out
318
323
  const columns = image.width / size.x | 0, rows = image.height / size.y | 0, count = columns * rows;
319
324
  const added = count && textureSheetAdd(vec2(columns * size.x, rows * size.y), size, padding, 0);
325
+ image.width || textureSheetLoadFailed(key, set);
320
326
  if (!added)
321
327
  return console.warn('loadTiles: ' + src + (count ? ' does not fit on a texture sheet' :
322
328
  ' failed to load, or is smaller than a tile') + ', its tiles are left out');
@@ -335,7 +341,8 @@ function loadTiles(sources, tileSize=tileDefaultSize, padding=textureSheetPaddin
335
341
  * - Aseprite frame tags become animations, so do names like run_0, run_1
336
342
  * - Trimmed frames are restored to their full source size when packed
337
343
  * - Rotated frames are rotated back upright when packed
338
- * - The same atlas loaded again by its paths, with the same padding, gives back what the first load did, packed once
344
+ * - The same atlas loaded again by its paths, with the same padding, gives back what the first load did, packed
345
+ * once, unless that load failed
339
346
  * @param {string} imageSrc - Atlas image path
340
347
  * @param {string|Object} jsonSrc - Atlas json path, or already parsed json data
341
348
  * @param {number} [padding] - How many pixels padding around each frame
@@ -390,7 +397,7 @@ function loadAtlas(imageSrc, jsonSrc, padding=textureSheetPadding)
390
397
  const added = textureSheetAdd(blockSize, sourceSize, padding);
391
398
  if (!added)
392
399
  {
393
- LOG('loadAtlas frames are too large to fit on a texture sheet:', group.name);
400
+ console.warn('loadAtlas frames are too large to fit on a texture sheet:', group.name);
394
401
  continue;
395
402
  }
396
403
  const {sheet, tile} = added;
@@ -424,7 +431,8 @@ function loadAtlas(imageSrc, jsonSrc, padding=textureSheetPadding)
424
431
  else
425
432
  {
426
433
  // leave the atlas empty if either file failed to load
427
- LOG('loadAtlas failed to load:', imageSrc, jsonSrc);
434
+ console.warn('loadAtlas failed to load:', imageSrc, jsonSrc);
435
+ textureSheetLoadFailed(key, atlas);
428
436
  }
429
437
  });
430
438
 
@@ -11,7 +11,8 @@
11
11
 
12
12
  ///////////////////////////////////////////////////////////////////////////////
13
13
 
14
- // Module-private list of tweens currently running, each with its active flag set while it is in it.
14
+ // Module-private list of tweens currently running, each with its active flag set while it is in it; a stopped
15
+ // one stays listed, inactive, until the next update takes it out, so stopping many is not a search for each
15
16
  const tweenActive = [];
16
17
  const tweenUpdateList = []; // the tweens an update moves, the ones active when it began
17
18
  let tweenUpdatePass = 0; // counts the updates, a tween started during one waits for the next
@@ -22,14 +23,10 @@ function tweenActivate(tween)
22
23
  tween.activePass = tweenUpdatePass; // started again, even while active, so this update leaves it alone
23
24
  if (tween.active) return;
24
25
  tween.active = true;
25
- tweenActive.push(tween);
26
- }
27
- function tweenDeactivate(tween)
28
- {
29
- if (!tween.active) return;
30
- tween.active = false;
31
- tweenActive.splice(tweenActive.indexOf(tween), 1);
26
+ tween.listed || tweenActive.push(tween); // one stopped this update is still listed, in its place
27
+ tween.listed = true;
32
28
  }
29
+ function tweenDeactivate(tween) { tween.active = false; }
33
30
 
34
31
  // True if the value is an instance of a class that exposes a numeric-percent
35
32
  // `lerp(other, percent)` method (Vector2, Color, or any future class).
@@ -57,9 +54,9 @@ class Tween
57
54
  * callback receives the interpolated value (a number, or a fresh instance
58
55
  * for lerp-able types). Both endpoints must be the same type.
59
56
  * @param {function(NonNullable<T>):void} callback - Called with the interpolated value each frame
60
- * @param {T} [start=0] - Starting value
61
- * @param {T} [end=1] - Ending value
62
- * @param {number} [duration=1] - Duration in seconds
57
+ * @param {T} [start] - Starting value
58
+ * @param {T} [end] - Ending value
59
+ * @param {number} [duration] - Duration in seconds
63
60
  * @param {Object} [options]
64
61
  * @param {function(number):number} [options.ease] - Easing function (defaults to LINEAR)
65
62
  * @param {boolean} [options.useRealTime=false] - Advance even when the game is paused (matches Timer's useRealTime)
@@ -111,9 +108,12 @@ class Tween
111
108
  /** Remaining iterations including the current run (loop/pingPong only).
112
109
  * @private */
113
110
  this.loopRemaining = 0;
114
- /** Whether it is in the active list, see isActive
111
+ /** Whether it is running, see isActive
115
112
  * @private */
116
113
  this.active = false;
114
+ /** Whether it is in the active list, which a stopped one leaves at the next update
115
+ * @private */
116
+ this.listed = false;
117
117
  /** The update it was started in, it first moves on the one after
118
118
  * @private */
119
119
  this.activePass = 0;
@@ -134,7 +134,7 @@ class Tween
134
134
 
135
135
  /** Set the easing curve and return this for chaining.
136
136
  * @param {function(number):number} easeFn
137
- * @returns {Tween<T>} */
137
+ * @return {Tween<T>} */
138
138
  setEase(easeFn)
139
139
  {
140
140
  this.ease = easeFn;
@@ -149,7 +149,7 @@ class Tween
149
149
  * - It is kept by `restart`, so a restarted tween calls it again when it completes
150
150
  * - `stop` and `tweenStopAll` end a tween without calling it
151
151
  * @param {function():void} callback
152
- * @returns {Tween<T>} */
152
+ * @return {Tween<T>} */
153
153
  then(callback)
154
154
  {
155
155
  this.onComplete = callback;
@@ -165,7 +165,7 @@ class Tween
165
165
  * A `then` callback, set before or after, is called when the last
166
166
  * iteration ends.
167
167
  * @param {number} [count=Infinity]
168
- * @returns {Tween<T>} */
168
+ * @return {Tween<T>} */
169
169
  loop(count = Infinity)
170
170
  {
171
171
  this.loopRemaining = count;
@@ -180,7 +180,7 @@ class Tween
180
180
  * A `then` callback, set before or after, is called when the last
181
181
  * iteration ends.
182
182
  * @param {number} [count=Infinity]
183
- * @returns {Tween<T>} */
183
+ * @return {Tween<T>} */
184
184
  pingPong(count = Infinity)
185
185
  {
186
186
  this.loopRemaining = count;
@@ -213,7 +213,7 @@ class Tween
213
213
  }
214
214
 
215
215
  /** True if this tween is in the active list and not paused.
216
- * @returns {boolean} */
216
+ * @return {boolean} */
217
217
  isActive()
218
218
  {
219
219
  return !this.paused && this.active;
@@ -221,7 +221,7 @@ class Tween
221
221
 
222
222
  /** Get how far this tween has progressed, from 0 (just started) to 1
223
223
  * (completed). Clamped — overshoot past completion still reads 1.
224
- * @returns {number} */
224
+ * @return {number} */
225
225
  getPercent()
226
226
  {
227
227
  return percent(this.duration - this.life, 0, this.duration);
@@ -230,7 +230,7 @@ class Tween
230
230
  /** Get the current interpolated value (the value most recently passed to
231
231
  * the callback). Returns a number, Vector2, Vector3 or Color depending on the
232
232
  * tween's start/end types.
233
- * @returns {T} */
233
+ * @return {T} */
234
234
  getValue()
235
235
  {
236
236
  return this.interp(this.life);
@@ -242,7 +242,7 @@ class Tween
242
242
  * - A vector goes past its ends as far as the easing does, as a number does; a Color stays between them,
243
243
  * so its channels stay in range, and any other type goes as far as its own lerp takes it
244
244
  * @param {number} life
245
- * @returns {T} */
245
+ * @return {T} */
246
246
  interp(life)
247
247
  {
248
248
  // the ends of whatever type it tweens, each kind is handled below
@@ -285,44 +285,44 @@ const Ease =
285
285
  {
286
286
  /** Linear (identity) curve.
287
287
  * @param {number} x
288
- * @returns {number}
288
+ * @return {number}
289
289
  * @memberof TweenSystem.Ease */
290
290
  LINEAR: (x) => x,
291
291
 
292
292
  /** Power curve factory: `Ease.POWER(n)` returns `x => x**n`.
293
293
  * Use n=2 for quadratic, n=3 for cubic, etc.
294
294
  * @param {number} n
295
- * @returns {function(number):number}
295
+ * @return {function(number):number}
296
296
  * @memberof TweenSystem.Ease */
297
297
  POWER: (n) => (x) => x ** n,
298
298
 
299
299
  /** Sine ease-in curve: starts slow, ends fast.
300
300
  * @param {number} x
301
- * @returns {number}
301
+ * @return {number}
302
302
  * @memberof TweenSystem.Ease */
303
303
  SINE: (x) => 1 - cos(x * (PI / 2)),
304
304
 
305
305
  /** Circular ease-in curve.
306
306
  * @param {number} x
307
- * @returns {number}
307
+ * @return {number}
308
308
  * @memberof TweenSystem.Ease */
309
309
  CIRC: (x) => 1 - (1 - x * x)**.5,
310
310
 
311
311
  /** Exponential ease-in curve (`2^(10x-10)`).
312
312
  * @param {number} x
313
- * @returns {number}
313
+ * @return {number}
314
314
  * @memberof TweenSystem.Ease */
315
315
  EXPO: (x) => x === 0 ? 0 : 2 ** (10 * x - 10),
316
316
 
317
317
  /** Back ease-in: overshoots backward at the start before snapping forward.
318
318
  * @param {number} x
319
- * @returns {number}
319
+ * @return {number}
320
320
  * @memberof TweenSystem.Ease */
321
321
  BACK: (x) => x * x * (2.70158 * x - 1.70158),
322
322
 
323
323
  /** Elastic ease-in: oscillations that grow toward the end.
324
324
  * @param {number} x
325
- * @returns {number}
325
+ * @return {number}
326
326
  * @memberof TweenSystem.Ease */
327
327
  ELASTIC: (x) =>
328
328
  x === 0 ? 0 :
@@ -332,7 +332,7 @@ const Ease =
332
332
  /** Spring ease-in: wobbles around the start before springing to the end;
333
333
  * `Ease.OUT(Ease.SPRING)` overshoots and settles on the target.
334
334
  * @param {number} x
335
- * @returns {number}
335
+ * @return {number}
336
336
  * @memberof TweenSystem.Ease */
337
337
  SPRING: (x) =>
338
338
  1 -
@@ -346,7 +346,7 @@ const Ease =
346
346
  * classic "object falls and hits the ground" shape (bounces near x=1),
347
347
  * wrap with `Ease.OUT`: `Ease.OUT(Ease.BOUNCE)`.
348
348
  * @param {number} x
349
- * @returns {number}
349
+ * @return {number}
350
350
  * @memberof TweenSystem.Ease
351
351
  * @example
352
352
  * Ease.BOUNCE // ease-in bounce (bouncy at start)
@@ -369,7 +369,7 @@ const Ease =
369
369
  * convention, so wrapping a curve in `IN` is a no-op — useful when
370
370
  * picking the direction programmatically.
371
371
  * @param {function(number):number} f - Curve to use as ease-in (returned unchanged)
372
- * @returns {function(number):number}
372
+ * @return {function(number):number}
373
373
  * @memberof TweenSystem.Ease
374
374
  * @example
375
375
  * // Pick direction at runtime
@@ -380,7 +380,7 @@ const Ease =
380
380
 
381
381
  /** Reverse a curve so it eases out instead of in: `x => 1 - f(1 - x)`.
382
382
  * @param {function(number):number} f
383
- * @returns {function(number):number}
383
+ * @return {function(number):number}
384
384
  * @memberof TweenSystem.Ease
385
385
  * @example
386
386
  * Ease.OUT(Ease.POWER(2)) // ease-out quadratic
@@ -389,7 +389,7 @@ const Ease =
389
389
 
390
390
  /** Combine the first half of `f` with `Ease.OUT(f)` for a symmetric curve.
391
391
  * @param {function(number):number} f
392
- * @returns {function(number):number}
392
+ * @return {function(number):number}
393
393
  * @memberof TweenSystem.Ease */
394
394
  IN_OUT: (f) => Ease.PIECEWISE(f, Ease.OUT(f)),
395
395
 
@@ -397,7 +397,7 @@ const Ease =
397
397
  * Each curve is mapped to its section: section i runs over [i/n, (i+1)/n]
398
398
  * and its output is mapped to [i/n, (i+1)/n] of the overall range.
399
399
  * @param {...function(number):number} fns
400
- * @returns {function(number):number}
400
+ * @return {function(number):number}
401
401
  * @memberof TweenSystem.Ease */
402
402
  PIECEWISE: (...fns) =>
403
403
  {
@@ -415,7 +415,7 @@ const Ease =
415
415
  * @param {number} y1
416
416
  * @param {number} x2
417
417
  * @param {number} y2
418
- * @returns {function(number):number}
418
+ * @return {function(number):number}
419
419
  * @memberof TweenSystem.Ease
420
420
  * @example
421
421
  * Ease.BEZIER(0.25, 0.1, 0.25, 1) // CSS "ease"
@@ -465,12 +465,12 @@ const Ease =
465
465
  * @param {string} propertyPath - Dot-separated path, e.g. `'pos.x'` or `'color'`
466
466
  * @param {T} start - Starting value
467
467
  * @param {T} end - Ending value
468
- * @param {number} [duration=1] - Duration in seconds
468
+ * @param {number} [duration] - Duration in seconds
469
469
  * @param {Object} [options] - Same options as the Tween constructor
470
470
  * @param {function(number):number} [options.ease] - Easing function (defaults to LINEAR)
471
471
  * @param {boolean} [options.useRealTime=false] - Advance even when the game is paused
472
472
  * @param {boolean} [options.paused=false] - Start in paused state
473
- * @returns {Tween<T>}
473
+ * @return {Tween<T>}
474
474
  * @memberof TweenSystem
475
475
  * @example
476
476
  * // Numeric: slide an object's x with an ease-out sine curve
@@ -591,8 +591,13 @@ function tweenUpdate(gameDelta, realDelta)
591
591
  // from the next update. Newest first, as the list has always been walked.
592
592
  // a callback that calls tweenUpdate itself gets a list of its own, the outer update is still walking this one
593
593
  const list = tweenUpdateList.length ? [] : tweenUpdateList, pass = ++tweenUpdatePass;
594
+ let kept = 0;
594
595
  for (const t of tweenActive)
595
- list.push(t);
596
+ if (t.active)
597
+ list.push(tweenActive[kept++] = t);
598
+ else
599
+ t.listed = false; // stopped since the last update, out of the list now
600
+ tweenActive.length = kept;
596
601
  // the list is let go however the walk ends: a callback that throws must not leave it held, or every
597
602
  // update after would take it for an update still going and make a list of its own
598
603
  try
@@ -645,7 +650,7 @@ function tweenUpdate(gameDelta, realDelta)
645
650
  function tweenStopAll()
646
651
  {
647
652
  for (const t of tweenActive)
648
- t.thenCallback = undefined, t.active = false;
653
+ t.thenCallback = undefined, t.active = t.listed = false;
649
654
  tweenActive.length = 0;
650
655
  }
651
656
 
@@ -35,8 +35,14 @@ let uiDebug = 0;
35
35
  function uiSetDebug(debugMode)
36
36
  { uiDebug = typeof debugMode === 'boolean' ? (debugMode ? 1 : 0) : debugMode; }
37
37
 
38
+ /**
39
+ * @callback DragAndDropCallback - Callback for drag and drop events
40
+ * @param {DragEvent} event - The drag event
41
+ * @memberof UISystem
42
+ */
43
+
38
44
  ///////////////////////////////////////////////////////////////////////////////
39
- /**
45
+ /**
40
46
  * UI System Global Object
41
47
  * @memberof UISystem
42
48
  */
@@ -513,7 +519,7 @@ class UISystemPlugin
513
519
  * @param {'left'|'center'|'right'} [align]
514
520
  * @param {string} [font=uiSystem.defaultFont]
515
521
  * @param {string} [fontStyle]
516
- * @param {boolean} [applyMaxWidth=true]
522
+ * @param {boolean} [applyMaxWidth]
517
523
  * @param {Vector2} [textShadow]
518
524
  * @param {Color} [shadowColor]
519
525
  * @param {number} [shadowBlur]
@@ -528,12 +534,6 @@ class UISystemPlugin
528
534
  context.shadowColor = '#0000';
529
535
  }
530
536
 
531
- /**
532
- * @callback DragAndDropCallback - Callback for drag and drop events
533
- * @param {DragEvent} event - The drag event
534
- * @memberof UISystem
535
- */
536
-
537
537
  /** Setup drag and drop event handlers
538
538
  * Automatically prevents defaults and calls the given functions
539
539
  * @param {DragAndDropCallback} [onDrop] - when a file is dropped
@@ -1212,19 +1212,24 @@ class UIObject
1212
1212
  /** Called when the navigation button is pressed on this object */
1213
1213
  navigatePressed() { this.click(); }
1214
1214
 
1215
- /** @return {boolean} - Is the mouse hovering over this element */
1215
+ /** Is the mouse hovering over this element
1216
+ * @return {boolean} */
1216
1217
  isHoverObject() { return uiSystem.hoverObject === this; }
1217
1218
 
1218
- /** @return {boolean} - Is the mouse held onto this element */
1219
+ /** Is the mouse held onto this element
1220
+ * @return {boolean} */
1219
1221
  isActiveObject() { return uiSystem.activeObject === this; }
1220
1222
 
1221
- /** @return {boolean} - Is the gamepad or keyboard navigation object */
1223
+ /** Is the gamepad or keyboard navigation object
1224
+ * @return {boolean} */
1222
1225
  isNavigationObject() { return uiSystem.navigationObject === this; }
1223
1226
 
1224
- /** @return {boolean} - Is this object in keyboard input mode */
1227
+ /** Is this object in keyboard input mode
1228
+ * @return {boolean} */
1225
1229
  isKeyInputObject() { return uiSystem.keyInputObject === this; }
1226
1230
 
1227
- /** @return {boolean} - Can it be interacted with, it and every parent visible and enabled */
1231
+ /** Can it be interacted with, it and every parent visible and enabled
1232
+ * @return {boolean} */
1228
1233
  isInteractive() { return this.interactive && uiObjectIsUsable(this); }
1229
1234
 
1230
1235
  /** Returns string containing info about this object for debugging
@@ -1755,9 +1760,9 @@ class UIVideo extends UIObject
1755
1760
  * @param {Vector2} pos
1756
1761
  * @param {Vector2} size
1757
1762
  * @param {string} src - Video file path or URL
1758
- * @param {boolean} [autoplay=false] - Start playing immediately?
1759
- * @param {boolean} [loop=false] - Loop the video?
1760
- * @param {number} [volume=1] - Volume percent scaled by global volume (0-1)
1763
+ * @param {boolean} [autoplay] - Start playing immediately?
1764
+ * @param {boolean} [loop] - Loop the video?
1765
+ * @param {number} [volume] - Volume percent scaled by global volume (0-1)
1761
1766
  */
1762
1767
  constructor(pos, size, src, autoplay=false, loop=false, volume=1)
1763
1768
  {
@@ -1912,10 +1917,10 @@ class UILayout extends UIObject
1912
1917
  {
1913
1918
  /** Create a UILayout container that auto-arranges children
1914
1919
  * @param {Vector2} [pos]
1915
- * @param {number} [columns=1] - Number of columns (1 = vertical list)
1916
- * @param {number} [gap=10] - Space between children
1917
- * @param {number} [padding=10] - Space between container border and children
1918
- * @param {boolean} [transparent=false] - If true, draws no background, outline, or shadow
1920
+ * @param {number} [columns] - Number of columns (1 = vertical list)
1921
+ * @param {number} [gap] - Space between children
1922
+ * @param {number} [padding] - Space between container border and children
1923
+ * @param {boolean} [transparent] - If true, draws no background, outline, or shadow
1919
1924
  */
1920
1925
  constructor(pos, columns=1, gap=10, padding=10, transparent=false)
1921
1926
  {
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.24.2';
35
+ const engineVersion = '1.25.0';
36
36
 
37
37
  /** Frames per second to update
38
38
  * @type {number}
@@ -165,7 +165,7 @@ let timeFixedStart = 0, frameFixedStart = 0;
165
165
  let windowWidthLast = 0, windowHeightLast = 0, windowPixelRatioLast = 0;
166
166
  let engineUpdateInternal; // assigned by engineInit so engineStep can drive it
167
167
  let engineFrameScheduled = false; // a frame of the loop is asked for and has not run yet
168
- let engineFrameErrors = 0; // errors a release build's loop went on past
168
+ let engineFrameErrorLast; // the last error a release build's loop went on past, as text
169
169
 
170
170
  // the pairs of objects asked about a collision this update, so the other's own physics does not ask again: each
171
171
  // asker's others, with true for a pair both said to resolve, and false for one left overlapping, ignored or only
@@ -319,7 +319,8 @@ function engineLoadingScreenDraw(elapsed)
319
319
  * ()=> { drawHUD(); }, // gameRenderPost
320
320
  * ['tiles.png', 'tilesLevel.png'] // images to load
321
321
  * );
322
- * @memberof Engine */
322
+ * @memberof Engine
323
+ * @return {Promise<void>} - Done when the images have loaded and gameInit has run */
323
324
  async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, imageSources=[], rootElement)
324
325
  {
325
326
  showEngineVersion && console.log(`${engineName} Engine v${engineVersion}`);
@@ -419,20 +420,30 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
419
420
  frameTimeBufferMS = 0;
420
421
  }
421
422
 
422
- // update multiple frames if necessary in case of slow framerate
423
- for (; frameTimeBufferMS >= 0; frameTimeBufferMS -= 1e3 / frameRate)
423
+ // update multiple frames if necessary in case of slow framerate; a tick that throws still spends its time
424
+ // and the smoothing still comes back, so a release build going on past an error each tick keeps its rate
425
+ try
424
426
  {
425
- // read again each tick, so a pause set by the game stops the rest of this frame's catch-up ticks
426
- const frozenTick = paused || !(timeScale * debugScale);
427
-
428
- // increment frame and update time, frozen does not advance time
429
- if (!frozenTick)
430
- time = timeFixedStart + (frame++ - frameFixedStart) / frameRate;
431
- engineTick(frozenTick);
427
+ while (frameTimeBufferMS >= 0)
428
+ {
429
+ try
430
+ {
431
+ // read again each tick, so a pause set by the game stops the rest of this frame's catch-up ticks
432
+ const frozenTick = paused || !(timeScale * debugScale);
433
+
434
+ // increment frame and update time, frozen does not advance time
435
+ if (!frozenTick)
436
+ time = timeFixedStart + (frame++ - frameFixedStart) / frameRate;
437
+ engineTick(frozenTick);
438
+ }
439
+ finally { frameTimeBufferMS -= 1e3 / frameRate; }
440
+ }
441
+ }
442
+ finally
443
+ {
444
+ // add the time smoothing back in
445
+ frameTimeBufferMS += deltaSmooth;
432
446
  }
433
-
434
- // add the time smoothing back in
435
- frameTimeBufferMS += deltaSmooth;
436
447
  }
437
448
 
438
449
  // one tick of the loop: update game and objects, when frozen update everything except them
@@ -729,9 +740,13 @@ function engineScheduleFrame()
729
740
  try { engineUpdateInternal(frameTimeMS); }
730
741
  catch (error)
731
742
  {
732
- // a release build goes on past an error in a frame, a frozen game is the worst a player can get; only the
733
- // first is logged, one every frame would flood the console
734
- engineFrameErrors++ || console.error(error);
743
+ // a release build goes on past an error in a frame, a frozen game is the worst a player can get; an error
744
+ // is logged when it is not the last one again, one every frame would flood the console
745
+ const text = String(error);
746
+ text === engineFrameErrorLast || console.error(error);
747
+ engineFrameErrorLast = text;
748
+ // the frame's input is cleared as its tick would have, or a key press that threw would be pressed again
749
+ inputUpdatePost();
735
750
  engineScheduleFrame();
736
751
  }
737
752
  };
@@ -805,9 +820,12 @@ function engineObjectsUpdate()
805
820
  const start = engineChildStack.length;
806
821
  for (const child of children)
807
822
  engineChildStack.push(child);
808
- for (let i = start; i < engineChildStack.length; ++i)
809
- updateChildObject(engineChildStack[i]);
810
- engineChildStack.length = start;
823
+ try
824
+ {
825
+ for (let i = start; i < engineChildStack.length; ++i)
826
+ updateChildObject(engineChildStack[i]);
827
+ }
828
+ finally { engineChildStack.length = start; } // put back when an update throws too, or it keeps them
811
829
  }
812
830
  const pass = engineObjectsUpdateCount;
813
831
  function updateChildObject(o)