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
@@ -11,6 +11,7 @@ export
11
11
  frameRate,
12
12
  timeDelta,
13
13
  engineObjects,
14
+ engineObjectsCollide,
14
15
  frame,
15
16
  time,
16
17
  timeReal,
@@ -33,6 +34,7 @@ export
33
34
  // Debug
34
35
  ASSERT,
35
36
  LOG,
37
+ debugPointSize,
36
38
  debugRect,
37
39
  debugPoly,
38
40
  debugCircle,
@@ -51,6 +53,7 @@ export
51
53
  cameraPos,
52
54
  cameraAngle,
53
55
  cameraScale,
56
+ timeScale,
54
57
  canvasColorTiles,
55
58
  canvasClearColor,
56
59
  canvasMaxSize,
@@ -76,11 +79,14 @@ export
76
79
  gravity,
77
80
  particleEmitRateScale,
78
81
  glEnable,
82
+ glCircleSides,
79
83
  gamepadsEnable,
80
84
  gamepadDirectionEmulateStick,
81
85
  inputWASDEmulateDirection,
86
+ touchInputEnable,
82
87
  touchGamepadEnable,
83
88
  touchGamepadCenterButtonSize,
89
+ touchGamepadButtonCount,
84
90
  touchGamepadAnalog,
85
91
  touchGamepadSize,
86
92
  touchGamepadAlpha,
@@ -90,14 +96,12 @@ export
90
96
  soundVolume,
91
97
  soundDefaultRange,
92
98
  soundDefaultTaper,
93
- medalDisplayTime,
94
- medalDisplaySlideTime,
95
- medalDisplaySize,
96
99
 
97
100
  // Setters for globals
98
101
  setCameraPos,
99
102
  setCameraAngle,
100
103
  setCameraScale,
104
+ setTimeScale,
101
105
  setCanvasColorTiles,
102
106
  setCanvasClearColor,
103
107
  setCanvasMaxSize,
@@ -123,6 +127,7 @@ export
123
127
  setObjectMaxSpeed,
124
128
  setGravity,
125
129
  setParticleEmitRateScale,
130
+ setGLCircleSides,
126
131
  setTouchInputEnable,
127
132
  setGamepadsEnable,
128
133
  setGamepadDirectionEmulateStick,
@@ -138,10 +143,6 @@ export
138
143
  setSoundVolume,
139
144
  setSoundDefaultRange,
140
145
  setSoundDefaultTaper,
141
- setMedalDisplayTime,
142
- setMedalDisplaySlideTime,
143
- setMedalDisplaySize,
144
- setMedalsPreventUnlock,
145
146
  setDebugWatermark,
146
147
  setDebugKey,
147
148
 
@@ -171,6 +172,7 @@ export
171
172
  percentLerp,
172
173
  smoothStep,
173
174
  nearestPowerOfTwo,
175
+ isPowerOfTwo,
174
176
  isOverlapping,
175
177
  isIntersecting,
176
178
  lineTest,
@@ -246,9 +248,11 @@ export
246
248
  drawTile,
247
249
  drawRect,
248
250
  drawRectGradient,
251
+ drawTextureWrapped,
249
252
  drawLineList,
250
253
  drawLine,
251
254
  drawPoly,
255
+ drawRegularPoly,
252
256
  drawEllipse,
253
257
  drawCircle,
254
258
  drawCanvas2D,
@@ -262,6 +266,7 @@ export
262
266
  toggleFullscreen,
263
267
  setCursor,
264
268
  getCameraSize,
269
+ isOnScreen,
265
270
 
266
271
  // WebGL
267
272
  glCanvas,
@@ -269,6 +274,7 @@ export
269
274
  glAntialias,
270
275
  glClearCanvas,
271
276
  glSetTexture,
277
+ glSetTextureWrap,
272
278
  glCompileShader,
273
279
  glCreateProgram,
274
280
  glCreateTexture,
@@ -310,8 +316,11 @@ export
310
316
  gamepadWasPressed,
311
317
  gamepadWasReleased,
312
318
  gamepadStick,
319
+ gamepadStickCount,
313
320
  gamepadDpad,
314
321
  gamepadConnected,
