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
@@ -118,16 +118,13 @@ 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
128
  // set object position in parent space
132
129
  function updateTransforms(o)
133
130
  {
@@ -469,8 +466,23 @@ class UISystemPlugin
469
466
  return p;
470
467
  }
471
468
 
469
+ /** Object to send keyboard input to (typically a UITextInput).
470
+ * The document keydown listener is only attached while this is set,
471
+ * so games that never use text input pay no event-handling cost.
472
+ * @type {UIObject} */
473
+ get keyInputObject() { return this._keyInputObject; }
474
+ set keyInputObject(obj)
475
+ {
476
+ const had = !!this._keyInputObject;
477
+ this._keyInputObject = obj;
478
+ if (!had && obj)
479
+ document.addEventListener('keydown', this._onKeyDown);
480
+ else if (had && !obj)
481
+ document.removeEventListener('keydown', this._onKeyDown);
482
+ }
483
+
472
484
  /** Destroy and remove all objects
473
- * @memberof Engine */
485
+ * @memberof UISystem */
474
486
  destroyObjects()
475
487
  {
476
488
  for (const o of this.uiObjects)
@@ -893,15 +905,13 @@ class UIObject
893
905
  * @return {string} */
894
906
  toString()
895
907
  {
896
- if (!debug) return;
897
-
898
908
  let text = 'type = ' + this.constructor.name;
899
909
  if (this.text)
900
910
  text += '\ntext = ' + this.text;
901
911
  if (this.pos.x || this.pos.y)
902
912
  text += '\npos = ' + this.pos;
903
913
  if (this.localPos.x || this.localPos.y)
904
- text += '\localPos = ' + this.localPos;
914
+ text += '\nlocalPos = ' + this.localPos;
905
915
  if (this.size.x || this.size.y)
906
916
  text += '\nsize = ' + this.size;
907
917
  if (this.color)
@@ -924,7 +934,7 @@ class UIObject
924
934
 
925
935
  /** Internal function called when object is clicked
926
936
  * @param {boolean} [playSound] */
927
- click(playSound)
937
+ click(playSound=true)
928
938
  {
929
939
  this.onClick();
930
940
  if (playSound && this.soundClick)
@@ -1495,4 +1505,133 @@ class UIVideo extends UIObject
1495
1505
  this.video.remove();
1496
1506
  super.destroy();
1497
1507
  }
1508
+ }
1509
+
1510
+ ///////////////////////////////////////////////////////////////////////////////
1511
+ /**
1512
+ * UILayout - A container that auto-arranges children in a vertical list, horizontal list, or grid
1513
+ * - Set columns to 1 for a vertical list (default)
1514
+ * - Set columns to children.length for a horizontal list
1515
+ * - Set columns to N (1 < N < children.length) for a grid with N columns
1516
+ * - 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
1517
+ * - Children are positioned centered in their cell
1518
+ * - Container auto-sizes to fit children plus padding
1519
+ * @extends UIObject
1520
+ * @memberof UISystem
1521
+ */
1522
+ class UILayout extends UIObject
1523
+ {
1524
+ /** Create a UILayout container that auto-arranges children
1525
+ * @param {Vector2} [pos]
1526
+ * @param {number} [columns=1] - Number of columns (1 = vertical list)
1527
+ * @param {number} [gap=10] - Space between children
1528
+ * @param {number} [padding=10] - Space between container border and children
1529
+ * @param {boolean} [transparent=false] - If true, draws no background, outline, or shadow
1530
+ */
1531
+ constructor(pos, columns=1, gap=10, padding=10, transparent=false)
1532
+ {
1533
+ super(pos);
1534
+
1535
+ ASSERT(isNumber(columns) && columns >= 1, 'ui layout columns must be a number >= 1');
1536
+ ASSERT(isNumber(gap), 'ui layout gap must be a number');
1537
+ ASSERT(isNumber(padding), 'ui layout padding must be a number');
1538
+
1539
+ /** @property {number} - Number of columns in the layout */
1540
+ this.columns = columns;
1541
+ /** @property {number} - Space between children */
1542
+ this.gap = gap;
1543
+ /** @property {number} - Space between container border and children */
1544
+ this.padding = padding;
1545
+
1546
+ if (transparent)
1547
+ {
1548
+ // pure positioning helper - skip background, outline, and shadow
1549
+ this.color = CLEAR_BLACK;
1550
+ this.gradientColor = undefined;
1551
+ this.lineWidth = 0;
1552
+ this.shadowColor = CLEAR_BLACK;
1553
+ }
1554
+ this.relayout();
1555
+ }
1556
+
1557
+ /** Add a child UIObject and re-layout
1558
+ * @param {UIObject} child
1559
+ * @return {UIObject} The child object added */
1560
+ addChild(child)
1561
+ {
1562
+ super.addChild(child);
1563
+ this.relayout();
1564
+ return child;
1565
+ }
1566
+
1567
+ /** Remove a child UIObject and re-layout
1568
+ * @param {UIObject} child */
1569
+ removeChild(child)
1570
+ {
1571
+ super.removeChild(child);
1572
+ this.relayout();
1573
+ }
1574
+
1575
+ /** Recompute child positions and container size based on per-child sizes.
1576
+ * Called automatically by addChild and removeChild. Call manually if you
1577
+ * mutate a child's size or change columns, gap, or padding. */
1578
+ relayout()
1579
+ {
1580
+ const n = this.children.length;
1581
+ if (!n)
1582
+ {
1583
+ this.size = vec2(this.padding * 2);
1584
+ return;
1585
+ }
1586
+
1587
+ const cols = this.columns;
1588
+ const rows = ceil(n / cols);
1589
+ const colWidths = new Array(cols).fill(0);
1590
+ const rowHeights = new Array(rows).fill(0);
1591
+
1592
+ // first pass: compute column widths and row heights from child sizes
1593
+ for (let i = 0; i < n; ++i)
1594
+ {
1595
+ const col = i % cols;
1596
+ const row = floor(i / cols);
1597
+ const child = this.children[i];
1598
+ colWidths[col] = max(colWidths[col], child.size.x);
1599
+ rowHeights[row] = max(rowHeights[row], child.size.y);
1600
+ }
1601
+
1602
+ // total content size (sum of column widths/row heights plus gaps between them)
1603
+ let contentWidth = this.gap * (cols - 1);
1604
+ for (const w of colWidths) contentWidth += w;
1605
+ let contentHeight = this.gap * (rows - 1);
1606
+ for (const h of rowHeights) contentHeight += h;
1607
+
1608
+ // cumulative column/row offsets so positioning is O(n) not O(n^2)
1609
+ const colOffsets = new Array(cols);
1610
+ let xAcc = 0;
1611
+ for (let c = 0; c < cols; ++c)
1612
+ {
1613
+ colOffsets[c] = xAcc;
1614
+ xAcc += colWidths[c];
1615
+ }
1616
+ const rowOffsets = new Array(rows);
1617
+ let yAcc = 0;
1618
+ for (let r = 0; r < rows; ++r)
1619
+ {
1620
+ rowOffsets[r] = yAcc;
1621
+ yAcc += rowHeights[r];
1622
+ }
1623
+
1624
+ // second pass: position each child centered in its cell
1625
+ for (let i = 0; i < n; ++i)
1626
+ {
1627
+ const col = i % cols;
1628
+ const row = floor(i / cols);
1629
+ const x = -contentWidth/2 + colOffsets[col] + this.gap * col + colWidths[col] / 2;
1630
+ const y = -contentHeight/2 + rowOffsets[row] + this.gap * row + rowHeights[row] / 2;
1631
+ this.children[i].localPos = vec2(x, y);
1632
+ }
1633
+
1634
+ // container size = content + padding on all sides
1635
+ this.size = vec2(contentWidth + this.padding * 2, contentHeight + this.padding * 2);
1636
+ }
1498
1637
  }
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.2';
35
+ const engineVersion = '1.18.7';
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 (debugScale <= 1)
205
208
  frameTimeBufferMS = min(frameTimeBufferMS, 50); // clamp min framerate
206
209
 
207
210
  let wasUpdated = false;
@@ -559,143 +562,4 @@ function engineObjectsRaycast(start, end, objects=engineObjects)
559
562
 
560
563
  debugRaycast && debugLine(start, end, hitObjects.length ? '#f00' : '#00f', .02);
561
564
  return hitObjects;
562
- }
563
-
564
- ///////////////////////////////////////////////////////////////////////////////
565
- function drawEngineLogo(t)
566
- {
567
- const blackAndWhite = 0;
568
- const showName = 1;
569
-
570
- // LittleJS Logo and Splash Screen
571
- const x = mainContext;
572
- const dpr = canvasPixelRatio ?? (devicePixelRatio || 1);
573
- const w = mainCanvas.width = innerWidth * dpr;
574
- const h = mainCanvas.height = innerHeight * dpr;
575
- {
576
- // background
577
- const p3 = percent(t, 1, .8);
578
- const p4 = percent(t, 0, .5);
579
- const g = x.createRadialGradient(w/2,h/2,0,w/2,h/2,hypot(w,h)*.6);
580
- g.addColorStop(0,hsl(0,0,lerp(0,p3/2,p4),p3).toString());
581
- g.addColorStop(1,hsl(0,0,0,p3).toString());
582
- x.save();
583
- x.fillStyle = g;
584
- x.fillRect(0,0,w,h);
585
- }
586
- const gradient = (X1,Y1,X2,Y2,C,S=1)=>
587
- {
588
- if (C >= 0)
589
- {
590
- if (blackAndWhite)
591
- x.fillStyle = '#fff';
592
- else
593
- {
594
- const g = x.fillStyle = x.createLinearGradient(X1,Y1,X2,Y2);
595
- g.addColorStop(0,color(C,2));
596
- g.addColorStop(1,color(C,1));
597
- }
598
- }
599
- else
600
- x.fillStyle = '#000';
601
- C >= -1 ? (x.fill(), S && x.stroke()) : x.stroke();
602
- }
603
- const circle = (X,Y,R,A=0,B=2*PI,C,S)=>
604
- {
605
- x.beginPath();
606
- x.arc(X,Y,R,p*A,p*B);
607
- gradient(X,Y-R,X,Y+R,C,S);
608
- }
609
- const rect = (X,Y,W,H,C)=>
610
- {
611
- x.beginPath();
612
- x.rect(X,Y,W,H*p);
613
- gradient(X,Y+H,X+W,Y,C);
614
- }
615
- const poly = (points,C,Y,H)=>
616
- {
617
- x.beginPath();
618
- for (const p of points)
619
- x.lineTo(p.x, p.y);
620
- x.closePath();
621
- gradient(0, Y, 0, Y+H,C);
622
- }
623
- const color = (c,l)=> l?`hsl(${[.95,.56,.13][c%3]*360} 99%${[0,50,75][l]}%`:'#000';
624
-
625
- // center and fit tos screen
626
- const alpha = oscillate(1,1,t);
627
- const p = percent(alpha, .1, .5);
628
- const size = min(6, min(w,h)/99);
629
- x.translate(w/2,h/2);
630
- x.scale(size,size);
631
- x.translate(-40,-35);
632
- p < 1 && x.setLineDash([99*p,99]);
633
- x.lineJoin = x.lineCap = 'round';
634
- x.lineWidth = .1 + p*1.9;
635
- //x.strokeStyle='#fff7';
636
-
637
- if (showName)
638
- {
639
- // engine name text
640
- const Y = 54;
641
- const s = 'LittleJS';
642
- x.font = '900 15.5px arial';
643
- x.lineWidth = .1+p*3.9;
644
- x.textAlign = 'center';
645
- x.textBaseline = 'top';
646
- rect(11,Y+1,59,8*p,-1);
647
- x.beginPath();
648
-
649
- let w2 = 0;
650
- for (let i=0;i<s.length;++i)
651
- w2 += x.measureText(s[i]).width;
652
- for (let j=2;j--;)
653
- for (let i=0,X=40-w2/2;i<s.length;++i)
654
- {
655
- const w = x.measureText(s[i]).width, X2 = X+w/2;
656
- gradient(X2,Y,X2+2,Y+13,i>5?1:0);
657
- x[j?'strokeText':'fillText'](s[i],X2,Y+.5,17*p);
658
- X += w;
659
- }
660
-
661
- x.lineWidth = .1 + p*1.9;
662
- rect(3,Y,73,0); // bottom
663
- }
664
-
665
- rect(7,15,26,-7,0); // cab top
666
- rect(25,15,8,25,-1); // cab front
667
- rect(10,40,15,-25,1); // cab back
668
- rect(14,21,7,9,2); // cab window
669
- rect(38,20,6,-6,2); // little stack
670
-
671
- // big stack
672
- rect(49,20,10,-6,0);
673
- const stackPoints = [vec2(44,8),vec2(64,8),vec2(59,8+6*p),vec2(49,8+6*p)];
674
- poly(stackPoints,2,8,6*p);
675
- rect(44,8,20,-7,0);
676
-
677
- // engine
678
- for (let i=5;i--;) circle(59-i*6*p,30,10,0,2*PI,1,0);
679
- circle(59,30,4,0,7,2); // light
680
-
681
- // engine outline
682
- rect(35,20,24,0); // top
683
- circle(59,30,10); // front
684
- circle(47,30,10,PI/2,PI*3/2); // middle
685
- circle(35,30,10,PI/2,PI*3/2); // back
686
- rect(7,40,13,7,-1); // bottom back
687
- rect(17,40,43,14,-1); // bottom center
688
-
689
- // wheels
690
- for (let i=3;i--;) for (let j=2;j--;) circle(17+15*i,47,j?7:1,0,2*PI,2);
691
-
692
- // cowcatcher
693
- for (let i=2;i--;)
694
- {
695
- let w=6, s=7, o=53+w*p*i
696
- const points = [vec2(o+s,54),vec2(o,40),vec2(o+w*p,40),vec2(o+s+w*p,54)];
697
- poly(points,0,40,14);
698
- }
699
-
700
- x.restore();
701
565
  }
