littlejsengine 1.18.1 → 1.18.4

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.
@@ -0,0 +1,509 @@
1
+ /**
2
+ * LittleJS Tween System Plugin
3
+ * - Lightweight tweens for numbers, Vector2, Color, or any .lerp-able type
4
+ * - Chainable easing, looping, and ping-pong
5
+ * - Property-path helper for the common case of animating an object field
6
+ * - Auto-updates via engineAddPlugin; pauses with the game by default
7
+ * @namespace TweenSystem
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ ///////////////////////////////////////////////////////////////////////////////
13
+
14
+ // Module-private list of tweens currently running.
15
+ const tweenActive = [];
16
+
17
+ // Time tracking for delta computation between engine plugin calls.
18
+ let lastTime = 0;
19
+ let lastTimeReal = 0;
20
+
21
+ // True if the value is an instance of a class that exposes a numeric-percent
22
+ // `lerp(other, percent)` method (Vector2, Color, or any future class).
23
+ function isLerpable(v) { return v && typeof v.lerp === 'function'; }
24
+
25
+ ///////////////////////////////////////////////////////////////////////////////
26
+
27
+ /** A numeric tween: drives a callback with a value interpolated between
28
+ * `start` and `end` over `duration` seconds. Pauses with the game by default.
29
+ * @memberof TweenSystem
30
+ * @example
31
+ * // Animate a fade-out over 2 seconds with an ease-out sine curve.
32
+ * new Tween((v) => obj.alpha = v, 1, 0, 2, { ease: Ease.OUT(Ease.SINE) });
33
+ */
34
+ class Tween
35
+ {
36
+ /** Create a new tween. The callback fires immediately with `start` so the
37
+ * target snaps to the start value on the same frame the tween is created.
38
+ *
39
+ * `start` and `end` may be numbers, Vector2 instances, Color instances, or
40
+ * any object exposing a `lerp(other, percent) => sameType` method. The
41
+ * callback receives the interpolated value (a number, or a fresh instance
42
+ * for lerp-able types). Both endpoints must be the same type.
43
+ * @param {function(number|Vector2|Color):void} callback - Called with the interpolated value each frame
44
+ * @param {number|Vector2|Color} [start=0] - Starting value
45
+ * @param {number|Vector2|Color} [end=1] - Ending value
46
+ * @param {number} [duration=1] - Duration in seconds
47
+ * @param {Object} [options]
48
+ * @param {function(number):number} [options.ease] - Easing function (defaults to LINEAR)
49
+ * @param {boolean} [options.useRealTime=false] - Advance even when the game is paused (matches Timer's useRealTime)
50
+ * @param {boolean} [options.paused=false] - Start in paused state */
51
+ constructor(callback, start = 0, end = 1, duration = 1, options = {})
52
+ {
53
+ ASSERT(typeof callback === 'function', 'Tween callback must be a function');
54
+ if (isLerpable(start))
55
+ {
56
+ ASSERT(start.constructor === end.constructor,
57
+ 'Tween start and end must be the same type');
58
+ }
59
+ else
60
+ {
61
+ ASSERT(isNumber(start), 'Tween start must be a number or have a .lerp method');
62
+ ASSERT(isNumber(end), 'Tween end must be a number when start is a number');
63
+ }
64
+ ASSERT(isNumber(duration) && duration > 0, 'Tween duration must be > 0');
65
+
66
+ this.callback = callback;
67
+ this.start = start;
68
+ this.end = end;
69
+ this.duration = duration;
70
+ this.life = duration;
71
+ this.ease = options.ease || Ease.LINEAR;
72
+ this.useRealTime = !!options.useRealTime;
73
+ this.paused = !!options.paused;
74
+
75
+ /** @private completion callback set by then(), loop(), pingPong(). */
76
+ this.thenCallback = undefined;
77
+ /** @private remaining iterations including the current run (loop/pingPong only). */
78
+ this.loopRemaining = 0;
79
+
80
+ tweenActive.push(this);
81
+ // Snap target to start immediately.
82
+ callback(this.interp(duration));
83
+ }
84
+
85
+ /** Set the easing curve and return this for chaining.
86
+ * @param {function(number):number} easeFn
87
+ * @returns {Tween}
88
+ * @memberof TweenSystem */
89
+ setEase(easeFn)
90
+ {
91
+ this.ease = easeFn;
92
+ return this;
93
+ }
94
+
95
+ /** Set a single completion callback. Calling `then` again replaces the
96
+ * previous callback. Returns this for chaining.
97
+ *
98
+ * Calling `then` after `loop` or `pingPong` overrides the loop chain
99
+ * (last call wins).
100
+ * @param {function():void} callback
101
+ * @returns {Tween}
102
+ * @memberof TweenSystem */
103
+ then(callback)
104
+ {
105
+ this.thenCallback = callback;
106
+ this.loopRemaining = 0;
107
+ return this;
108
+ }
109
+
110
+ /** Repeat this tween `n` total times. After each iteration finishes, a
111
+ * fresh tween with the same parameters takes over via the `then` slot.
112
+ * `loop()` with no argument loops forever.
113
+ *
114
+ * Mutually exclusive with `pingPong`; calling either replaces the other,
115
+ * and calling `then` after either clears the loop (last call wins).
116
+ * @param {number} [count=Infinity]
117
+ * @returns {Tween}
118
+ * @memberof TweenSystem */
119
+ loop(count = Infinity)
120
+ {
121
+ this.loopRemaining = count;
122
+ this.thenCallback = () => loopContinuation(this);
123
+ return this;
124
+ }
125
+
126
+ /** Like `loop`, but swap `start` and `end` between iterations so the value
127
+ * bounces back and forth. `pingPong()` with no argument bounces forever.
128
+ *
129
+ * Mutually exclusive with `loop`; calling either replaces the other, and
130
+ * calling `then` after either clears the loop (last call wins).
131
+ * @param {number} [count=Infinity]
132
+ * @returns {Tween}
133
+ * @memberof TweenSystem */
134
+ pingPong(count = Infinity)
135
+ {
136
+ this.loopRemaining = count;
137
+ this.thenCallback = () => pingPongContinuation(this);
138
+ return this;
139
+ }
140
+
141
+ /** Pause this tween. While paused, tweenUpdate skips it.
142
+ * @memberof TweenSystem */
143
+ pause() { this.paused = true; }
144
+
145
+ /** Resume a paused tween.
146
+ * @memberof TweenSystem */
147
+ resume() { this.paused = false; }
148
+
149
+ /** Reset this tween to the start: life back to duration, pause cleared,
150
+ * re-added to the active list if previously stopped, and the callback
151
+ * re-fired with the start value.
152
+ * @memberof TweenSystem */
153
+ restart()
154
+ {
155
+ this.life = this.duration;
156
+ this.paused = false;
157
+ if (tweenActive.indexOf(this) < 0) tweenActive.push(this);
158
+ this.callback(this.interp(this.duration));
159
+ }
160
+
161
+ /** True if this tween is in the active list and not paused.
162
+ * @returns {boolean}
163
+ * @memberof TweenSystem */
164
+ isActive()
165
+ {
166
+ return !this.paused && tweenActive.indexOf(this) >= 0;
167
+ }
168
+
169
+ /** Get how far this tween has progressed, from 0 (just started) to 1
170
+ * (completed). Clamped — overshoot past completion still reads 1.
171
+ * @returns {number}
172
+ * @memberof TweenSystem */
173
+ getPercent()
174
+ {
175
+ return percent(this.duration - this.life, 0, this.duration);
176
+ }
177
+
178
+ /** Get the current interpolated value (the value most recently passed to
179
+ * the callback). Returns a number, Vector2, or Color depending on the
180
+ * tween's start/end types.
181
+ * @returns {number|Vector2|Color}
182
+ * @memberof TweenSystem */
183
+ getValue()
184
+ {
185
+ return this.interp(this.life);
186
+ }
187
+
188
+ /** Compute the interpolated value at the given remaining `life`.
189
+ * At life === duration the result is `start`; at life === 0 it is `end`.
190
+ * @param {number} life
191
+ * @returns {number}
192
+ * @memberof TweenSystem */
193
+ interp(life)
194
+ {
195
+ const x = this.ease((this.duration - life) / this.duration);
196
+ if (isLerpable(this.start))
197
+ return this.start.lerp(this.end, x);
198
+ return this.start + (this.end - this.start) * x;
199
+ }
200
+
201
+ /** Remove this tween from the active list and prevent any pending then-callback.
202
+ * @memberof TweenSystem */
203
+ stop()
204
+ {
205
+ const i = tweenActive.indexOf(this);
206
+ if (i >= 0) tweenActive.splice(i, 1);
207
+ this.thenCallback = undefined;
208
+ }
209
+ }
210
+
211
+ /** Library of named easing curves and direction modifiers.
212
+ * All curves accept `x` in [0,1] and return [0,1] (with possible overshoot
213
+ * for ELASTIC/BACK/SPRING/BOUNCE). Curves are values you pass to `setEase`
214
+ * or compose via the IN/OUT/IN_OUT/PIECEWISE/BEZIER modifiers.
215
+ * @memberof TweenSystem
216
+ * @example
217
+ * // Use a basic curve
218
+ * new Tween(callback, 0, 10, 1).setEase(Ease.SINE);
219
+ * // Use a modifier on a curve
220
+ * new Tween(callback, 0, 10, 1).setEase(Ease.OUT(Ease.BACK));
221
+ */
222
+ const Ease =
223
+ {
224
+ /** Linear (identity) curve.
225
+ * @param {number} x
226
+ * @returns {number}
227
+ * @memberof TweenSystem */
228
+ LINEAR: (x) => x,
229
+
230
+ /** Power curve factory: `Ease.POWER(n)` returns `x => x**n`.
231
+ * Use n=2 for quadratic, n=3 for cubic, etc.
232
+ * @param {number} n
233
+ * @returns {function(number):number}
234
+ * @memberof TweenSystem */
235
+ POWER: (n) => (x) => x ** n,
236
+
237
+ /** Sine ease-in curve: starts slow, ends fast.
238
+ * @param {number} x
239
+ * @returns {number}
240
+ * @memberof TweenSystem */
241
+ SINE: (x) => 1 - Math.cos(x * (Math.PI / 2)),
242
+
243
+ /** Circular ease-in curve.
244
+ * @param {number} x
245
+ * @returns {number}
246
+ * @memberof TweenSystem */
247
+ CIRC: (x) => 1 - Math.sqrt(1 - x * x),
248
+
249
+ /** Exponential ease-in curve (`2^(10x-10)`).
250
+ * @param {number} x
251
+ * @returns {number}
252
+ * @memberof TweenSystem */
253
+ EXPO: (x) => 2 ** (10 * x - 10),
254
+
255
+ /** Back ease-in: overshoots backward at the start before snapping forward.
256
+ * @param {number} x
257
+ * @returns {number}
258
+ * @memberof TweenSystem */
259
+ BACK: (x) => x * x * (2.70158 * x - 1.70158),
260
+
261
+ /** Elastic ease-in: oscillates with decreasing amplitude.
262
+ * @param {number} x
263
+ * @returns {number}
264
+ * @memberof TweenSystem */
265
+ ELASTIC: (x) =>
266
+ -(2 ** (10 * x - 10)) * Math.sin(((37 - 40 * x) * Math.PI) / 6),
267
+
268
+ /** Spring-like ease-out: oscillates outward after passing the target.
269
+ * @param {number} x
270
+ * @returns {number}
271
+ * @memberof TweenSystem */
272
+ SPRING: (x) =>
273
+ 1 -
274
+ (Math.sin(Math.PI * (1 - x) * (0.2 + 2.5 * (1 - x) ** 3)) *
275
+ Math.pow(x, 2.2) +
276
+ (1 - x)) *
277
+ (1.0 + 1.2 * x),
278
+
279
+ /** Bouncing ease-in: slow ramp with bouncing impacts near the end.
280
+ * Symmetric with the other base curves, which are all ease-in. To get the
281
+ * classic "object falls and hits the ground" shape (bounces near x=1),
282
+ * wrap with `Ease.OUT`: `Ease.OUT(Ease.BOUNCE)`.
283
+ * @param {number} x
284
+ * @returns {number}
285
+ * @memberof TweenSystem
286
+ * @example
287
+ * Ease.BOUNCE // ease-in bounce (slow, then bouncy at end)
288
+ * Ease.OUT(Ease.BOUNCE) // ease-out bounce (object hits ground)
289
+ * Ease.IN_OUT(Ease.BOUNCE) // bounces at both ends
290
+ */
291
+ BOUNCE: (x) =>
292
+ {
293
+ // Inverted form of the standard easeOutBounce: 1 - bounceOut(1 - x).
294
+ let t = 1 - x, f;
295
+ if (t < 4 / 11) f = 7.5625 * t * t;
296
+ else if (t < 8 / 11) f = 7.5625 * (t -= 6 / 11) * t + 0.75;
297
+ else if (t < 10 / 11) f = 7.5625 * (t -= 9 / 11) * t + 0.9375;
298
+ else f = 7.5625 * (t -= 10.5 / 11) * t + 0.984375;
299
+ return 1 - f;
300
+ },
301
+
302
+ /** Ease-in direction modifier: returns the curve unchanged. Symmetric
303
+ * with `OUT` and `IN_OUT`. Base curves are already ease-in by
304
+ * convention, so wrapping a curve in `IN` is a no-op — useful when
305
+ * picking the direction programmatically.
306
+ * @param {function(number):number} f - Curve to use as ease-in (returned unchanged)
307
+ * @returns {function(number):number}
308
+ * @memberof TweenSystem
309
+ * @example
310
+ * // Pick direction at runtime
311
+ * const dir = bouncyMode ? Ease.OUT : Ease.IN;
312
+ * new Tween(cb, 0, 10, 1).setEase(dir(Ease.BACK));
313
+ */
314
+ IN: (f) => f,
315
+
316
+ /** Reverse a curve so it eases out instead of in: `x => 1 - f(1 - x)`.
317
+ * @param {function(number):number} f
318
+ * @returns {function(number):number}
319
+ * @memberof TweenSystem
320
+ * @example
321
+ * Ease.OUT(Ease.POWER(2)) // ease-out quadratic
322
+ */
323
+ OUT: (f) => (x) => 1 - f(1 - x),
324
+
325
+ /** Combine the first half of `f` with `Ease.OUT(f)` for a symmetric curve.
326
+ * Bug-fix vs the original library: the original referenced an undefined
327
+ * global `Piecewise`; this implementation routes through `Ease.PIECEWISE`.
328
+ * @param {function(number):number} f
329
+ * @returns {function(number):number}
330
+ * @memberof TweenSystem */
331
+ IN_OUT: (f) => Ease.PIECEWISE(f, Ease.OUT(f)),
332
+
333
+ /** Split [0,1] into N equal sections and run a different curve in each.
334
+ * Each curve is mapped to its section: section i runs over [i/n, (i+1)/n]
335
+ * and its output is mapped to [i/n, (i+1)/n] of the overall range.
336
+ * @param {...function(number):number} fns
337
+ * @returns {function(number):number}
338
+ * @memberof TweenSystem */
339
+ PIECEWISE: (...fns) =>
340
+ {
341
+ const n = fns.length;
342
+ return (x) =>
343
+ {
344
+ const i = (x * n - 1e-9) >> 0;
345
+ return (fns[i]((x - i / n) * n) + i) / n;
346
+ };
347
+ },
348
+
349
+ /** Cubic Bezier curve solver in the style of CSS `cubic-bezier`.
350
+ * Control points (0,0), (x1,y1), (x2,y2), (1,1).
351
+ * @param {number} x1
352
+ * @param {number} y1
353
+ * @param {number} x2
354
+ * @param {number} y2
355
+ * @returns {function(number):number}
356
+ * @memberof TweenSystem
357
+ * @example
358
+ * Ease.BEZIER(0.25, 0.1, 0.25, 1) // CSS "ease"
359
+ */
360
+ BEZIER: (x1, y1, x2, y2) =>
361
+ {
362
+ // Parametric cubic Bezier with implicit (0,0) and (1,1) endpoints.
363
+ const curve = (t) =>
364
+ {
365
+ const u = 1 - t;
366
+ const c1 = 3 * u * u * t;
367
+ const c2 = 3 * u * t * t;
368
+ const t3 = t ** 3;
369
+ return [c1 * x1 + c2 * x2 + t3, c1 * y1 + c2 * y2 + t3];
370
+ };
371
+ return (x) =>
372
+ {
373
+ // Binary search for t such that curve(t).x ≈ x, then return curve(t).y.
374
+ let t0 = 0, t1 = 1;
375
+ for (let i = 0; i < 128; i++)
376
+ {
377
+ const tMid = (t0 + t1) / 2;
378
+ const [bx, by] = curve(tMid);
379
+ if (Math.abs(bx - x) < 1e-5) return by;
380
+ if (bx < x) t0 = tMid; else t1 = tMid;
381
+ }
382
+ return curve((t0 + t1) / 2)[1];
383
+ };
384
+ },
385
+ };
386
+
387
+ /** Tween a property on an object by dot-path. Returns the underlying Tween
388
+ * so all chaining methods (`setEase`, `then`, `loop`, `pingPong`, etc.)
389
+ * remain available.
390
+ *
391
+ * `start` and `end` may be numbers, Vector2 instances, Color instances, or
392
+ * any object with a `lerp(other, percent) => sameType` method.
393
+ * @param {Object} target - The object whose property is being animated
394
+ * @param {string} propertyPath - Dot-separated path, e.g. `'pos.x'` or `'color'`
395
+ * @param {number|Vector2|Color} start - Starting value
396
+ * @param {number|Vector2|Color} end - Ending value
397
+ * @param {number} [duration=1] - Duration in seconds
398
+ * @param {Object} [options] - Same options as the Tween constructor
399
+ * @returns {Tween}
400
+ * @memberof TweenSystem
401
+ * @example
402
+ * // Numeric: slide an object's x with an ease-out sine curve
403
+ * tweenProperty(player, 'pos.x', 0, 10, 2).setEase(Ease.OUT(Ease.SINE));
404
+ * // Vector2: animate a position diagonally
405
+ * tweenProperty(player, 'pos', vec2(-5, 0), vec2(5, 3), 2);
406
+ * // Color: pulse between two colors
407
+ * tweenProperty(sprite, 'color', RED, BLUE, 1).pingPong();
408
+ */
409
+ function tweenProperty(target, propertyPath, start, end, duration = 1, options = {})
410
+ {
411
+ ASSERT(target != null && typeof target === 'object', 'tweenProperty target must be an object');
412
+ ASSERT(isString(propertyPath) && propertyPath.length > 0, 'tweenProperty propertyPath must be a non-empty string');
413
+
414
+ const parts = propertyPath.split('.');
415
+ const lastKey = parts.pop();
416
+ const callback = (value) =>
417
+ {
418
+ let obj = target;
419
+ for (const k of parts) obj = obj[k];
420
+ obj[lastKey] = value;
421
+ };
422
+ return new Tween(callback, start, end, duration, options);
423
+ }
424
+
425
+ // Continuation that schedules the next loop iteration when one finishes.
426
+ // Called from the completed tween's `then` slot. Decrements the counter and
427
+ // only spawns a new tween if more iterations remain.
428
+ function loopContinuation(prev)
429
+ {
430
+ if (prev.loopRemaining !== Infinity && prev.loopRemaining <= 1) return;
431
+ const next = new Tween(prev.callback, prev.start, prev.end, prev.duration,
432
+ { ease: prev.ease, useRealTime: prev.useRealTime });
433
+ next.loopRemaining = prev.loopRemaining === Infinity
434
+ ? Infinity
435
+ : prev.loopRemaining - 1;
436
+ next.thenCallback = () => loopContinuation(next);
437
+ }
438
+
439
+ // Continuation for pingPong: spawns a new tween with start and end swapped.
440
+ function pingPongContinuation(prev)
441
+ {
442
+ if (prev.loopRemaining !== Infinity && prev.loopRemaining <= 1) return;
443
+ const next = new Tween(prev.callback, prev.end, prev.start, prev.duration,
444
+ { ease: prev.ease, useRealTime: prev.useRealTime });
445
+ next.loopRemaining = prev.loopRemaining === Infinity
446
+ ? Infinity
447
+ : prev.loopRemaining - 1;
448
+ next.thenCallback = () => pingPongContinuation(next);
449
+ }
450
+
451
+ /** Engine plugin hook: advance every active tween by the appropriate delta.
452
+ * Called once per render frame by the engine (no arguments). May also be
453
+ * called explicitly with `(gameDelta, realDelta)` to drive tweens manually
454
+ * — useful for headless tests or custom replay/scrubbing systems.
455
+ * @param {number} [gameDelta] - Game-time delta in seconds; default: time - lastTime
456
+ * @param {number} [realDelta] - Real-time delta in seconds; default: timeReal - lastTimeReal
457
+ * @memberof TweenSystem */
458
+ function tweenUpdate(gameDelta, realDelta)
459
+ {
460
+ if (gameDelta === undefined)
461
+ {
462
+ // Engine path: compute deltas from engine time globals.
463
+ gameDelta = time - lastTime;
464
+ realDelta = timeReal - lastTimeReal;
465
+ lastTime = time;
466
+ lastTimeReal = timeReal;
467
+ }
468
+ else if (realDelta === undefined)
469
+ {
470
+ // Manual path with one arg: real and game advance together.
471
+ realDelta = gameDelta;
472
+ }
473
+
474
+ // Iterate in reverse so removals don't disturb iteration.
475
+ for (let i = tweenActive.length; i--;)
476
+ {
477
+ const t = tweenActive[i];
478
+ if (t.paused) continue;
479
+ const dt = t.useRealTime ? realDelta : gameDelta;
480
+ if (dt <= 0) continue;
481
+
482
+ t.life -= dt;
483
+ if (t.life > 0)
484
+ {
485
+ t.callback(t.interp(t.life));
486
+ }
487
+ else
488
+ {
489
+ // Completion: fire end value, remove from active, fire then-callback.
490
+ t.callback(t.interp(0));
491
+ tweenActive.splice(i, 1);
492
+ const cb = t.thenCallback;
493
+ t.thenCallback = undefined;
494
+ if (cb) cb();
495
+ }
496
+ }
497
+ }
498
+
499
+ /** Stop every active tween and clear their then-callbacks. Useful for resets
500
+ * on level transitions or when changing scenes.
501
+ * @memberof TweenSystem */
502
+ function tweenStopAll()
503
+ {
504
+ for (const t of tweenActive) t.thenCallback = undefined;
505
+ tweenActive.length = 0;
506
+ }
507
+
508
+ // Register with the engine so tweens auto-advance.
509
+ engineAddPlugin(tweenUpdate);
@@ -924,7 +924,7 @@ class UIObject
924
924
 