322
+ gamepadVibrate,
323
+ gamepadVibrateStop,
315
324
  vibrate,
316
325
  vibrateStop,
317
326
  pointerLockRequest,
@@ -322,6 +331,7 @@ export
322
331
  audioContext,
323
332
  audioMasterGain,
324
333
  audioDefaultSampleRate,
334
+ audioIsRunning,
325
335
  Sound,
326
336
  SoundInstance,
327
337
  speak,
@@ -349,9 +359,4 @@ export
349
359
  ParticleEmitter,
350
360
  Particle,
351
361
 
352
- // Medals
353
- medals,
354
- medalsPreventUnlock,
355
- medalsInit,
356
- Medal,
357
362
  }
@@ -66,7 +66,7 @@ const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
66
66
  * This is useful to disable for html menus so the browser can handle input normally
67
67
  * @param {boolean} preventDefault
68
68
  * @memberof Input */
69
- function setInputPreventDefault(preventDefault) { inputPreventDefault = preventDefault; }
69
+ function setInputPreventDefault(preventDefault=true) { inputPreventDefault = preventDefault; }
70
70
 
71
71
  /** Clears an input key state
72
72
  * @param {string|number} key
@@ -260,6 +260,32 @@ function gamepadStickCount(gamepad=gamepadPrimary)
260
260
  return gamepadStickData[gamepad]?.length ?? 0;
261
261
  }
262
262
 
263
+ /** Pulse a gamepad's vibration hardware using the dual-rumble effect if it exists
264
+ * Strong magnitude is usually the left side motor, weak magnitude is usually the right side motor
265
+ * @param {number} [gamepad] - gamepad index
266
+ * @param {number} [duration] - effect duration in ms
267
+ * @param {number} [strongMagnitude] - strong (left) motor intensity, 0 to 1
268
+ * @param {number} [weakMagnitude] - weak (right) motor intensity, 0 to 1
269
+ * @param {number} [startDelay] - delay in ms before the effect starts
270
+ * @memberof Input */
271
+ function gamepadVibrate(gamepad=gamepadPrimary, duration=200, strongMagnitude=1, weakMagnitude=1, startDelay=0)
272
+ {
273
+ ASSERT(isNumber(gamepad), 'gamepad must be a number');
274
+ if (!vibrateEnable || headlessMode) return;
275
+ const pad = navigator?.getGamepads?.()[gamepad];
276
+ pad?.vibrationActuator?.playEffect?.('dual-rumble', {duration, strongMagnitude, weakMagnitude, startDelay});
277
+ }
278
+
279
+ /** Stop vibration on a gamepad
280
+ * @memberof Input */
281
+ function gamepadVibrateStop(gamepad=gamepadPrimary)
282
+ {
283
+ ASSERT(isNumber(gamepad), 'gamepad must be a number');
284
+ if (!vibrateEnable || headlessMode) return;
285
+ const pad = navigator?.getGamepads?.()[gamepad];
286
+ pad?.vibrationActuator?.reset?.();
287
+ }
288
+
263
289
  ///////////////////////////////////////////////////////////////////////////////
264
290
 
