littlejsengine 1.24.3 → 1.26.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 (53) hide show
  1. package/CHANGELOG.md +123 -0
  2. package/EDITOR.md +2 -2
  3. package/FAQ.md +55 -11
  4. package/README.md +6 -2
  5. package/REFERENCE.md +203 -57
  6. package/dist/littlejs.d.ts +933 -313
  7. package/dist/littlejs.esm.js +5143 -2419
  8. package/dist/littlejs.esm.min.js +38 -24
  9. package/dist/littlejs.js +5132 -2416
  10. package/dist/littlejs.min.js +37 -23
  11. package/dist/littlejs.release.js +4905 -2232
  12. package/package.json +4 -4
  13. package/plugins/box2d.js +141 -56
  14. package/plugins/drawUtilities.js +374 -380
  15. package/plugins/gltf.js +383 -60
  16. package/plugins/lightSystem.js +412 -30
  17. package/plugins/math3d.js +1015 -1014
  18. package/plugins/medalSystem.js +19 -7
  19. package/plugins/newgrounds.js +5 -6
  20. package/plugins/parallax.js +1 -0
  21. package/plugins/particleEffects.js +47 -14
  22. package/plugins/pathFinder.js +60 -38
  23. package/plugins/pluginExport.js +11 -0
  24. package/plugins/postProcess.js +200 -67
  25. package/plugins/render3d.js +493 -2391
  26. package/plugins/render3dDebug.js +5 -8
  27. package/plugins/render3dEditor.js +84 -9
  28. package/plugins/render3dExtras.js +122 -33
  29. package/plugins/render3dLevel.js +67 -8
  30. package/plugins/render3dMesh.js +1158 -0
  31. package/plugins/render3dObject.js +1451 -0
  32. package/plugins/render3dVoxels.js +15 -3
  33. package/plugins/textureSheet.js +9 -8
  34. package/plugins/threejs.js +1 -1
  35. package/plugins/tweenSystem.js +117 -87
  36. package/plugins/uiSystem.js +62 -28
  37. package/plugins/wavedash.js +229 -0
  38. package/src/engine.js +192 -40
  39. package/src/engineAudio.js +47 -27
  40. package/src/engineBuild.mjs +6 -1
  41. package/src/engineDebug.js +23 -162
  42. package/src/engineDraw.js +148 -43
  43. package/src/engineEditor.js +153 -40
  44. package/src/engineExport.js +0 -3
  45. package/src/engineInput.js +31 -25
  46. package/src/engineMath.js +38 -20
  47. package/src/engineObject.js +68 -50
  48. package/src/engineParticles.js +66 -18
  49. package/src/engineRelease.js +18 -15
  50. package/src/engineSettings.js +48 -9
  51. package/src/engineTileLayer.js +44 -18
  52. package/src/engineUtilities.js +56 -11
  53. package/src/engineWebGL.js +68 -22
@@ -88,9 +88,11 @@ class VoxelMap extends EngineObject3D
88
88
  this.chunkTransparentMeshes = []; // each chunk's transparent blocks, drawn blended
89
89
  /** @type {Array<Vector3>} */
90
90
  this.chunkCenters = []; // a chunk mesh's points are around its center, which it is drawn at
91
+ /** @type {Set<number>} */
91
92
  this.chunksChanged = new Set;
92
93
  /** @type {Array<{faces: Array<number>, seeThrough: boolean, transparent: boolean, doubleSided: boolean}|undefined>} */
93
94
  this.blockTypes = [];
95
+ /** @type {Map<number, Array<Vector2>>} */
94
96
  this.tiles = new Map; // tile index to the uvs of its corners
95
97
 
96
98
  // the transparent blocks draw in the transparent stage, blended and sorted, through a child that draws them
@@ -98,6 +100,7 @@ class VoxelMap extends EngineObject3D
98
100
  glass.size3D = vec3(); // not a thing to collect either
99
101
  glass.transparent = true;
100
102
  glass.render3D = ()=> this.renderChunks(true);
103
+ render3DShareSettings(glass, this); // it draws as the map does, its material, stage and render order
101
104
  this.addChild(glass);
