littlejsengine 1.18.2 → 1.18.7

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 (172) hide show
  1. package/COPYRIGHT.txt +38 -0
  2. package/FAQ.md +633 -0
  3. package/LICENSE +5 -27
  4. package/README.md +19 -1
  5. package/dist/littlejs.d.ts +550 -57
  6. package/dist/littlejs.esm.js +2208 -492
  7. package/dist/littlejs.esm.min.js +1 -1
  8. package/dist/littlejs.js +2165 -480
  9. package/dist/littlejs.min.js +1 -1
  10. package/dist/littlejs.release.js +2166 -481
  11. package/package.json +8 -1
  12. package/plugins/box2d.js +36 -9
  13. package/{src/engineMedals.js → plugins/medalSystem.js} +248 -196
  14. package/plugins/pathFinder.js +758 -0
  15. package/plugins/pluginExport.js +26 -0
  16. package/plugins/tween.js +509 -0
  17. package/plugins/tweenSystem.js +509 -0
  18. package/plugins/uiSystem.js +151 -12
  19. package/src/engine.js +9 -145
  20. package/src/engineAudio.js +8 -5
  21. package/src/engineBuild.mjs +4 -1
  22. package/src/engineDebug.js +1 -1
  23. package/src/engineDraw.js +154 -5
  24. package/src/engineExport.js +17 -12
  25. package/src/engineInput.js +27 -1
  26. package/src/engineLogo.js +146 -0
  27. package/src/engineMath.js +10 -3
  28. package/src/engineObject.js +11 -5
  29. package/src/engineParticles.js +6 -3
  30. package/src/engineRelease.js +1 -1
  31. package/src/engineSettings.js +17 -47
  32. package/src/engineUtilities.js +7 -2
  33. package/src/engineWebGL.js +31 -8
  34. package/src/jsconfig.json +3 -1
  35. package/.claude/settings.local.json +0 -7
  36. package/.github/workflows/test.yml +0 -17
  37. package/AI.md +0 -172
  38. package/CLAUDE.md +0 -1
  39. package/examples/box2d/game.js +0 -192
  40. package/examples/box2d/gameObjects.js +0 -564
  41. package/examples/box2d/index.html +0 -9
  42. package/examples/box2d/scenes.js +0 -194
  43. package/examples/box2d/tiles.png +0 -0
  44. package/examples/breakout/game.js +0 -175
  45. package/examples/breakout/gameObjects.js +0 -147
  46. package/examples/breakout/index.html +0 -8
  47. package/examples/breakout/tiles.png +0 -0
  48. package/examples/breakoutTutorial/README.md +0 -521
  49. package/examples/breakoutTutorial/game.js +0 -191
  50. package/examples/breakoutTutorial/images/1.png +0 -0
  51. package/examples/breakoutTutorial/images/10.png +0 -0
  52. package/examples/breakoutTutorial/images/11.png +0 -0
  53. package/examples/breakoutTutorial/images/2.png +0 -0
  54. package/examples/breakoutTutorial/images/3.png +0 -0
  55. package/examples/breakoutTutorial/images/4.png +0 -0
  56. package/examples/breakoutTutorial/images/5.png +0 -0
  57. package/examples/breakoutTutorial/images/6.png +0 -0
  58. package/examples/breakoutTutorial/images/7.png +0 -0
  59. package/examples/breakoutTutorial/images/8.png +0 -0
  60. package/examples/breakoutTutorial/images/9.png +0 -0
  61. package/examples/breakoutTutorial/index.html +0 -8
  62. package/examples/electron/build.mjs +0 -127
  63. package/examples/electron/electron.js +0 -35
  64. package/examples/electron/game.js +0 -54
  65. package/examples/electron/index.html +0 -13
  66. package/examples/electron/package.json +0 -5
  67. package/examples/electron/tiles.png +0 -0
  68. package/examples/empty/game.js +0 -51
  69. package/examples/empty/index.html +0 -5
  70. package/examples/empty/tiles.png +0 -0
  71. package/examples/favicon.png +0 -0
  72. package/examples/games.jpg +0 -0
  73. package/examples/htmlMenu/game.js +0 -82
  74. package/examples/htmlMenu/index.html +0 -45
  75. package/examples/htmlMenu/tiles.png +0 -0
  76. package/examples/index.html +0 -800
  77. package/examples/logo.png +0 -0
  78. package/examples/logo2.png +0 -0
  79. package/examples/module/build.mjs +0 -124
  80. package/examples/module/game.js +0 -132
  81. package/examples/module/index.html +0 -10
  82. package/examples/module/tiles.png +0 -0
  83. package/examples/particles/index.html +0 -426
  84. package/examples/particles/tiles.png +0 -0
  85. package/examples/platformer/data/gameLevelData.tmx +0 -143
  86. package/examples/platformer/data/gameLevelData.tsx +0 -4
  87. package/examples/platformer/game.js +0 -147
  88. package/examples/platformer/gameCharacter.js +0 -309
  89. package/examples/platformer/gameEffects.js +0 -278
  90. package/examples/platformer/gameLevel.js +0 -205
  91. package/examples/platformer/gameLevelData.json +0 -171
  92. package/examples/platformer/gameObjects.js +0 -378
  93. package/examples/platformer/gamePlayer.js +0 -35
  94. package/examples/platformer/index.html +0 -9
  95. package/examples/platformer/tiles.png +0 -0
  96. package/examples/platformer/tilesLevel.png +0 -0
  97. package/examples/puzzle/game.js +0 -331
  98. package/examples/puzzle/index.html +0 -8
  99. package/examples/puzzle/tiles.png +0 -0
  100. package/examples/screenshot.jpg +0 -0
  101. package/examples/shorts/animation.js +0 -18
  102. package/examples/shorts/base.html +0 -54
  103. package/examples/shorts/blending.js +0 -14
  104. package/examples/shorts/box2d.js +0 -51
  105. package/examples/shorts/box2dCar.js +0 -59
  106. package/examples/shorts/box2dPool.js +0 -107
  107. package/examples/shorts/box2dTileLayer.js +0 -48
  108. package/examples/shorts/cameraDrag.js +0 -22
  109. package/examples/shorts/clock.js +0 -21
  110. package/examples/shorts/colors.js +0 -25
  111. package/examples/shorts/debugDraw.js +0 -37
  112. package/examples/shorts/empty.js +0 -30
  113. package/examples/shorts/flappyGame.js +0 -55
  114. package/examples/shorts/fontImage.js +0 -17
  115. package/examples/shorts/fps.js +0 -90
  116. package/examples/shorts/helloWorld.js +0 -11
  117. package/examples/shorts/hillGlideGame.js +0 -63
  118. package/examples/shorts/input.js +0 -64
  119. package/examples/shorts/landerGame.js +0 -57
  120. package/examples/shorts/maze.js +0 -48
  121. package/examples/shorts/medals.js +0 -51
  122. package/examples/shorts/music.js +0 -78
  123. package/examples/shorts/musicPlayer.js +0 -137
  124. package/examples/shorts/nineSlice.js +0 -40
  125. package/examples/shorts/parallax.js +0 -72
  126. package/examples/shorts/particles.js +0 -28
  127. package/examples/shorts/piano.js +0 -44
  128. package/examples/shorts/platformer.js +0 -45
  129. package/examples/shorts/pongGame.js +0 -41
  130. package/examples/shorts/postProcess.js +0 -62
  131. package/examples/shorts/sequencer.js +0 -124
  132. package/examples/shorts/shader.js +0 -29
  133. package/examples/shorts/shapes.js +0 -21
  134. package/examples/shorts/slidingPuzzle.js +0 -52
  135. package/examples/shorts/song.mp3 +0 -0
  136. package/examples/shorts/sound.js +0 -36
  137. package/examples/shorts/spaceGame.js +0 -58
  138. package/examples/shorts/spriteAtlas.js +0 -31
  139. package/examples/shorts/starfield.js +0 -15
  140. package/examples/shorts/texture.js +0 -16
  141. package/examples/shorts/tileLayer.js +0 -48
  142. package/examples/shorts/tileRaycast.js +0 -39
  143. package/examples/shorts/tiles.png +0 -0
  144. package/examples/shorts/tiltedView.js +0 -63
  145. package/examples/shorts/timers.js +0 -53
  146. package/examples/shorts/topDown.js +0 -38
  147. package/examples/shorts/uiSystem.js +0 -61
  148. package/examples/shorts/video.webm +0 -0
  149. package/examples/shorts/videoPlayer.js +0 -34
  150. package/examples/starter/build.bat +0 -7
  151. package/examples/starter/build.mjs +0 -126
  152. package/examples/starter/game.js +0 -137
  153. package/examples/starter/index.html +0 -35
  154. package/examples/starter/tiles.png +0 -0
  155. package/examples/stress/index.html +0 -173
  156. package/examples/style.css +0 -150
  157. package/examples/typescript/build.mjs +0 -60
  158. package/examples/typescript/game.js +0 -100
  159. package/examples/typescript/game.ts +0 -132
  160. package/examples/typescript/index.html +0 -10
  161. package/examples/typescript/tiles.png +0 -0
  162. package/examples/typescript/tsconfig.json +0 -17
  163. package/examples/uiSystem/game.js +0 -139
  164. package/examples/uiSystem/index.html +0 -10
  165. package/examples/uiSystem/tiles.png +0 -0
  166. package/jsconfig.json +0 -12
  167. package/plugins/desktop.ini +0 -2
  168. package/reference.md +0 -447
  169. package/test/math.test.mjs +0 -774
  170. package/test/setup.mjs +0 -22
  171. package/test/smoke.test.mjs +0 -258
  172. package/test/util.test.mjs +0 -80