265
291
  /** Pulse the vibration hardware if it exists
@@ -0,0 +1,146 @@
1
+ /**
2
+ * LittleJS Engine Logo
3
+ * - Draws the LittleJS splash screen logo
4
+ * - Used internally during engine startup
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ ///////////////////////////////////////////////////////////////////////////////
10
+ function drawEngineLogo(t)
11
+ {
12
+ const blackAndWhite = 0;
13
+ const showName = 1;
14
+
15
+ // LittleJS Logo and Splash Screen
16
+ const x = mainContext;
17
+ const dpr = canvasPixelRatio ?? (devicePixelRatio || 1);
18
+ const w = mainCanvas.width = innerWidth * dpr;
19
+ const h = mainCanvas.height = innerHeight * dpr;
20
+ {
21
+ // background
22
+ const p3 = percent(t, 1, .8);
23
+ const p4 = percent(t, 0, .5);
24
+ const g = x.createRadialGradient(w/2,h/2,0,w/2,h/2,hypot(w,h)*.6);
25
+ g.addColorStop(0,hsl(0,0,lerp(0,p3/2,p4),p3).toString());
26
+ g.addColorStop(1,hsl(0,0,0,p3).toString());
27
+ x.save();
28
+ x.fillStyle = g;
29
+ x.fillRect(0,0,w,h);
30
+ }
31
+ const gradient = (X1,Y1,X2,Y2,C,S=1)=>
32
+ {
33
+ if (C >= 0)
34
+ {
35
+ if (blackAndWhite)
36
+ x.fillStyle = '#fff';
37
+ else
38
+ {
39
+ const g = x.fillStyle = x.createLinearGradient(X1,Y1,X2,Y2);
40
+ g.addColorStop(0,color(C,2));
41
+ g.addColorStop(1,color(C,1));
42
+ }
43
+ }
44
+ else
45
+ x.fillStyle = '#000';
46
+ C >= -1 ? (x.fill(), S && x.stroke()) : x.stroke();
47
+ }
48
+ const circle = (X,Y,R,A=0,B=2*PI,C,S)=>
49
+ {
50
+ x.beginPath();
51
+ x.arc(X,Y,R,p*A,p*B);
52
+ gradient(X,Y-R,X,Y+R,C,S);
53
+ }
54
+ const rect = (X,Y,W,H,C)=>
55
+ {
56
+ x.beginPath();
57
+ x.rect(X,Y,W,H*p);
58
+ gradient(X,Y+H,X+W,Y,C);
59
+ }
60
+ const poly = (points,C,Y,H)=>
61
+ {
62
+ x.beginPath();
63
+ for (const p of points)
64
+ x.lineTo(p.x, p.y);
65
+ x.closePath();
66
+ gradient(0, Y, 0, Y+H,C);
67
+ }
68
+ const color = (c,l)=> l?`hsl(${[.95,.56,.13][c%3]*360} 99%${[0,50,75][l]}%`:'#000';
69
+
70
+ // center and fit to screen
71
+ const alpha = oscillate(1,1,t);
72
+ const p = percent(alpha, .1, .5);
73
+ const size = min(6, min(w,h)/99);
74
+ x.translate(w/2,h/2);
75
+ x.scale(size,size);
76
+ x.translate(-40,-35);
77
+ p < 1 && x.setLineDash([99*p,99]);
78
+ x.lineJoin = x.lineCap = 'round';
79
+ x.lineWidth = .1 + p*1.9;
80
+ //x.strokeStyle='#fff7';
81
+
82
+ if (showName)
83
+ {
84
+ // engine name text
85
+ const Y = 54;
86
+ const s = 'LittleJS';
87
+ x.font = '900 15.5px arial';
88
+ x.lineWidth = .1+p*3.9;
89
+ x.textAlign = 'center';
90
+ x.textBaseline = 'top';
91
+ rect(11,Y+1,59,8*p,-1);
92
+ x.beginPath();
93
+
94
+ let w2 = 0;
95
+ for (let i=0;i<s.length;++i)
96
+ w2 += x.measureText(s[i]).width;
97
+ for (let j=2;j--;)
98
+ for (let i=0,X=40-w2/2;i<s.length;++i)
99
+ {
100
+ const w = x.measureText(s[i]).width, X2 = X+w/2;
101
+ gradient(X2,Y,X2+2,Y+13,i>5?1:0);
102
+ x[j?'strokeText':'fillText'](s[i],X2,Y+.5,17*p);
103
+ X += w;
104
+ }
105
+
106
+ x.lineWidth = .1 + p*1.9;
107
+ rect(3,Y,73,0); // bottom
108
+ }
109
+
110
+ rect(7,15,26,-7,0); // cab top
111
+ rect(25,15,8,25,-1); // cab front
112
+ rect(10,40,15,-25,1); // cab back
113
+ rect(14,21,7,9,2); // cab window
114
+ rect(38,20,6,-6,2); // little stack
115
+
116
+ // big stack
117
+ rect(49,20,10,-6,0);
118
+ const stackPoints = [vec2(44,8),vec2(64,8),vec2(59,8+6*p),vec2(49,8+6*p)];
119
+ poly(stackPoints,2,8,6*p);
120
+ rect(44,8,20,-7,0);
121
+
122
+ // engine
123
+ for (let i=5;i--;) circle(59-i*6*p,30,10,0,2*PI,1,0);
124
+ circle(59,30,4,0,7,2); // light
125
+
126
+ // engine outline
127
+ rect(35,20,24,0); // top
128
+ circle(59,30,10); // front
129
+ circle(47,30,10,PI/2,PI*3/2); // middle
130
+ circle(35,30,10,PI/2,PI*3/2); // back
131
+ rect(7,40,13,7,-1); // bottom back
132
+ rect(17,40,43,14,-1); // bottom center
133
+
134
+ // wheels
135
+ for (let i=3;i--;) for (let j=2;j--;) circle(17+15*i,47,j?7:1,0,2*PI,2);
136
+
137
+ // cowcatcher
138
+ for (let i=2;i--;)
139
+ {
140
+ let w=6, s=7, o=53+w*p*i
141
+ const points = [vec2(o+s,54),vec2(o,40),vec2(o+w*p,40),vec2(o+s+w*p,54)];
142
+ poly(points,0,40,14);
143
+ }
144
+
145
+ x.restore();
146
+ }
package/src/engineMath.js CHANGED
@@ -153,7 +153,11 @@ function percentLerp(value, percentA, percentB, lerpA, lerpB)
153
153
  * @return {number}
154
154
  * @memberof Math */