102
105
  render3DLevel.push(this);
103
106
  }
@@ -473,15 +476,24 @@ class VoxelMap extends EngineObject3D
473
476
  levelRaycast3D(ray) { return this.raycast(ray)?.distance; }
474
477
 
475
478
  /** Where a short move goes into a block, for a particle's move in one frame: the part of the move made before it,
476
- * 0 to 1, and the normal of the face it comes in through; undefined when it hits none, or starts inside one
479
+ * 0 to 1, and the normal of the face it comes in through; undefined when it hits none, or starts inside one; a
480
+ * move that starts on a block's face, coming in, hits it at 0
477
481
  * @param {Vector3} from
478
482
  * @param {Vector3} to
479
483
  * @return {{distance: number, normal: Vector3}|undefined}
480
484
  * @ignore */
481
485
  levelSegment3D(from, to)
482
486
  {
483
- const hit = this.raycast(new Ray3D(from, to.subtract(from)), 1);
484
- return hit && hit.distance > 0 ? {distance: hit.distance, normal: hit.normal} : undefined;
487
+ const move = to.subtract(from), hit = this.raycast(new Ray3D(from, move), 1);
488
+ if (!hit) return;
489
+ if (!hit.distance)
490
+ {
491
+ // met at its start: on a face when the point just behind the start is clear, inside a block when not
492
+ const behind = from.subtract(move.normalize(1e-6)).subtract(this.pos3D);
493
+ if (this.voxelAt(floor(behind.x), floor(behind.y), floor(behind.z)))
494
+ return;
495
+ }
496
+ return {distance: hit.distance, normal: hit.normal};
485
497
  }
486
498
 
487
499
  /** Keeps an eye on its placement, called automatically each frame */
@@ -263,7 +263,7 @@ function loadSprite(src, frameSize, padding=textureSheetPadding, sourcePadding=0
263
263
  console.warn('loadSprite failed to load image:', src);
264
264
  textureSheetLoadFailed(key, tileInfo);
265
265
  }
266
- });
266
+ }, ()=> textureSheetLoadFailed(key, tileInfo));
267
267
 
268
268
  return tileInfo;
269
269
  }
@@ -329,7 +329,7 @@ function loadTiles(sources, tileSize=tileDefaultSize, padding=textureSheetPaddin
329
329
  added.sheet.drawImage(image, added.tile, false); // upload once per batch
330
330
  for (let k = 0; k < count; ++k)
331
331
  set.tiles.push(added.tile.frame(k));
332
- });
332
+ }, ()=> textureSheetLoadFailed(key, set));
333
333
  }
334
334
  return set;
335
335
  }
@@ -434,20 +434,21 @@ function loadAtlas(imageSrc, jsonSrc, padding=textureSheetPadding)
434
434
  console.warn('loadAtlas failed to load:', imageSrc, jsonSrc);
435
435
  textureSheetLoadFailed(key, atlas);
436
436
  }
437
- });
437
+ }, ()=> textureSheetLoadFailed(key, atlas));
438
438
 
439
439
  return atlas;
440
440
  }
441
441
 