@@ -4,6 +4,20 @@
4
4
 
5
5
  export
6
6
  {
7
+ // Medals
8
+ medals,
9
+ medalsPreventUnlock,
10
+ medalsInit,
11
+ medalsForEach,
12
+ Medal,
13
+ medalDisplayTime,
14
+ medalDisplaySlideTime,
15
+ medalDisplaySize,
16
+ setMedalDisplayTime,
17
+ setMedalDisplaySlideTime,
18
+ setMedalDisplaySize,
19
+ setMedalsPreventUnlock,
20
+
7
21
  // Newgrounds
8
22
  newgrounds,
9
23
  NewgroundsPlugin,
@@ -29,6 +43,7 @@ export
29
43
  UICheckbox,
30
44
  UISlider,
31
45
  UIVideo,
46
+ UILayout,
32
47
 
33
48
  // Box2D Physics
34
49
  box2d,
@@ -60,4 +75,15 @@ export
60
75
  drawNineSliceScreen,
61
76
  drawThreeSlice,
62
77
  drawThreeSliceScreen,
78
+
79
+ // Tween System
80
+ Tween,
81
+ tweenProperty,
82
+ tweenStopAll,
83
+ tweenUpdate,
84
+ Ease,
85
+
86
+ // Path Finding
87
+ PathFinder,
88
+ PathFinderNode,
63
89
  }
@@ -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 - (1 - x * x)**.5,
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);