155
155
  function distanceWrap(valueA, valueB, wrapSize=1)
156
- { const d = (valueA - valueB) % wrapSize; return d*2 % wrapSize - d; }
156
+ {
157
+ ASSERT(wrapSize > 0, 'distanceWrap wrapSize must be > 0');
158
+ const d = (valueA - valueB) % wrapSize;
159
+ return d*2 % wrapSize - d;
160
+ }
157
161
 
158
162
  /** Linearly interpolates between values passed in with wrapping
159
163
  * @param {number} valueA
@@ -470,6 +474,7 @@ class RandomGenerator
470
474
  * @param {number} [seed] - Starting seed or engine default seed */
471
475
  constructor(seed = 123456789)
472
476
  {
477
+ ASSERT(seed !== 0, 'RandomGenerator seed must be non-zero (xorshift is fixed at 0)');
473
478
  /** @property {number} - random seed */
474
479
  this.seed = seed;
475
480
  }
@@ -763,8 +768,10 @@ class Vector2
763
768
  * @return {Vector2} */
764
769
  floor() { return new Vector2(floor(this.x), floor(this.y)); }
765
770
 
766
- /** Returns a copy of this vector snapped to a grid
767
- * @param {number} grid - grid size to snap to
771
+ /** Returns a copy of this vector snapped to a grid. Note that `grid` is
772
+ * the number of snap steps per unit (so `grid=2` snaps to halves and
773
+ * `grid=0.5` snaps to twos), not the cell size.
774
+ * @param {number} grid - snap steps per unit
768
775
  * @return {Vector2} */
769
776
  snap(grid)