925
925
  /** Internal function called when object is clicked
926
926
  * @param {boolean} [playSound] */
927
- click(playSound)
927
+ click(playSound=true)
928
928
  {
929
929
  this.onClick();
930
930
  if (playSound && this.soundClick)
package/reference.md CHANGED
@@ -134,6 +134,7 @@ Timer.valueOf() // Get how long since elapsed, 0 if not set
134
134
  drawTile(pos, size, tileInfo, color=WHITE, angle=0, mirror, additiveColor)
135
135
  drawRect(pos, size, color=WHITE, angle=0)
136
136
  drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0)
137
+ drawTextureWrapped(pos, size, wrapCount, texture=0, color=WHITE, angle=0, additiveColor)
137
138
  drawLine(posA, posB, width=.1, color=WHITE, pos=(0,0), angle=0)
138
139
  drawLineList(points, width=.1, color, wrap=false, pos=(0,0), angle=0)
139
140
  drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos, angle=0)
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.18.1';
35
+ const engineVersion = '1.18.4';
36
36
 
37
37
  /** Frames per second to update
38
38
  * @type {number}
@@ -66,7 +66,7 @@ let frame = 0;
66
66
  * @memberof Engine */
67
67
  let time = 0;
68
68
 
69
- /** Actual clock time since start in seconds (not affected by pause or frame rate clamping)
69
+ /** Actual clock time since start in seconds (not affected by pause, timescale, or frame rate clamping)
70
70
  * @type {number}
71
71
  * @memberof Engine */
