littlejsengine 1.18.4 → 1.18.8

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 (176) hide show
  1. package/COPYRIGHT.txt +38 -0
  2. package/FAQ.md +633 -0
  3. package/README.md +19 -1
  4. package/dist/littlejs.d.ts +354 -55
  5. package/dist/littlejs.esm.js +1531 -489
  6. package/dist/littlejs.esm.min.js +1 -1
  7. package/dist/littlejs.js +1499 -476
  8. package/dist/littlejs.min.js +1 -1
  9. package/dist/littlejs.release.js +1493 -470
  10. package/package.json +8 -1
  11. package/plugins/box2d.js +32 -7
  12. package/{src/engineMedals.js → plugins/medalSystem.js} +248 -196
  13. package/plugins/pathFinder.js +758 -0
  14. package/plugins/pluginExport.js +19 -0
  15. package/plugins/{tween.js → tweenSystem.js} +7 -7
  16. package/plugins/uiSystem.js +186 -25
  17. package/src/engine.js +2 -141
  18. package/src/engineAudio.js +3 -3
  19. package/src/engineBuild.mjs +4 -2
  20. package/src/engineDebug.js +7 -7
  21. package/src/engineDraw.js +26 -8
  22. package/src/engineExport.js +13 -13
  23. package/src/engineInput.js +8 -8
  24. package/src/engineLogo.js +146 -0
  25. package/src/engineMath.js +15 -6
  26. package/src/engineObject.js +11 -5
  27. package/src/engineParticles.js +6 -3
  28. package/src/engineRelease.js +1 -1
  29. package/src/engineSettings.js +0 -47
  30. package/src/engineUtilities.js +13 -8
  31. package/src/engineWebGL.js +34 -7
  32. package/src/jsconfig.json +3 -1
  33. package/.github/workflows/test.yml +0 -17
  34. package/AI.md +0 -173
  35. package/CLAUDE.md +0 -1
  36. package/examples/box2d/game.js +0 -192
  37. package/examples/box2d/gameObjects.js +0 -564
  38. package/examples/box2d/index.html +0 -9
  39. package/examples/box2d/scenes.js +0 -194
  40. package/examples/box2d/tiles.png +0 -0
  41. package/examples/breakout/game.js +0 -175
  42. package/examples/breakout/gameObjects.js +0 -147
  43. package/examples/breakout/index.html +0 -8
  44. package/examples/breakout/tiles.png +0 -0
  45. package/examples/breakoutTutorial/README.md +0 -521
  46. package/examples/breakoutTutorial/game.js +0 -191
  47. package/examples/breakoutTutorial/images/1.png +0 -0
  48. package/examples/breakoutTutorial/images/10.png +0 -0
  49. package/examples/breakoutTutorial/images/11.png +0 -0
  50. package/examples/breakoutTutorial/images/2.png +0 -0
  51. package/examples/breakoutTutorial/images/3.png +0 -0
  52. package/examples/breakoutTutorial/images/4.png +0 -0
  53. package/examples/breakoutTutorial/images/5.png +0 -0
  54. package/examples/breakoutTutorial/images/6.png +0 -0
  55. package/examples/breakoutTutorial/images/7.png +0 -0
  56. package/examples/breakoutTutorial/images/8.png +0 -0
  57. package/examples/breakoutTutorial/images/9.png +0 -0
  58. package/examples/breakoutTutorial/index.html +0 -8
  59. package/examples/electron/build.mjs +0 -127
  60. package/examples/electron/electron.js +0 -35
  61. package/examples/electron/game.js +0 -54
  62. package/examples/electron/index.html +0 -13
  63. package/examples/electron/package.json +0 -5
  64. package/examples/electron/tiles.png +0 -0
  65. package/examples/empty/game.js +0 -51
  66. package/examples/empty/index.html +0 -5
  67. package/examples/empty/tiles.png +0 -0
  68. package/examples/favicon.png +0 -0
  69. package/examples/games.jpg +0 -0
  70. package/examples/htmlMenu/game.js +0 -82
  71. package/examples/htmlMenu/index.html +0 -45
  72. package/examples/htmlMenu/tiles.png +0 -0
  73. package/examples/index.html +0 -62
  74. package/examples/logo.png +0 -0
  75. package/examples/logo2.png +0 -0
  76. package/examples/module/build.mjs +0 -124
  77. package/examples/module/game.js +0 -132
  78. package/examples/module/index.html +0 -10
  79. package/examples/module/tiles.png +0 -0
  80. package/examples/particles/index.html +0 -426
  81. package/examples/particles/tiles.png +0 -0
  82. package/examples/platformer/data/gameLevelData.tmx +0 -143
  83. package/examples/platformer/data/gameLevelData.tsx +0 -4
  84. package/examples/platformer/game.js +0 -147
  85. package/examples/platformer/gameCharacter.js +0 -309
  86. package/examples/platformer/gameEffects.js +0 -278
  87. package/examples/platformer/gameLevel.js +0 -205
  88. package/examples/platformer/gameLevelData.json +0 -171
  89. package/examples/platformer/gameObjects.js +0 -378
  90. package/examples/platformer/gamePlayer.js +0 -35
  91. package/examples/platformer/index.html +0 -9
  92. package/examples/platformer/tiles.png +0 -0
  93. package/examples/platformer/tilesLevel.png +0 -0
  94. package/examples/puzzle/game.js +0 -331
  95. package/examples/puzzle/index.html +0 -8
  96. package/examples/puzzle/tiles.png +0 -0
  97. package/examples/screenshot.jpg +0 -0
  98. package/examples/shorts/animation.js +0 -18
  99. package/examples/shorts/base.html +0 -54
  100. package/examples/shorts/blending.js +0 -14
  101. package/examples/shorts/box2d.js +0 -51
  102. package/examples/shorts/box2dCar.js +0 -59
  103. package/examples/shorts/box2dPool.js +0 -107
  104. package/examples/shorts/box2dTileLayer.js +0 -48
  105. package/examples/shorts/cameraDrag.js +0 -22
  106. package/examples/shorts/clock.js +0 -21
  107. package/examples/shorts/colors.js +0 -25
  108. package/examples/shorts/debugDraw.js +0 -37
  109. package/examples/shorts/empty.js +0 -30
  110. package/examples/shorts/flappyGame.js +0 -55
  111. package/examples/shorts/fontImage.js +0 -17
  112. package/examples/shorts/fps.js +0 -90
  113. package/examples/shorts/helloWorld.js +0 -11
  114. package/examples/shorts/hillGlideGame.js +0 -63
  115. package/examples/shorts/input.js +0 -64
  116. package/examples/shorts/landerGame.js +0 -57
  117. package/examples/shorts/maze.js +0 -48
  118. package/examples/shorts/medals.js +0 -51
  119. package/examples/shorts/music.js +0 -78
  120. package/examples/shorts/musicPlayer.js +0 -137
  121. package/examples/shorts/nineSlice.js +0 -40
  122. package/examples/shorts/parallax.js +0 -72
  123. package/examples/shorts/particles.js +0 -28
  124. package/examples/shorts/piano.js +0 -44
  125. package/examples/shorts/platformer.js +0 -45
  126. package/examples/shorts/pongGame.js +0 -41
  127. package/examples/shorts/postProcess.js +0 -62
  128. package/examples/shorts/sequencer.js +0 -124
  129. package/examples/shorts/shader.js +0 -29
  130. package/examples/shorts/shapes.js +0 -21
  131. package/examples/shorts/slidingPuzzle.js +0 -52
  132. package/examples/shorts/song.mp3 +0 -0
  133. package/examples/shorts/sound.js +0 -36
  134. package/examples/shorts/spaceGame.js +0 -58
  135. package/examples/shorts/spriteAtlas.js +0 -31
  136. package/examples/shorts/starfield.js +0 -15
  137. package/examples/shorts/texture.js +0 -16
  138. package/examples/shorts/textureWrapped.js +0 -12
  139. package/examples/shorts/tileLayer.js +0 -48
  140. package/examples/shorts/tileRaycast.js +0 -39
  141. package/examples/shorts/tiles.png +0 -0
  142. package/examples/shorts/tiltedView.js +0 -63
  143. package/examples/shorts/timers.js +0 -53
  144. package/examples/shorts/topDown.js +0 -38
  145. package/examples/shorts/tween.js +0 -24
  146. package/examples/shorts/uiSystem.js +0 -61
  147. package/examples/shorts/video.webm +0 -0
  148. package/examples/shorts/videoPlayer.js +0 -34
  149. package/examples/shorts.js +0 -743
  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/tweenSystem/game.js +0 -171
  158. package/examples/tweenSystem/index.html +0 -10
  159. package/examples/tweenSystem/tiles.png +0 -0
  160. package/examples/typescript/build.mjs +0 -60
  161. package/examples/typescript/game.js +0 -100
  162. package/examples/typescript/game.ts +0 -132
  163. package/examples/typescript/index.html +0 -10
  164. package/examples/typescript/tiles.png +0 -0
  165. package/examples/typescript/tsconfig.json +0 -17
  166. package/examples/uiSystem/game.js +0 -139
  167. package/examples/uiSystem/index.html +0 -10
  168. package/examples/uiSystem/tiles.png +0 -0
  169. package/jsconfig.json +0 -12
  170. package/plugins/desktop.ini +0 -2
  171. package/reference.md +0 -448
  172. package/test/math.test.mjs +0 -774
  173. package/test/setup.mjs +0 -22
  174. package/test/smoke.test.mjs +0 -274
  175. package/test/tween.test.mjs +0 -576
  176. 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,