770
777
  {
@@ -132,10 +132,18 @@ class EngineObject
132
132
  const parent = this.parent;
133
133
  if (parent)
134
134
  {
135
- // copy parent pos/angle
135
+ // compose with parent transform inline to avoid intermediate vector allocs
136
136
  const mirror = parent.getMirrorSign();
137
- this.pos = this.localPos.multiply(vec2(mirror,1)).rotate(parent.angle).add(parent.pos);
138
- this.angle = mirror*this.localAngle + parent.angle;
137
+ const lp = this.localPos, pp = parent.pos;
138
+ const lx = lp.x*mirror, ly = lp.y, pa = parent.angle;
139
+ if (pa)
140
+ {
141
+ const c = cos(-pa), s = sin(-pa);
142
+ this.pos = new Vector2(lx*c - ly*s + pp.x, lx*s + ly*c + pp.y);
143
+ }
144
+ else
145
+ this.pos = new Vector2(lx + pp.x, ly + pp.y);
146
+ this.angle = mirror*this.localAngle + pa;
139
147
  }
140
148
 
141
149
  // update children
@@ -501,8 +509,6 @@ class EngineObject
501
509
  * @return {string} */
502
510
  toString()
503
511
  {
504
- if (!debug) return;
505
-
506
512
  let text = 'type = ' + this.constructor.name;
507
513
  if (this.pos.x || this.pos.y)
508
514
  text += '\npos = ' + this.pos;
@@ -315,11 +315,14 @@ class ParticleEmitter extends EngineObject
315
315
  }
316
316
 
317
317
  ///////////////////////////////////////////////////////////////////////////////
318
+ // scratch vector reused by Particle.render to avoid per-frame allocations
319
+ const particleDrawPos = new Vector2;
320
+
318
321
  /**
319
322
  * Particle Object - Created automatically by Particle Emitters
320
323
  * @memberof Particles
321
324
  */
322
- class Particle
325
+ class Particle
323
326
  {
324
327
  /**
325
328
  * Create a particle with the passed in settings
@@ -494,14 +497,14 @@ class Particle
494
497
  additive && setBlendMode(true);
495
498
 
496
499
  // update the position and angle for drawing
497
- const pos = this.pos.copy();
500
+ const pos = particleDrawPos.set(this.pos.x, this.pos.y);
498
501
  let angle = this.angle;
499
502
  if (localSpace)
500
503
  {
501
504
  // in local space of emitter
502
505
  const a = emitter.angle;
503
506
  const c = cos(a), s = sin(a);
504
- pos.set(emitter.pos.x + pos.x*c - pos.y*s,
507
+ pos.set(emitter.pos.x + pos.x*c - pos.y*s,
505
508
  emitter.pos.y + pos.x*s + pos.y*c);
506
509
  angle += a;
507
510
  }
@@ -18,7 +18,7 @@ const debugPhysics = 0;
18
18
  const debugParticles = 0;
19
19
  const debugRaycast = 0;
20
20
  const debugGamepads = 0;
21
- const debugMedals = 0;
21
+ const debugPointSize = .5;
22
22
 
23
23
  // debug commands are automatically removed from the final build
24
24
  function ASSERT (){}
@@ -27,6 +27,18 @@ let cameraAngle = 0;
27
27
  * @memberof Settings */
28
28
  let cameraScale = 32;
29
29
 
30
+ ///////////////////////////////////////////////////////////////////////////////
31
+ // Time settings
32
+
33
+ /** Scale applied to engine time, can be used for slow motion or fast forward
34
+ * - 1 is normal speed, 2 is double speed, 0.5 is half speed
35
+ * - 0 freezes the simulation without setting the paused flag
36
+ * - Should be >= 0; stacks multiplicatively with the debug +/- shortcut
37
+ * @type {number}
38
+ * @default
39
+ * @memberof Settings */
40
+ let timeScale = 1;
41
+
30
42
  ///////////////////////////////////////////////////////////////////////////////
31
43
  // Display settings
32
44
 
@@ -310,33 +322,6 @@ let soundDefaultRange = 40;
310
322
  * @memberof Settings */
311
323
  let soundDefaultTaper = .7;
312
324
 
313
- ///////////////////////////////////////////////////////////////////////////////
314
- // Medals settings
315
-
316
- /** How long to show medals for in seconds
317
- * @type {number}
318
- * @default
319
- * @memberof Settings */
320
- let medalDisplayTime = 5;
321
-
322
- /** How quickly to slide on/off medals in seconds
323
- * @type {number}
324
- * @default
325
- * @memberof Settings */
326
- let medalDisplaySlideTime = .5;
327
-
328
- /** Size of medal display
329
- * @type {Vector2}
330
- * @default Vector2(640,80)
331
- * @memberof Settings */
332
- let medalDisplaySize = vec2(640, 80);
333
-
334
- /** Set to stop medals from being unlockable (like if cheats are enabled)
335
- * @type {boolean}
336
- * @default
337
- * @memberof Settings */
338
- let medalsPreventUnlock = false;
339
-
340
325
  ///////////////////////////////////////////////////////////////////////////////
341
326
  // Setters for global variables
342
327
 
@@ -355,6 +340,11 @@ function setCameraAngle(angle) { cameraAngle = angle; }
355
340
  * @memberof Settings */
356
341
  function setCameraScale(scale) { cameraScale = scale; }
357
342
 
343
+ /** Set scale applied to engine time
344
+ * @param {number} scale
345
+ * @memberof Settings */
346
+ function setTimeScale(scale) { timeScale = scale; }
347
+
358
348
  /** Set if tiles should be colorized when using canvas2d
359
349
  * This can be slower but results should look nearly identical to WebGL rendering
360
350
  * It can be enabled/disabled at any time
@@ -593,26 +583,6 @@ function setSoundDefaultRange(range) { soundDefaultRange = range; }
593
583
  * @memberof Settings */
594
584
  function setSoundDefaultTaper(taper) { soundDefaultTaper = taper; }
595
585
 
596
- /** Set how long to show medals for in seconds
597
- * @param {number} time
598
- * @memberof Settings */
599
- function setMedalDisplayTime(time) { medalDisplayTime = time; }
600
-
601
- /** Set how quickly to slide on/off medals in seconds
602
- * @param {number} time
603
- * @memberof Settings */
604
- function setMedalDisplaySlideTime(time) { medalDisplaySlideTime = time; }
605
-
606
- /** Set size of medal display
607
- * @param {Vector2} size
608
- * @memberof Settings */
609
- function setMedalDisplaySize(size) { medalDisplaySize = size.copy(); }
610
-
611
- /** Set to stop medals from being unlockable
612
- * @param {boolean} preventUnlock
613
- * @memberof Settings */
614
- function setMedalsPreventUnlock(preventUnlock) { medalsPreventUnlock = preventUnlock; }
615
-
616
586
  /** Set if watermark with FPS should be shown
617
587
  * @param {boolean} show
618
588
  * @memberof Debug */
@@ -190,9 +190,14 @@ function readSaveData(saveName, defaultSaveData)
190
190
  {
191
191
  ASSERT(isString(saveName), 'loadData requires saveName string');
192
192
 
193
- // replace undefined values with defaults
193
+ // replace undefined values with defaults; tolerate corrupt JSON
194
194
  const data = localStorage[saveName];
195
- const loadedData = data ? JSON.parse(data) : {};
195
+ let loadedData = {};
196
+ if (data)
197
+ {
198
+ try { loadedData = JSON.parse(data); }
199
+ catch { LOG('readSaveData: corrupt JSON for', saveName, '— using defaults'); }
200
+ }
196
201
  return { ...defaultSaveData, ...loadedData };
197
202
  }
198
203
 
@@ -99,7 +99,7 @@ function glInit(rootElement)
99
99
  // reinit WebGL and restore textures
100
100
  initWebGL();
101
101
  for (const info of glTextureInfos)
102
- info.glTexture = glCreateTexture(info.image);
102
+ info.glTexture = glCreateTexture(info.image, info.wrap);
103
103
  pluginList.forEach(plugin=>plugin.glContextRestored?.());
104
104
  });