@@ -14,10 +14,12 @@
14
14
 
15
15
  'use strict';
16
16
 
17
- /** Audio context used by the engine
17
+ /** Audio context used by the engine. Created lazily in audioInit() to avoid
18
+ * browser autoplay warnings about constructing an AudioContext before any
19
+ * user gesture.
18
20
  * @type {AudioContext}
19
21
  * @memberof Audio */
20
- let audioContext = new AudioContext;
22
+ let audioContext;
21
23
 
22
24
  /** Master gain node for all audio to pass through
23
25
  * @type {GainNode}
@@ -33,12 +35,13 @@ const audioDefaultSampleRate = 44100;
33
35
  * @return {boolean} - True if the audio context is running
34
36
  * @memberof Audio */
35
37
  function audioIsRunning()
36
- { return audioContext.state === 'running'; }
38
+ { return audioContext?.state === 'running'; }
37
39
 
38
40
  function audioInit()
39
41
  {
40
42
  if (!soundEnable || headlessMode) return;
41
43
 
44
+ audioContext = new AudioContext;
42
45
  audioMasterGain = audioContext.createGain();
43
46
  audioMasterGain.connect(audioContext.destination);
44
47
  audioMasterGain.gain.value = soundVolume; // set starting value
@@ -105,8 +108,8 @@ class Sound
105
108
 
106
109
  if (Array.isArray(asset))
107
110
  {
108
- // generate zzfx sound
109
- const zzfxSound = asset;
111
+ // generate zzfx sound — copy so we don't mutate the caller's array
112
+ const zzfxSound = asset.slice();
110
113
 
111
114
  // remove randomness so it can be applied on playback
112
115
  const defaultRandomness = randomness ?? .05;
@@ -33,17 +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`,
48
+ `${PLUGIN_FOLDER}/tweenSystem.js`,
49
+ `${PLUGIN_FOLDER}/pathFinder.js`,
47
50
  ];
48
51
  const engineExtraFiles =
49
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
package/src/engineDraw.js CHANGED
@@ -72,6 +72,12 @@ let textureInfos = [];
72
72
  * @memberof Draw */
73
73
  let drawCount;
74
74
 
75
+ // internal predicates for tint short-circuiting in canvas2D draw paths
76
+ // isWhite ignores alpha because alpha is applied via globalAlpha, not multiply
77
+ // isBlack includes alpha so additive colors that only contribute alpha are not skipped
78
+ /** @param {Color} c */ function isWhite(c) { return c.r >= 1 && c.g >= 1 && c.b >= 1; }
79
+ /** @param {Color} c */ function isBlack(c) { return c.r <= 0 && c.g <= 0 && c.b <= 0 && c.a <= 0; }
80
+
75
81
  ///////////////////////////////////////////////////////////////////////////////
76
82
 
77
83
  /**
@@ -211,8 +217,9 @@ class TextureInfo
211
217
  * Create a TextureInfo, called automatically by the engine
212
218
  * @param {HTMLImageElement|OffscreenCanvas} image
213
219
  * @param {boolean} [useWebGL] - Should use WebGL if available?
220
+ * @param {boolean} [wrap] - Should the texture wrap (REPEAT) or clamp (CLAMP_TO_EDGE)?
214
221
  */
215
- constructor(image, useWebGL=true)
222
+ constructor(image, useWebGL=true, wrap=false)
216
223
  {
217
224
  /** @property {HTMLImageElement|OffscreenCanvas} - image source */
218
225
  this.image = image;
@@ -222,6 +229,8 @@ class TextureInfo
222
229
  this.sizeInverse = image ? vec2(1/image.width, 1/image.height) : vec2();
223
230
  /** @property {WebGLTexture} - WebGL texture */
224
231
  this.glTexture = undefined;
232
+ /** @property {boolean} - true for REPEAT wrap mode, false for CLAMP_TO_EDGE */
233
+ this.wrap = wrap;
225
234
  useWebGL && this.createWebGLTexture();
226
235
  }
227
236
 
@@ -234,6 +243,14 @@ class TextureInfo
234
243
  /** Check if the texture is webgl enabled
235
244
  * @return {boolean} */
236
245
  hasWebGL() { return !!this.glTexture; }
246
+
247
+ /** Set the wrap mode for this texture
248
+ * @param {boolean} [wrap] - true for REPEAT, false for CLAMP_TO_EDGE */
249
+ setWrap(wrap=true)
250
+ {
251
+ this.wrap = wrap;
252
+ glSetTextureWrap(this.glTexture, wrap);
253
+ }
237
254
  }
238
255
 
239
256
  ///////////////////////////////////////////////////////////////////////////////
@@ -295,8 +312,13 @@ function drawTile(pos, size=vec2(1), tileInfo, color=WHITE,
295
312
  }
296
313
  else
297
314
  {
298
- // if no tile info, force untextured
299
- glDraw(pos.x, pos.y, size.x, size.y, angle, 0, 0, 0, 0, 0, color.rgbaInt());
315
+ // if no tile info, force untextured by zeroing rgba (so whatever
316
+ // texture is bound doesn't leak in) and folding color+additive
317
+ // into the additive slot — matches the Canvas2D path's
318
+ // color.add(additiveColor) on line ~337.
319
+ const combined = additiveColor ? color.add(additiveColor) : color;
320
+ glDraw(pos.x, pos.y, size.x, size.y, angle, 0, 0, 0, 0,
321
+ 0, combined.rgbaInt());
300
322
  }
301
323
  }
302
324
  else
@@ -401,6 +423,98 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
401
423
  }
402
424
  }
403
425
 
426
+ /** Draw a texture tiled (wrapped) across a rectangle in world space.
427
+ * Useful for backgrounds, repeating patterns, and seamless fills.
428
+ * The whole texture is tiled — sub-region (TileInfo) wrapping is not supported.
429
+ * @param {Vector2} pos - Center of the rect in world space
430
+ * @param {Vector2} size - Size of the rect in world space
431
+ * @param {Vector2} wrapCount - How many times the texture repeats (x, y)
432
+ * @param {TextureInfo|number} [texture=0] - TextureInfo or texture index into textureInfos
433
+ * @param {Color} [color=WHITE] - Color to modulate with
434
+ * @param {number} [angle=0] - Angle to rotate by
435
+ * @param {Color} [additiveColor] - Additive color to be applied if any
436
+ * @param {boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering?
437
+ * @param {boolean} [screenSpace=false] - Are pos and size in screen space?
438
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas 2D context to draw to
439
+ * @memberof Draw */
440
+ function drawTextureWrapped(pos, size, wrapCount, texture=0, color=WHITE,
441
+ angle=0, additiveColor, useWebGL=glEnable, screenSpace=false, context)
442
+ {
443
+ ASSERT(isVector2(pos), 'pos must be a vec2');
444
+ ASSERT(isVector2(size), 'size must be a vec2');
445
+ ASSERT(isVector2(wrapCount), 'wrapCount must be a vec2');
446
+ ASSERT(isColor(color), 'color is invalid');
447
+ ASSERT(isNumber(angle), 'angle must be a number');
448
+ ASSERT(!additiveColor || isColor(additiveColor), 'additiveColor must be a color');
449
+ ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
450
+ ASSERT(!(texture instanceof TileInfo),
451
+ 'pass a TextureInfo or texture index, not a TileInfo — use tileInfo.textureInfo');
452
+
453
+ // short-circuit before texture lookup — textureInfos[0] is undefined in headless mode
454
+ if (headlessMode) return;
455
+
456
+ // resolve texture argument: TextureInfo or index
457
+ const textureInfo = typeof texture === 'number' ? textureInfos[texture] : texture;
458
+ ASSERT(textureInfo instanceof TextureInfo, 'texture not loaded');
459
+ ASSERT(textureInfo.size.x > 0, 'texture not loaded');
460
+ ASSERT(textureInfo.wrap,
461
+ 'drawTextureWrapped requires a wrap-enabled texture; call textureInfo.setWrap(true) first');
462
+
463
+ if (useWebGL && glEnable)
464
+ {
465
+ ASSERT(!!glContext, 'WebGL is not enabled!');
466
+ if (screenSpace)
467
+ [pos, size, angle] = screenToWorldTransform(pos, size, angle);
468
+ glSetTexture(textureInfo.glTexture);
469
+ glDraw(pos.x, pos.y, size.x, size.y, angle,
470
+ 0, 0, wrapCount.x, wrapCount.y,
471
+ color.rgbaInt(), additiveColor && additiveColor.rgbaInt());
472
+ return;
473
+ }
474
+
475
+ // Canvas2D path — increment drawCount here (WebGL batch counts via glBatchCount)
476
+ ++drawCount;
477
+
478
+ if (!screenSpace)
479
+ {
480
+ pos = worldToScreen(pos);
481
+ size = size.scale(cameraScale);
482
+ angle -= cameraAngle;
483
+ }
484
+
485
+ // pick image source: raw, or tinted bake. Match drawImageColor's
486
+ // "no tint needed" predicate so behavior stays consistent.
487
+ const noTint = !canvasColorTiles ||
488
+ (additiveColor
489
+ ? isWhite(color.add(additiveColor)) && additiveColor.a <= 0
490
+ : isWhite(color));
491
+ // alpha is baked into pixels by bakeTintedImage's additive branch;
492
+ // in that case globalAlpha must NOT also apply color.a
493
+ const alphaBaked = !noTint && additiveColor && !isBlack(additiveColor);
494
+ const source = noTint
495
+ ? textureInfo.image
496
+ : bakeTintedImage(textureInfo.image, color, additiveColor);
497
+
498
+ context = context || drawContext;
499
+ context.save();
500
+ context.translate(pos.x + .5, pos.y + .5);
501
+ context.rotate(angle);
502
+ context.globalAlpha = alphaBaked ? 1 : color.a;
503
+
504
+ const pattern = context.createPattern(source, 'repeat');
505
+ // map pattern-source pixels into user space so the rect contains
506
+ // wrapCount.x × wrapCount.y repeats
507
+ const m = new DOMMatrix()
508
+ .translate(-size.x/2, -size.y/2)
509
+ .scale(size.x / (wrapCount.x * source.width),
510
+ size.y / (wrapCount.y * source.height));
511
+ pattern.setTransform(m);
512
+ context.fillStyle = pattern;
513
+ context.fillRect(-size.x/2, -size.y/2, size.x, size.y);
514
+ context.globalAlpha = 1;
515
+ context.restore();
516
+ }
517
+
404
518
  /** Draw connected lines between a series of points
405
519
  * @param {Array<Vector2>} points
406
520
  * @param {number} [width]
@@ -924,6 +1038,43 @@ function combineCanvases()
924
1038
  mainContext.drawImage(workCanvas, 0, 0);
925
1039
  }
926
1040
 
1041
+ // Internal: bake a color/additive-color tint into workReadCanvas at the
1042
+ // image's native resolution. Returns the work canvas, suitable for
1043
+ // passing to context.createPattern. Used by drawTextureWrapped's
1044
+ // Canvas2D path. Caller is responsible for short-circuiting when no
1045
+ // tint is needed (i.e. color is white and additiveColor is black/none).
1046
+ function bakeTintedImage(image, color, additiveColor)
1047
+ {
1048
+ const w = image.width|0, h = image.height|0;
1049
+ workReadCanvas.width = w;
1050
+ workReadCanvas.height = h;
1051
+ workReadContext.drawImage(image, 0, 0);
1052
+
1053
+ const imageData = workReadContext.getImageData(0, 0, w, h);
1054
+ const data = imageData.data;
1055
+ if (additiveColor && !isBlack(additiveColor))
1056
+ {
1057
+ // multiply + additive (slower)
1058
+ const colorMultiply = [color.r, color.g, color.b, color.a];
1059
+ const colorAdd = [additiveColor.r * 255, additiveColor.g * 255,
1060
+ additiveColor.b * 255, additiveColor.a * 255];
1061
+ for (let i = 0; i < data.length; ++i)
1062
+ data[i] = data[i] * colorMultiply[i&3] + colorAdd[i&3] |0;
1063
+ }
1064
+ else
1065
+ {
1066
+ // RGB only, faster — alpha left intact for the caller
1067
+ for (let i = 0; i < data.length; i+=4)
1068
+ {
1069
+ data[i ] *= color.r;
1070
+ data[i+1] *= color.g;
1071
+ data[i+2] *= color.b;
1072
+ }
1073
+ }
1074
+ workReadContext.putImageData(imageData, 0, 0);
1075
+ return workReadCanvas;
1076
+ }
1077
+
927
1078
  /** Helper function to draw an image with color and additive color applied
928
1079
  * This is slower then normal drawImage when color is applied
929
1080
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context
@@ -942,8 +1093,6 @@ function combineCanvases()
942
1093
  * @memberof Draw */
943
1094
  function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight, color, additiveColor, bleed=0)
944
1095
  {
945
- function isWhite(c) { return c.r >= 1 && c.g >= 1 && c.b >= 1; }
946
- function isBlack(c) { return c.r <= 0 && c.g <= 0 && c.b <= 0 && c.a <= 0; }
947
1096
  const sx2 = bleed;
948
1097
  const sy2 = bleed;
949
1098
  sWidth = max(1,sWidth|0);