72
72
  let timeReal = 0;
@@ -197,11 +197,14 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
197
197
  averageFPS = lerp(averageFPS, 1e3/(frameTimeDeltaMS||1), .05);
198
198
  const debugSpeedUp = debug && keyIsDown('Equal'); // +
199
199
  const debugSpeedDown = debug && keyIsDown('Minus'); // -
200
- if (debug) // +/- to speed/slow time
201
- frameTimeDeltaMS *= debugSpeedUp ? 10 : debugSpeedDown ? .1 : 1;
202
- timeReal += frameTimeDeltaMS / 1e3;
200
+ const debugScale = debugSpeedUp ? 10 : debugSpeedDown ? .1 : 1;
201
+
202
+ // apply time deltas
203
+ timeReal += frameTimeDeltaMS * debugScale / 1e3;
204
+ const combinedScale = timeScale * debugScale;
205
+ frameTimeDeltaMS *= combinedScale;
203
206
  frameTimeBufferMS += paused ? 0 : frameTimeDeltaMS;
204
- if (!debugSpeedUp)
207
+ if (combinedScale <= 1)
205
208
  frameTimeBufferMS = min(frameTimeBufferMS, 50); // clamp min framerate
206
209
 
207
210
  let wasUpdated = false;
@@ -44,6 +44,7 @@ const enginePluginFiles =
44
44
  `${PLUGIN_FOLDER}/uiSystem.js`,
45
45
  `${PLUGIN_FOLDER}/box2d.js`,
46
46
  `${PLUGIN_FOLDER}/drawUtilities.js`,
47
+ `${PLUGIN_FOLDER}/tween.js`,
47
48
  ];
48
49
  const engineExtraFiles =
49
50
  [