105
105
 
@@ -304,9 +304,8 @@ function glClearCanvas()
304
304
  /** Set the WebGL texture, called automatically if using multiple textures
305
305
  * - This may also flush the gl buffer resulting in more draw calls and worse performance
306
306
  * @param {WebGLTexture} texture
307
- * @param {boolean} [wrap] - Should the texture wrap or clamp
308
307
  * @memberof WebGL */
309
- function glSetTexture(texture, wrap=false)
308
+ function glSetTexture(texture)
310
309
  {
311
310
  // must flush cache with the old texture to set a new one
312
311
  if (!glContext || texture === glActiveTexture) return;
@@ -314,11 +313,30 @@ function glSetTexture(texture, wrap=false)
314
313
  glFlush();
315
314
  glActiveTexture = texture;
316
315
  glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
316
+ }
317
+
318
+ /** Set the wrap mode (REPEAT or CLAMP_TO_EDGE) on an existing WebGL texture
319
+ * Flushes the current batch only if the texture is the active one
320
+ * @param {WebGLTexture} texture
321
+ * @param {boolean} [wrap] - true for REPEAT, false for CLAMP_TO_EDGE
322
+ * @memberof WebGL */
323
+ function glSetTextureWrap(texture, wrap=true)
324
+ {
325
+ if (!glContext || !texture) return;
326
+
327
+ // flush only if changing wrap on the currently bound texture
328
+ const isCurrent = texture === glActiveTexture;
329
+ if (isCurrent)
330
+ glFlush();
331
+ else
332
+ glContext.bindTexture(glContext.TEXTURE_2D, texture);
317
333
 
318
- // set wrap mode
319
334
  const wrapMode = wrap ? glContext.REPEAT : glContext.CLAMP_TO_EDGE;
320
335
  glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_WRAP_S, wrapMode);