@@ -67,4 +82,8 @@ export
67
82
  tweenStopAll,
68
83
  tweenUpdate,
69
84
  Ease,
85
+
86
+ // Path Finding
87
+ PathFinder,
88
+ PathFinderNode,
70
89
  }
@@ -238,13 +238,13 @@ const Ease =
238
238
  * @param {number} x
239
239
  * @returns {number}
240
240
  * @memberof TweenSystem */
241
- SINE: (x) => 1 - Math.cos(x * (Math.PI / 2)),
241
+ SINE: (x) => 1 - cos(x * (PI / 2)),
242
242
 
243
243
  /** Circular ease-in curve.
244
244
  * @param {number} x
245
245
  * @returns {number}
246
246
  * @memberof TweenSystem */
247
- CIRC: (x) => 1 - Math.sqrt(1 - x * x),
247
+ CIRC: (x) => 1 - (1 - x * x)**.5,
248
248
 
249
249
  /** Exponential ease-in curve (`2^(10x-10)`).
250
250
  * @param {number} x
@@ -263,7 +263,7 @@ const Ease =
263
263
  * @returns {number}
264
264
  * @memberof TweenSystem */
265
265
  ELASTIC: (x) =>
266
- -(2 ** (10 * x - 10)) * Math.sin(((37 - 40 * x) * Math.PI) / 6),
266
+ -(2 ** (10 * x - 10)) * sin(((37 - 40 * x) * PI) / 6),
267
267
 