442
- // run a load in the queue: a load that throws is reported and the loads after it carry on, the pending count
443
- // always comes back down, and the sheets upload to webgl once per batch, when the last pending load finishes
444
- function textureSheetQueueJob(name, job)
442
+ // run a load in the queue: a load that throws is reported, let go of so the next call loads it again (failed), as
443
+ // one whose file did not load is, and the loads after it carry on; the pending count always comes back down, and the
444
+ // sheets upload to webgl once per batch, when the last pending load finishes
445
+ function textureSheetQueueJob(name, job, failed)
445
446
  {
446
447
  ++textureSheetPendingCount;
447
448
  textureSheetQueue = textureSheetQueue.then(async ()=>
448
449
  {
449
450
  try { await job(); }
450
- catch (e) { console.error(name + ' failed:', e); }
451
+ catch (e) { console.error(name + ' failed:', e); failed?.(); }
451
452
  finally
452
453
  {
453
454
  if (!--textureSheetPendingCount)
@@ -464,7 +465,7 @@ function textureSheetQueueJob(name, job)
464
465
  * - Accepts TexturePacker json (hash and array) and Aseprite json
465
466
  * - Frames tagged in Aseprite or named like run_0, run_1 group into animations
466
467
  * @param {Object} data - Parsed atlas json data
467
- * @return {Array<Object>} List of {name, frames} groups in atlas order
468
+ * @return {Array<{name: string, frames: Array<Object>}>} List of {name, frames} groups in atlas order
468
469
  * @memberof TextureSheets */
469
470
  function parseAtlas(data)
470
471
  {
@@ -65,7 +65,7 @@ class ThreeJSPlugin
65
65
  rootElement.insertBefore(threeCanvas, rootElement.firstChild);
66
66
  threeCanvas.style.cssText = mainCanvas.style.cssText;
67
67
 
68
- // composite the 3D canvas into screenshots and video capture
68
+ // composite the 3D canvas into screenshots
69
69
  setBackgroundCanvas(threeCanvas);
70
70
 
71
71
  // render automatically each frame after the engine renders
@@ -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,13 +23,23 @@ 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
+ tween.listed || tweenActive.push(tween); // one stopped this update is still listed, in its place
27
+ tween.listed = true;
26
28
  }
27
- function tweenDeactivate(tween)
29
+ function tweenDeactivate(tween) { tween.active = false; }
30
+
31
+ // call a tween's callback with its value at life; one that throws, its easing too, is ended, or it would be called and
32
+ // throw at every update, unless the callback itself started it again, which is its own new run (pass, the update it
33
+ // is in, undefined outside one)
34
+ function tweenCall(tween, life, pass)
28
35
  {
29
- if (!tween.active) return;
30
- tween.active = false;
31
- tweenActive.splice(tweenActive.indexOf(tween), 1);
36
+ try { tween.callback(tween.interp(life)); }
37
+ catch (error)
38
+ {
39
+ if (tween.active && (pass === undefined || tween.activePass < pass))
40
+ tweenDeactivate(tween);
41
+ throw error;
42
+ }
32
43
  }
33
44
 
34
45
  // True if the value is an instance of a class that exposes a numeric-percent
@@ -57,9 +68,9 @@ class Tween
57
68
  * callback receives the interpolated value (a number, or a fresh instance
58
69
  * for lerp-able types). Both endpoints must be the same type.
59
70
  * @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
71
+ * @param {T} [start] - Starting value
72
+ * @param {T} [end] - Ending value
73
+ * @param {number} [duration] - Duration in seconds
63
74
  * @param {Object} [options]
64
75
  * @param {function(number):number} [options.ease] - Easing function (defaults to LINEAR)
65
76
  * @param {boolean} [options.useRealTime=false] - Advance even when the game is paused (matches Timer's useRealTime)
@@ -105,15 +116,18 @@ class Tween
105
116
  * @type {undefined|function():void} */
106
117
  this.onComplete = undefined;
107
118
 
108
- /** Continuation when a pass ends, set by loop() and pingPong() to start the next iteration.
119
+ /** Continuation when a pass ends, set by loop() and pingPong() to start the next iteration
109
120
  * @private */
110
121
  this.thenCallback = undefined;
111
- /** Remaining iterations including the current run (loop/pingPong only).
122
+ /** Remaining iterations including the current run (loop/pingPong only)
112
123
  * @private */
113
124
  this.loopRemaining = 0;
114
- /** Whether it is in the active list, see isActive
125
+ /** Whether it is running, see isActive
115
126
  * @private */
116
127
  this.active = false;
128
+ /** Whether it is in the active list, which a stopped one leaves at the next update
129
+ * @private */
130
+ this.listed = false;
117
131
  /** The update it was started in, it first moves on the one after
118
132
  * @private */
119
133
  this.activePass = 0;
@@ -128,13 +142,13 @@ class Tween
128
142
  this.target = undefined;
129
143
 
130
144
  tweenActivate(this);
131
- // Snap target to start immediately.
132
- callback(this.interp(duration));
145
+ // Snap target to start immediately; one that throws here is no tween, the game never gets it to stop
146
+ tweenCall(this, duration);
133
147
  }
134
148
 
135
- /** Set the easing curve and return this for chaining.
149
+ /** Set the easing curve and return this for chaining
136
150
  * @param {function(number):number} easeFn
137
- * @returns {Tween<T>} */
151
+ * @return {Tween<T>} */
138
152
  setEase(easeFn)
139
153
  {
140
154
  this.ease = easeFn;
@@ -149,7 +163,7 @@ class Tween
149
163
  * - It is kept by `restart`, so a restarted tween calls it again when it completes
150
164
  * - `stop` and `tweenStopAll` end a tween without calling it
151
165
  * @param {function():void} callback
152
- * @returns {Tween<T>} */
166
+ * @return {Tween<T>} */
153
167
  then(callback)
154
168
  {
155
169
  this.onComplete = callback;
@@ -165,7 +179,7 @@ class Tween
165
179
  * A `then` callback, set before or after, is called when the last
166
180
  * iteration ends.
167
181
  * @param {number} [count=Infinity]
168
- * @returns {Tween<T>} */
182
+ * @return {Tween<T>} */
169
183
  loop(count = Infinity)
170
184
  {
171
185
  this.loopRemaining = count;
@@ -180,7 +194,7 @@ class Tween
180
194
  * A `then` callback, set before or after, is called when the last
181
195
  * iteration ends.
182
196
  * @param {number} [count=Infinity]
183
- * @returns {Tween<T>} */
197
+ * @return {Tween<T>} */
184
198
  pingPong(count = Infinity)
185
199
  {
186
200
  this.loopRemaining = count;
@@ -188,10 +202,10 @@ class Tween
188
202
  return this;
189
203
  }
190
204
 
191
- /** Pause this tween. While paused, tweenUpdate skips it. */
205
+ /** Pause this tween. While paused, tweenUpdate skips it */
192
206
  pause() { this.paused = true; }
193
207
 
194
- /** Resume a paused tween. */
208
+ /** Resume a paused tween */
195
209
  resume() { this.paused = false; }
196
210
 
197
211
  /** Reset this tween to the start: life back to duration, pause cleared,
@@ -209,11 +223,11 @@ class Tween
209
223
  this.lastTime = time;
210
224
  this.lastTimeReal = timeReal;
211
225
  tweenActivate(this);
212
- this.callback(this.interp(this.duration));
226
+ tweenCall(this, this.duration);
213
227
  }
214
228
 
215
- /** True if this tween is in the active list and not paused.
216
- * @returns {boolean} */
229
+ /** True if this tween is in the active list and not paused
230
+ * @return {boolean} */
217
231
  isActive()
218
232
  {
219
233
  return !this.paused && this.active;
@@ -221,7 +235,7 @@ class Tween
221
235
 
222
236
  /** Get how far this tween has progressed, from 0 (just started) to 1
223
237
  * (completed). Clamped — overshoot past completion still reads 1.
224
- * @returns {number} */
238
+ * @return {number} */
225
239
  getPercent()
226
240
  {
227
241
  return percent(this.duration - this.life, 0, this.duration);
@@ -230,7 +244,7 @@ class Tween
230
244
  /** Get the current interpolated value (the value most recently passed to
231
245
  * the callback). Returns a number, Vector2, Vector3 or Color depending on the
232
246
  * tween's start/end types.
233
- * @returns {T} */
247
+ * @return {T} */
234
248
  getValue()
235
249
  {
236
250
  return this.interp(this.life);
@@ -242,7 +256,7 @@ class Tween
242
256
  * - A vector goes past its ends as far as the easing does, as a number does; a Color stays between them,
243
257
  * so its channels stay in range, and any other type goes as far as its own lerp takes it
244
258
  * @param {number} life
245
- * @returns {T} */
259
+ * @return {T} */
246
260
  interp(life)
247
261
  {
248
262
  // the ends of whatever type it tweens, each kind is handled below
@@ -283,46 +297,46 @@ class Tween
283
297
  */
284
298
  const Ease =
285
299
  {
286
- /** Linear (identity) curve.
300
+ /** Linear (identity) curve
287
301
  * @param {number} x
288
- * @returns {number}
302
+ * @return {number}
289
303
  * @memberof TweenSystem.Ease */
290
304
  LINEAR: (x) => x,
291
305
 
292
306
  /** Power curve factory: `Ease.POWER(n)` returns `x => x**n`.
293
307
  * Use n=2 for quadratic, n=3 for cubic, etc.
294
308
  * @param {number} n
295
- * @returns {function(number):number}
309
+ * @return {function(number):number}
296
310
  * @memberof TweenSystem.Ease */
297
311
  POWER: (n) => (x) => x ** n,
298
312
 
299
- /** Sine ease-in curve: starts slow, ends fast.
313
+ /** Sine ease-in curve: starts slow, ends fast
300
314
  * @param {number} x
301
- * @returns {number}
315
+ * @return {number}
302
316
  * @memberof TweenSystem.Ease */
303
317
  SINE: (x) => 1 - cos(x * (PI / 2)),
304
318
 
305
- /** Circular ease-in curve.
319
+ /** Circular ease-in curve
306
320
  * @param {number} x
307
- * @returns {number}
321
+ * @return {number}
308
322
  * @memberof TweenSystem.Ease */
309
323
  CIRC: (x) => 1 - (1 - x * x)**.5,
310
324
 
311
- /** Exponential ease-in curve (`2^(10x-10)`).
325
+ /** Exponential ease-in curve (`2^(10x-10)`)
312
326
  * @param {number} x
313
- * @returns {number}
327
+ * @return {number}
314
328
  * @memberof TweenSystem.Ease */
315
329
  EXPO: (x) => x === 0 ? 0 : 2 ** (10 * x - 10),
316
330
 
317
- /** Back ease-in: overshoots backward at the start before snapping forward.
331
+ /** Back ease-in: overshoots backward at the start before snapping forward
318
332
  * @param {number} x
319
- * @returns {number}
333
+ * @return {number}
320
334
  * @memberof TweenSystem.Ease */
321
335
  BACK: (x) => x * x * (2.70158 * x - 1.70158),
322
336
 
323
- /** Elastic ease-in: oscillations that grow toward the end.
337
+ /** Elastic ease-in: oscillations that grow toward the end
324
338
  * @param {number} x
325
- * @returns {number}
339
+ * @return {number}
326
340
  * @memberof TweenSystem.Ease */
327
341
  ELASTIC: (x) =>
328
342
  x === 0 ? 0 :
@@ -332,7 +346,7 @@ const Ease =
332
346
  /** Spring ease-in: wobbles around the start before springing to the end;
333
347
  * `Ease.OUT(Ease.SPRING)` overshoots and settles on the target.
334
348
  * @param {number} x
335
- * @returns {number}
349
+ * @return {number}
336
350
  * @memberof TweenSystem.Ease */
337
351
  SPRING: (x) =>
338
352
  1 -
@@ -346,7 +360,7 @@ const Ease =
346
360
  * classic "object falls and hits the ground" shape (bounces near x=1),
347
361
  * wrap with `Ease.OUT`: `Ease.OUT(Ease.BOUNCE)`.
348
362
  * @param {number} x
349
- * @returns {number}
363
+ * @return {number}
350
364
  * @memberof TweenSystem.Ease
351
365
  * @example
352
366
  * Ease.BOUNCE // ease-in bounce (bouncy at start)
@@ -369,7 +383,7 @@ const Ease =
369
383
  * convention, so wrapping a curve in `IN` is a no-op — useful when
370
384
  * picking the direction programmatically.
371
385
  * @param {function(number):number} f - Curve to use as ease-in (returned unchanged)
372
- * @returns {function(number):number}
386
+ * @return {function(number):number}
373
387
  * @memberof TweenSystem.Ease
374
388
  * @example
375
389
  * // Pick direction at runtime
@@ -380,16 +394,16 @@ const Ease =
380
394
 
381
395
  /** Reverse a curve so it eases out instead of in: `x => 1 - f(1 - x)`.
382
396
  * @param {function(number):number} f
383
- * @returns {function(number):number}
397
+ * @return {function(number):number}
384
398
  * @memberof TweenSystem.Ease
385
399
  * @example
386
400
  * Ease.OUT(Ease.POWER(2)) // ease-out quadratic
387
401
  */
388
402
  OUT: (f) => (x) => 1 - f(1 - x),
389
403
 
390
- /** Combine the first half of `f` with `Ease.OUT(f)` for a symmetric curve.
404
+ /** Combine the first half of `f` with `Ease.OUT(f)` for a symmetric curve
391
405
  * @param {function(number):number} f
392
- * @returns {function(number):number}
406
+ * @return {function(number):number}
393
407
  * @memberof TweenSystem.Ease */
394
408
  IN_OUT: (f) => Ease.PIECEWISE(f, Ease.OUT(f)),
395
409
 
@@ -397,7 +411,7 @@ const Ease =
397
411
  * Each curve is mapped to its section: section i runs over [i/n, (i+1)/n]
398
412
  * and its output is mapped to [i/n, (i+1)/n] of the overall range.
399
413
  * @param {...function(number):number} fns
400
- * @returns {function(number):number}
414
+ * @return {function(number):number}
401
415
  * @memberof TweenSystem.Ease */
402
416
  PIECEWISE: (...fns) =>
403
417
  {
@@ -415,7 +429,7 @@ const Ease =
415
429
  * @param {number} y1
416
430
  * @param {number} x2
417
431
  * @param {number} y2
418
- * @returns {function(number):number}
432
+ * @return {function(number):number}
419
433
  * @memberof TweenSystem.Ease
420
434
  * @example
421
435
  * Ease.BEZIER(0.25, 0.1, 0.25, 1) // CSS "ease"
@@ -465,12 +479,12 @@ const Ease =
465
479
  * @param {string} propertyPath - Dot-separated path, e.g. `'pos.x'` or `'color'`
466
480
  * @param {T} start - Starting value
467
481
  * @param {T} end - Ending value
468
- * @param {number} [duration=1] - Duration in seconds
482
+ * @param {number} [duration] - Duration in seconds
469
483
  * @param {Object} [options] - Same options as the Tween constructor
470
484
  * @param {function(number):number} [options.ease] - Easing function (defaults to LINEAR)
471
485
  * @param {boolean} [options.useRealTime=false] - Advance even when the game is paused
472
486
  * @param {boolean} [options.paused=false] - Start in paused state
473
- * @returns {Tween<T>}
487
+ * @return {Tween<T>}
474
488
  * @memberof TweenSystem
475
489
  * @example
476
490
  * // Numeric: slide an object's x with an ease-out sine curve
@@ -527,7 +541,7 @@ function tweenNextIteration(tween, passed, continuation)
527
541
  tweenCarryOvershoot(tween);
528
542
  tween.thenCallback = continuation;
529
543
  tweenActivate(tween);
530
- tween.callback(tween.interp(tween.life)); // snap to where the new iteration is
544
+ tweenCall(tween, tween.life); // snap to where the new iteration is
531
545
  return true;
532
546
  }
533
547
 
@@ -591,52 +605,68 @@ function tweenUpdate(gameDelta, realDelta)
591
605
  // from the next update. Newest first, as the list has always been walked.
592
606
  // a callback that calls tweenUpdate itself gets a list of its own, the outer update is still walking this one
593
607
  const list = tweenUpdateList.length ? [] : tweenUpdateList, pass = ++tweenUpdatePass;
608
+ let kept = 0;
594
609
  for (const t of tweenActive)
595
- list.push(t);
610
+ if (t.active)
611
+ list.push(tweenActive[kept++] = t);
612
+ else
613
+ t.listed = false; // stopped since the last update, out of the list now
614
+ tweenActive.length = kept;
596
615
  // the list is let go however the walk ends: a callback that throws must not leave it held, or every
597
616
  // update after would take it for an update still going and make a list of its own
617
+ let failed;
598
618
  try
599
619
  {
600
620
  for (let i = list.length; i--;)
601
621
  {
602
622
  const t = list[i];
603
- // stopped, or started again by a callback this update, or during an update a callback ran inside it, which
604
- // counts on from this one
605
- if (!t.active || t.activePass >= pass) continue;
606
- let dt;
607
- if (enginePath)
608
- {
609
- // a paused tween keeps count too, so it does not jump when resumed
610
- dt = t.useRealTime ? timeReal - t.lastTimeReal : time - t.lastTime;
611
- t.lastTime = time;
612
- t.lastTimeReal = timeReal;
613
- }
614
- else
615
- dt = t.useRealTime ? realDelta : gameDelta;
616
- if (t.target?.destroyed) { t.stop(); continue; } // its object is gone, paused or not
617
- if (t.paused || dt <= 0) continue;
618
-
619
- t.life -= dt;
620
- if (t.life > 1e-9) // the engine's deltas add up a rounding error short of the duration
621
- {
622
- t.callback(t.interp(t.life));
623
- }
624
- else
625
- {
626
- // Completion: fire end value, remove from active, start the next iteration
627
- // of a loop or pingPong, or when there is none it has completed, fire onComplete
628
- t.callback(t.interp(0));
629
- if (!t.active || t.activePass >= pass)
630
- continue; // stopped or restarted by its own callback, the run it was on ends without completing
631
- tweenDeactivate(t);
632
- const next = t.thenCallback;
633
- t.thenCallback = undefined;
634
- if (!(next && next()) && t.onComplete)
635
- t.onComplete();
636
- }
623
+ // one that throws does not stop the others: its error comes out once they have all moved
624
+ try { tweenStep(t, pass, enginePath, gameDelta, realDelta); }
625
+ catch (error) { failed ??= {error}; }
637
626
  }
638
627
  }
639
628
  finally { list.length = 0; }
629
+ if (failed)
630
+ throw failed.error;
631
+ }
632
+
633
+ // move one tween by an update, as tweenUpdate walks them
634
+ function tweenStep(t, pass, enginePath, gameDelta, realDelta)
635
+ {
636
+ // stopped, or started again by a callback this update, or during an update a callback ran inside it, which
637
+ // counts on from this one
638
+ if (!t.active || t.activePass >= pass) return;
639
+ let dt;
640
+ if (enginePath)
641
+ {
642
+ // a paused tween keeps count too, so it does not jump when resumed
643
+ dt = t.useRealTime ? timeReal - t.lastTimeReal : time - t.lastTime;
644
+ t.lastTime = time;
645
+ t.lastTimeReal = timeReal;
646
+ }
647
+ else
648
+ dt = t.useRealTime ? realDelta : gameDelta;
649
+ if (t.target?.destroyed) { t.stop(); return; } // its object is gone, paused or not
650
+ if (t.paused || dt <= 0) return;
651
+
652
+ t.life -= dt;
653
+ if (t.life > 1e-9) // the engine's deltas add up a rounding error short of the duration
654
+ {
655
+ tweenCall(t, t.life, pass);
656
+ }
657
+ else
658
+ {
659
+ // Completion: fire end value, remove from active, start the next iteration
660
+ // of a loop or pingPong, or when there is none it has completed, fire onComplete
661
+ tweenCall(t, 0, pass); // one whose last call throws is ended all the same
662
+ if (!t.active || t.activePass >= pass)
663
+ return; // stopped or restarted by its own callback, the run it was on ends without completing
664
+ tweenDeactivate(t);
665
+ const next = t.thenCallback;
666
+ t.thenCallback = undefined;
667
+ if (!(next && next()) && t.onComplete)
668
+ t.onComplete();
669
+ }
640
670
  }
641
671
 
642
672
  /** Stop every active tween, ending loops too, without calling their then-callbacks.
@@ -645,7 +675,7 @@ function tweenUpdate(gameDelta, realDelta)
645
675
  function tweenStopAll()
646
676
  {
647
677
  for (const t of tweenActive)
648
- t.thenCallback = undefined, t.active = false;
678
+ t.thenCallback = undefined, t.active = t.listed = false;
649
679
  tweenActive.length = 0;
650
680
  }
651
681