321
336
  glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_WRAP_T, wrapMode);
337
+
338
+ if (!isCurrent && glActiveTexture)
339
+ glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
322
340
  }
323
341
 
324
342
  /** Compile WebGL shader of the given type, will throw errors if in debug mode
@@ -365,9 +383,10 @@ function glCreateProgram(vsSource, fsSource)
365
383
  /** Create WebGL texture from an image and init the texture settings
366
384
  * Restores the active texture when done
367
385
  * @param {HTMLImageElement|HTMLCanvasElement|OffscreenCanvas} [image]
386
+ * @param {boolean} [wrap] - true for REPEAT, false for CLAMP_TO_EDGE
368
387
  * @return {WebGLTexture}
369
388
  * @memberof WebGL */
370
- function glCreateTexture(image)
389
+ function glCreateTexture(image, wrap=false)
371
390
  {
372
391
  if (!glContext) return;
373
392
 
@@ -393,6 +412,9 @@ function glCreateTexture(image)
393
412
  const minFilter = mipMap ? glContext.LINEAR_MIPMAP_LINEAR : magFilter;
394
413
  glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MAG_FILTER, magFilter);
395
414
  glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MIN_FILTER, minFilter);
415
+ const wrapMode = wrap ? glContext.REPEAT : glContext.CLAMP_TO_EDGE;
416
+ glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_WRAP_S, wrapMode);
417
+ glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_WRAP_T, wrapMode);
396
418
  if (mipMap)
397
419
  glContext.generateMipmap(glContext.TEXTURE_2D);
398
420
 
@@ -444,7 +466,7 @@ function glRegisterTextureInfo(textureInfo)
444
466
  if (textureInfo.glTexture)
445
467
  glSetTextureData(textureInfo.glTexture, textureInfo.image);
446
468
  else
447
- textureInfo.glTexture = glCreateTexture(textureInfo.image);
469
+ textureInfo.glTexture = glCreateTexture(textureInfo.image, textureInfo.wrap);
448
470
  }
449
471
 
450
472
  /** Tells WebGL to destroy the glTexture and stop tracking it
@@ -663,6 +685,7 @@ function glSetRenderTarget(texture, clear=false)
663
685
  glFlush();
664
686
  glRenderTarget = undefined;
665
687
  glContext.bindFramebuffer(glContext.FRAMEBUFFER, null);
688
+ glContext.viewport(0, 0, mainCanvasSize.x, mainCanvasSize.y);
666
689
  }
667
690
  }
668
691
 
@@ -785,9 +808,9 @@ function glPolyStrip(points)
785
808
  return area;
786
809
  }
787
810
 
788
- // ensure counter-clockwise winding
811
+ // ensure counter-clockwise winding (slice first so we don't mutate caller's array)
789
812
  if (signedArea(points) < 0)
790
- points = points.reverse();
813
+ points = points.slice().reverse();
791
814
 
792
815
  // check if point is inside triangle
793
816
  const e = 1e-9;
package/src/jsconfig.json CHANGED
@@ -2,7 +2,9 @@
2
2
  "compilerOptions": {
3
3
  "checkJs": true,
4
4
  "target": "ES2020",
5
- "module": "esnext"
5
+ "module": "esnext",
6
+ "noImplicitAny": false,
7
+ "strict": false
6
8
  },
7
9
  "exclude": [
8
10
  "engineRelease.js",
@@ -1,7 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(npm test *)"
5
- ]
6
- }
7
- }
@@ -1,17 +0,0 @@
1
- name: Test
2
-
3
- on:
4
- push:
5
- pull_request:
6
-
7
- jobs:
8
- test:
9
- runs-on: ubuntu-latest
10
- steps:
11
- - uses: actions/checkout@v5
12
- - uses: actions/setup-node@v5
13
- with:
14
- node-version: '22'
15
- - run: npm install
16
- - run: npm run build
17
- - run: npm test