268
268
  /** Spring-like ease-out: oscillates outward after passing the target.
269
269
  * @param {number} x
@@ -271,8 +271,8 @@ const Ease =
271
271
  * @memberof TweenSystem */
272
272
  SPRING: (x) =>
273
273
  1 -
274
- (Math.sin(Math.PI * (1 - x) * (0.2 + 2.5 * (1 - x) ** 3)) *
275
- Math.pow(x, 2.2) +
274
+ (sin(PI * (1 - x) * (0.2 + 2.5 * (1 - x) ** 3)) *
275
+ x ** 2.2 +
276
276
  (1 - x)) *
277
277
  (1.0 + 1.2 * x),
278
278
 
@@ -376,7 +376,7 @@ const Ease =
376
376
  {
377
377
  const tMid = (t0 + t1) / 2;
378
378
  const [bx, by] = curve(tMid);
379
- if (Math.abs(bx - x) < 1e-5) return by;
379
+ if (abs(bx - x) < 1e-5) return by;
380
380
  if (bx < x) t0 = tMid; else t1 = tMid;
381
381
  }
382
382
  return curve((t0 + t1) / 2)[1];
@@ -409,7 +409,7 @@ const Ease =
409
409
  function tweenProperty(target, propertyPath, start, end, duration = 1, options = {})
410
410
  {
411
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');
412
+ ASSERT(isStringLike(propertyPath) && propertyPath.length > 0, 'tweenProperty propertyPath must be a non-empty string');
413
413
 
414
414
  const parts = propertyPath.split('.');
415
415
  const lastKey = parts.pop();
@@ -118,22 +118,38 @@ class UISystemPlugin
118
118
  this.lastHoverObject = undefined;
119
119
  /** @property {UIObject} - Current confirm menu being shown */
120
120
  this.confirmDialog = undefined;
121
- /** @property {UIObject} - Object to send keyboard input to */
122
- this.keyInputObject = undefined;
121
+ /** @private */
122
+ this._keyInputObject = undefined;
123
+ /** @private */
124
+ this._onKeyDown = (e) => this._keyInputObject?.onKeyDown(e);
123
125
 
124
126
  engineAddPlugin(uiUpdate, uiRender);
125
127
 
126
- // key down handler
127
- function onKeyDown(e)
128
- { uiSystem.keyInputObject?.onKeyDown(e); }
129
- document.addEventListener('keydown', onKeyDown);
130
-
131
- // set object position in parent space
128
+ // set object position based on anchor target (parent box, or canvas for roots),
129
+ // self-pivot, and localPos offset
132
130
  function updateTransforms(o)
133
131
  {
134
- if (!o.parent) return;
135
- o.pos.x = o.localPos.x + o.parent.pos.x;
136
- o.pos.y = o.localPos.y + o.parent.pos.y;
132
+ let targetPos, targetSize;
133
+ if (o.parent)
134
+ {
135
+ targetPos = o.parent.pos;
136
+ targetSize = o.parent.size;
137
+ }
138
+ else
139
+ {
140
+ // anchor to canvas in native coords (handles nativeHeight if set)
141
+ targetPos = uiSystem.screenToNative(mainCanvasSize.scale(.5));
142
+ targetSize = uiSystem.nativeHeight
143
+ ? vec2(mainCanvasSize.x * uiSystem.nativeHeight / mainCanvasSize.y,
144
+ uiSystem.nativeHeight)
145
+ : mainCanvasSize;
146
+ }
147
+
148
+ const a = o.anchor;
149
+ o.pos = targetPos
150
+ .add(targetSize.multiply(a).scale(.5)) // anchor point on target
151
+ .subtract(o.size.multiply(a).scale(.5)) // pivot shift on self
152
+ .add(o.localPos); // user offset
137
153
  }
138
154
 
139
155
  // setup recursive update and render
@@ -469,8 +485,23 @@ class UISystemPlugin
469
485
  return p;
470
486
  }
471
487
 
488
+ /** Object to send keyboard input to (typically a UITextInput).
489
+ * The document keydown listener is only attached while this is set,
490
+ * so games that never use text input pay no event-handling cost.
491
+ * @type {UIObject} */
492
+ get keyInputObject() { return this._keyInputObject; }
493
+ set keyInputObject(obj)
494
+ {
495
+ const had = !!this._keyInputObject;
496
+ this._keyInputObject = obj;
497
+ if (!had && obj)
498
+ document.addEventListener('keydown', this._onKeyDown);
499
+ else if (had && !obj)
500
+ document.removeEventListener('keydown', this._onKeyDown);
501
+ }
502
+
472
503
  /** Destroy and remove all objects
473
- * @memberof Engine */
504
+ * @memberof UISystem */
474
505
  destroyObjects()
475
506
  {
476
507
  for (const o of this.uiObjects)
@@ -585,9 +616,8 @@ class UISystemPlugin
585
616
  // confirm menu
586
617
  const confirmMenu = new UIObject(vec2(), size);
587
618
  uiSystem.confirmDialog = confirmMenu;
588
- confirmMenu.onRender = ()=>
619
+ confirmMenu.onRender = ()=>
589
620
  {
590
- confirmMenu.pos = uiSystem.screenToNative(mainCanvasSize.scale(.5));
591
621
  const backgroundColor = hsl(0,0,0,.7);
592
622
  uiSystem.drawRect(vec2(), vec2(1e9), backgroundColor);
593
623
  }
@@ -720,7 +750,11 @@ class UIObject
720
750
  this.navigationIndex = undefined;
721
751
  /** @property {boolean} - Should this be auto selected by navigation? Must also have valid navigation index. */
722
752
  this.navigationAutoSelect = false;
723
-
753
+ /** @property {Vector2} - Where on parent (or canvas if no parent) this object is anchored.
754
+ * Components in [-1, 1]: (0,0)=center, (-1,-1)=top-left, (1,1)=bottom-right.
755
+ * Also acts as self-pivot — e.g. (1,-1) puts your top-right corner at the anchor point. */
756
+ this.anchor = vec2();
757
+
724
758
  uiSystem.uiObjects.push(this);
725
759
  }
726
760
 
@@ -893,15 +927,13 @@ class UIObject
893
927
  * @return {string} */
894
928
  toString()
895
929
  {
896
- if (!debug) return;
897
-
898
930
  let text = 'type = ' + this.constructor.name;
899
931
  if (this.text)
900
932
  text += '\ntext = ' + this.text;
901
933
  if (this.pos.x || this.pos.y)
902
934
  text += '\npos = ' + this.pos;
903
935
  if (this.localPos.x || this.localPos.y)
904
- text += '\localPos = ' + this.localPos;
936
+ text += '\nlocalPos = ' + this.localPos;
905
937
  if (this.size.x || this.size.y)
906
938
  text += '\nsize = ' + this.size;
907
939
  if (this.color)
@@ -975,9 +1007,9 @@ class UIText extends UIObject
975
1007
  {
976
1008
  super(pos, size);
977
1009
 
978
- ASSERT(isString(text), 'ui text must be a string');
1010
+ ASSERT(isStringLike(text), 'ui text must be a string');
979
1011
  ASSERT(['left','center','right'].includes(align), 'ui text align must be left, center, or right');
980
- ASSERT(isString(font), 'ui text font must be a string');
1012
+ ASSERT(isStringLike(font), 'ui text font must be a string');
981
1013
 
982
1014
  // set properties
983
1015
  this.text = text;
@@ -1025,7 +1057,7 @@ class UITextInput extends UIObject
1025
1057
  {
1026
1058
  super(pos, size);
1027
1059
 
1028
- ASSERT(isString(text), 'ui text must be a string');
1060
+ ASSERT(isStringLike(text), 'ui text must be a string');
1029
1061
 
1030
1062
  /** @property {number} - Max length of input (0 = no limit) */
1031
1063
  this.maxLength = 0;
@@ -1162,7 +1194,7 @@ class UIButton extends UIObject
1162
1194
  {
1163
1195
  super(pos, size);
1164
1196
 
1165
- ASSERT(isString(text), 'ui button must be a string');
1197
+ ASSERT(isStringLike(text), 'ui button must be a string');
1166
1198
  ASSERT(isColor(color), 'ui button color must be a color');
1167
1199
 
1168
1200
  /** @property {Vector2} - Text offset for the button */
@@ -1203,7 +1235,7 @@ class UICheckbox extends UIObject
1203
1235
  {
1204
1236
  super(pos, size);
1205
1237
 
1206
- ASSERT(isString(text), 'ui checkbox must be a string');
1238
+ ASSERT(isStringLike(text), 'ui checkbox must be a string');
1207
1239
  ASSERT(isColor(color), 'ui checkbox color must be a color');
1208
1240
 
1209
1241
  /** @property {boolean} - Current percentage value of this slider 0-1 */
@@ -1260,7 +1292,7 @@ class UISlider extends UIObject
1260
1292
  super(pos, size);
1261
1293
 
1262
1294
  ASSERT(isNumber(value), 'ui slider value must be a number');
1263
- ASSERT(isString(text), 'ui slider must be a string');
1295
+ ASSERT(isStringLike(text), 'ui slider must be a string');
1264
1296
  ASSERT(isColor(color), 'ui slider color must be a color');
1265
1297
  ASSERT(isColor(handleColor), 'ui slider handleColor must be a color');
1266
1298
 
@@ -1378,7 +1410,7 @@ class UIVideo extends UIObject
1378
1410
  {
1379
1411
  super(pos, size || vec2());
1380
1412
 
1381
- ASSERT(isString(src), 'video src must be a string');
1413
+ ASSERT(isStringLike(src), 'video src must be a string');
1382
1414
  ASSERT(isNumber(volume), 'video volume must be a number');
1383
1415
 
1384
1416
  this.color = BLACK; // default to black background
@@ -1495,4 +1527,133 @@ class UIVideo extends UIObject
1495
1527
  this.video.remove();
1496
1528
  super.destroy();
1497
1529
  }
1530
+ }
1531
+
1532
+ ///////////////////////////////////////////////////////////////////////////////
1533
+ /**
1534
+ * UILayout - A container that auto-arranges children in a vertical list, horizontal list, or grid
1535
+ * - Set columns to 1 for a vertical list (default)
1536
+ * - Set columns to children.length for a horizontal list
1537
+ * - Set columns to N (1 < N < children.length) for a grid with N columns
1538
+ * - Per-child sizing: each row's height = max child.size.y in that row, each column's width = max child.size.x in that column
1539
+ * - Children are positioned centered in their cell
1540
+ * - Container auto-sizes to fit children plus padding
1541
+ * @extends UIObject
1542
+ * @memberof UISystem
1543
+ */
1544
+ class UILayout extends UIObject
1545
+ {
1546
+ /** Create a UILayout container that auto-arranges children
1547
+ * @param {Vector2} [pos]
1548
+ * @param {number} [columns=1] - Number of columns (1 = vertical list)
1549
+ * @param {number} [gap=10] - Space between children
1550
+ * @param {number} [padding=10] - Space between container border and children
1551
+ * @param {boolean} [transparent=false] - If true, draws no background, outline, or shadow
1552
+ */
1553
+ constructor(pos, columns=1, gap=10, padding=10, transparent=false)
1554
+ {
1555
+ super(pos);
1556
+
1557
+ ASSERT(isNumber(columns) && columns >= 1, 'ui layout columns must be a number >= 1');
1558
+ ASSERT(isNumber(gap), 'ui layout gap must be a number');
1559
+ ASSERT(isNumber(padding), 'ui layout padding must be a number');
1560
+
1561
+ /** @property {number} - Number of columns in the layout */
1562
+ this.columns = columns;
1563
+ /** @property {number} - Space between children */
1564
+ this.gap = gap;
1565
+ /** @property {number} - Space between container border and children */
1566
+ this.padding = padding;
1567
+
1568
+ if (transparent)
1569
+ {
1570
+ // pure positioning helper - skip background, outline, and shadow
1571
+ this.color = CLEAR_BLACK;
1572
+ this.gradientColor = undefined;
1573
+ this.lineWidth = 0;
1574
+ this.shadowColor = CLEAR_BLACK;
1575
+ }
1576
+ this.relayout();
1577
+ }
1578
+
1579
+ /** Add a child UIObject and re-layout
1580
+ * @param {UIObject} child
1581
+ * @return {UIObject} The child object added */
1582
+ addChild(child)
1583
+ {
1584
+ super.addChild(child);
1585
+ this.relayout();
1586
+ return child;
1587
+ }
1588
+
1589
+ /** Remove a child UIObject and re-layout
1590
+ * @param {UIObject} child */
1591
+ removeChild(child)
1592
+ {
1593
+ super.removeChild(child);
1594
+ this.relayout();
1595
+ }
1596
+
1597
+ /** Recompute child positions and container size based on per-child sizes.
1598
+ * Called automatically by addChild and removeChild. Call manually if you
1599
+ * mutate a child's size or change columns, gap, or padding. */
1600
+ relayout()
1601
+ {
1602
+ const n = this.children.length;
1603
+ if (!n)
1604
+ {
1605
+ this.size = vec2(this.padding * 2);
1606
+ return;
1607
+ }
1608
+
1609
+ const cols = this.columns;
1610
+ const rows = ceil(n / cols);
1611
+ const colWidths = new Array(cols).fill(0);
1612
+ const rowHeights = new Array(rows).fill(0);
1613
+
1614
+ // first pass: compute column widths and row heights from child sizes
1615
+ for (let i = 0; i < n; ++i)
1616
+ {
1617
+ const col = i % cols;
1618
+ const row = floor(i / cols);
1619
+ const child = this.children[i];
1620
+ colWidths[col] = max(colWidths[col], child.size.x);
1621
+ rowHeights[row] = max(rowHeights[row], child.size.y);
1622
+ }
1623
+
1624
+ // total content size (sum of column widths/row heights plus gaps between them)
1625
+ let contentWidth = this.gap * (cols - 1);
1626
+ for (const w of colWidths) contentWidth += w;
1627
+ let contentHeight = this.gap * (rows - 1);
1628
+ for (const h of rowHeights) contentHeight += h;
1629
+
1630
+ // cumulative column/row offsets so positioning is O(n) not O(n^2)
1631
+ const colOffsets = new Array(cols);
1632
+ let xAcc = 0;
1633
+ for (let c = 0; c < cols; ++c)
1634
+ {
1635
+ colOffsets[c] = xAcc;
1636
+ xAcc += colWidths[c];
1637
+ }
1638
+ const rowOffsets = new Array(rows);
1639
+ let yAcc = 0;
1640
+ for (let r = 0; r < rows; ++r)
1641
+ {
1642
+ rowOffsets[r] = yAcc;
1643
+ yAcc += rowHeights[r];
1644
+ }
1645
+
1646
+ // second pass: position each child centered in its cell
1647
+ for (let i = 0; i < n; ++i)
1648
+ {
1649
+ const col = i % cols;
1650
+ const row = floor(i / cols);
1651
+ const x = -contentWidth/2 + colOffsets[col] + this.gap * col + colWidths[col] / 2;
1652
+ const y = -contentHeight/2 + rowOffsets[row] + this.gap * row + rowHeights[row] / 2;
1653
+ this.children[i].localPos = vec2(x, y);
1654
+ }
1655
+
1656
+ // container size = content + padding on all sides
1657
+ this.size = vec2(contentWidth + this.padding * 2, contentHeight + this.padding * 2);
1658
+ }
1498
1659
  }
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.4';
35
+ const engineVersion = '1.18.8';
36
36
 
37
37
  /** Frames per second to update
38
38
  * @type {number}
@@ -204,7 +204,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
204
204
  const combinedScale = timeScale * debugScale;
205
205
  frameTimeDeltaMS *= combinedScale;
206
206
  frameTimeBufferMS += paused ? 0 : frameTimeDeltaMS;
207
- if (combinedScale <= 1)
207
+ if (debugScale <= 1)
208
208
  frameTimeBufferMS = min(frameTimeBufferMS, 50); // clamp min framerate
209
209
 
210
210
  let wasUpdated = false;
@@ -562,143 +562,4 @@ function engineObjectsRaycast(start, end, objects=engineObjects)
562
562
 
563
563
  debugRaycast && debugLine(start, end, hitObjects.length ? '#f00' : '#00f', .02);
564
564
  return hitObjects;
565
- }
566
-
567
- ///////////////////////////////////////////////////////////////////////////////
568
- function drawEngineLogo(t)
569
- {
570
- const blackAndWhite = 0;
571
- const showName = 1;
572
-
573
- // LittleJS Logo and Splash Screen
574
- const x = mainContext;
575
- const dpr = canvasPixelRatio ?? (devicePixelRatio || 1);
576
- const w = mainCanvas.width = innerWidth * dpr;
577
- const h = mainCanvas.height = innerHeight * dpr;
578
- {
579
- // background
580
- const p3 = percent(t, 1, .8);
581
- const p4 = percent(t, 0, .5);
582
- const g = x.createRadialGradient(w/2,h/2,0,w/2,h/2,hypot(w,h)*.6);
583
- g.addColorStop(0,hsl(0,0,lerp(0,p3/2,p4),p3).toString());
584
- g.addColorStop(1,hsl(0,0,0,p3).toString());
585
- x.save();
586
- x.fillStyle = g;
587
- x.fillRect(0,0,w,h);
588
- }
589
- const gradient = (X1,Y1,X2,Y2,C,S=1)=>
590
- {
591
- if (C >= 0)
592
- {
593
- if (blackAndWhite)
594
- x.fillStyle = '#fff';
595
- else
596
- {
597
- const g = x.fillStyle = x.createLinearGradient(X1,Y1,X2,Y2);
598
- g.addColorStop(0,color(C,2));
599
- g.addColorStop(1,color(C,1));
600
- }
601
- }
602
- else
603
- x.fillStyle = '#000';
604
- C >= -1 ? (x.fill(), S && x.stroke()) : x.stroke();
605
- }
606
- const circle = (X,Y,R,A=0,B=2*PI,C,S)=>
607
- {
608
- x.beginPath();
609
- x.arc(X,Y,R,p*A,p*B);
610
- gradient(X,Y-R,X,Y+R,C,S);
611
- }
612
- const rect = (X,Y,W,H,C)=>
613
- {
614
- x.beginPath();
615
- x.rect(X,Y,W,H*p);
616
- gradient(X,Y+H,X+W,Y,C);
617
- }
618
- const poly = (points,C,Y,H)=>
619
- {
620
- x.beginPath();
621
- for (const p of points)
622
- x.lineTo(p.x, p.y);
623
- x.closePath();
624
- gradient(0, Y, 0, Y+H,C);
625
- }
626
- const color = (c,l)=> l?`hsl(${[.95,.56,.13][c%3]*360} 99%${[0,50,75][l]}%`:'#000';
627
-
628
- // center and fit tos screen
629
- const alpha = oscillate(1,1,t);
630
- const p = percent(alpha, .1, .5);
631
- const size = min(6, min(w,h)/99);
632
- x.translate(w/2,h/2);
633
- x.scale(size,size);
634
- x.translate(-40,-35);
635
- p < 1 && x.setLineDash([99*p,99]);
636
- x.lineJoin = x.lineCap = 'round';
637
- x.lineWidth = .1 + p*1.9;
638
- //x.strokeStyle='#fff7';
639
-
640
- if (showName)
641
- {
642
- // engine name text
643
- const Y = 54;
644
- const s = 'LittleJS';
645
- x.font = '900 15.5px arial';
646
- x.lineWidth = .1+p*3.9;
647
- x.textAlign = 'center';
648
- x.textBaseline = 'top';
649
- rect(11,Y+1,59,8*p,-1);
650
- x.beginPath();
651
-
652
- let w2 = 0;
653
- for (let i=0;i<s.length;++i)
654
- w2 += x.measureText(s[i]).width;
655
- for (let j=2;j--;)
656
- for (let i=0,X=40-w2/2;i<s.length;++i)
657
- {
658
- const w = x.measureText(s[i]).width, X2 = X+w/2;
659
- gradient(X2,Y,X2+2,Y+13,i>5?1:0);
660
- x[j?'strokeText':'fillText'](s[i],X2,Y+.5,17*p);
661
- X += w;
662
- }
663
-
664
- x.lineWidth = .1 + p*1.9;
665
- rect(3,Y,73,0); // bottom
666
- }
667
-
668
- rect(7,15,26,-7,0); // cab top
669
- rect(25,15,8,25,-1); // cab front
670
- rect(10,40,15,-25,1); // cab back
671
- rect(14,21,7,9,2); // cab window
672
- rect(38,20,6,-6,2); // little stack
673
-
674
- // big stack
675
- rect(49,20,10,-6,0);
676
- const stackPoints = [vec2(44,8),vec2(64,8),vec2(59,8+6*p),vec2(49,8+6*p)];
677
- poly(stackPoints,2,8,6*p);
678
- rect(44,8,20,-7,0);
679
-
680
- // engine
681
- for (let i=5;i--;) circle(59-i*6*p,30,10,0,2*PI,1,0);
682
- circle(59,30,4,0,7,2); // light
683
-
684
- // engine outline
685
- rect(35,20,24,0); // top
686
- circle(59,30,10); // front
687
- circle(47,30,10,PI/2,PI*3/2); // middle
688
- circle(35,30,10,PI/2,PI*3/2); // back
689
- rect(7,40,13,7,-1); // bottom back
690
- rect(17,40,43,14,-1); // bottom center
691
-
692
- // wheels
693
- for (let i=3;i--;) for (let j=2;j--;) circle(17+15*i,47,j?7:1,0,2*PI,2);
694
-
695
- // cowcatcher
696
- for (let i=2;i--;)
697
- {
698
- let w=6, s=7, o=53+w*p*i
699
- const points = [vec2(o+s,54),vec2(o,40),vec2(o+w*p,40),vec2(o+s+w*p,54)];
700
- poly(points,0,40,14);
701
- }
702
-
703
- x.restore();
704
565
  }
@@ -84,7 +84,7 @@ class Sound
84
84
  {
85
85
  if (!soundEnable || headlessMode) return;
86
86
 
87
- ASSERT(!asset || isArray(asset) || isString(asset), 'asset must be a file name or zzfx array');
87
+ ASSERT(!asset || isArray(asset) || isStringLike(asset), 'asset must be a file name or zzfx array');
88
88
  ASSERT(randomness === undefined || isNumber(randomness), 'randomness must be a number');
89
89
  ASSERT(randomness === undefined || randomness >= 0 && randomness <=1, 'randomness must be between 0 and 1');
90
90
  ASSERT(isNumber(range), 'range must be a number');
@@ -105,8 +105,8 @@ class Sound
105
105
 
106
106
  if (Array.isArray(asset))
107
107
  {
108
- // generate zzfx sound
109
- const zzfxSound = asset;
108
+ // generate zzfx sound — copy so we don't mutate the caller's array
109
+ const zzfxSound = asset.slice();
110
110
 
111
111
  // remove randomness so it can be applied on playback
112
112
  const defaultRandomness = randomness ?? .05;
@@ -33,18 +33,20 @@ const engineSourceFiles =
33
33
  `${SOURCE_FOLDER}/engineAudio.js`,
34
34
  `${SOURCE_FOLDER}/engineTileLayer.js`,
35
35
  `${SOURCE_FOLDER}/engineParticles.js`,
36
- `${SOURCE_FOLDER}/engineMedals.js`,
37
36
  `${SOURCE_FOLDER}/engineWebGL.js`,
37
+ `${SOURCE_FOLDER}/engineLogo.js`,
38
38
  ];
39
39
  const enginePluginFiles =
40
40
  [
41
+ `${PLUGIN_FOLDER}/medalSystem.js`,
41
42
  `${PLUGIN_FOLDER}/newgrounds.js`,
42
43
  `${PLUGIN_FOLDER}/postProcess.js`,
43
44
  `${PLUGIN_FOLDER}/zzfxm.js`,
44
45
  `${PLUGIN_FOLDER}/uiSystem.js`,
45
46
  `${PLUGIN_FOLDER}/box2d.js`,
46
47
  `${PLUGIN_FOLDER}/drawUtilities.js`,
47
- `${PLUGIN_FOLDER}/tween.js`,
48
+ `${PLUGIN_FOLDER}/tweenSystem.js`,
49
+ `${PLUGIN_FOLDER}/pathFinder.js`,
48
50
  ];
49
51
  const engineExtraFiles =
50
52
  [
@@ -44,7 +44,7 @@ let debugKey = 'Escape';
44
44
  let debugOverlay = false;
45
45
 
46
46
  // Engine internal variables not exposed to documentation
47
- let debugPrimitives = [], debugPhysics = false, debugRaycast = false, debugParticles = false, debugGamepads = false, debugMedals = false, debugTakeScreenshot;
47
+ let debugPrimitives = [], debugPhysics = false, debugRaycast = false, debugParticles = false, debugGamepads = false, debugTakeScreenshot;
48
48
 
49
49
  ///////////////////////////////////////////////////////////////////////////////
50
50
  // Debug helper functions
@@ -79,7 +79,7 @@ function debugRect(pos, size=vec2(), color=WHITE, time=0, angle=0, fill=false, s
79
79
  {
80
80
  ASSERT(isVector2(pos), 'pos must be a vec2');
81
81
  ASSERT(isVector2(size), 'size must be a vec2');
82
- ASSERT(isString(color) || isColor(color), 'color is invalid');
82
+ ASSERT(isStringLike(color) || isColor(color), 'color is invalid');
83
83
  ASSERT(isNumber(time), 'time must be a number');
84
84
  ASSERT(isNumber(angle), 'angle must be a number');
85
85
 
@@ -106,7 +106,7 @@ function debugPoly(pos, points, color=WHITE, time=0, angle=0, fill=false, screen
106
106
  {
107
107
  ASSERT(isVector2(pos), 'pos must be a vec2');
108
108
  ASSERT(isArray(points), 'points must be an array');
109
- ASSERT(isString(color) || isColor(color), 'color is invalid');
109
+ ASSERT(isStringLike(color) || isColor(color), 'color is invalid');
110
110
  ASSERT(isNumber(time), 'time must be a number');
111
111
  ASSERT(isNumber(angle), 'angle must be a number');
112
112
 
@@ -130,7 +130,7 @@ function debugCircle(pos, size=0, color=WHITE, time=0, fill=false, screenSpace=f
130
130
  {
131
131
  ASSERT(isVector2(pos), 'pos must be a vec2');
132
132
  ASSERT(isNumber(size), 'size must be a number');
133
- ASSERT(isString(color) || isColor(color), 'color is invalid');
133
+ ASSERT(isStringLike(color) || isColor(color), 'color is invalid');
134
134
  ASSERT(isNumber(time), 'time must be a number');
135
135
 
136
136
  if (isColor(color))
@@ -208,13 +208,13 @@ function debugOverlap(posA, sizeA, posB, sizeB, color, time, screenSpace=false)
208
208
  * @memberof Debug */
209
209
  function debugText(text, pos, size=1, color=WHITE, time=0, angle=0, font='monospace', screenSpace=false)
210
210
  {
211
- ASSERT(isString(text), 'text must be a string');
211
+ ASSERT(isStringLike(text), 'text must be a string');
212
212
  ASSERT(isVector2(pos), 'pos must be a vec2');
213
213
  ASSERT(isNumber(size), 'size must be a number');
214
- ASSERT(isString(color) || isColor(color), 'color is invalid');
214
+ ASSERT(isStringLike(color) || isColor(color), 'color is invalid');
215
215
  ASSERT(isNumber(time), 'time must be a number');
216
216
  ASSERT(isNumber(angle), 'angle must be a number');
217
- ASSERT(isString(font), 'font must be a string');
217
+ ASSERT(isStringLike(font), 'font must be a string');
218
218
 
219
219
  if (isColor(color))
220
220
  color = color.toString();