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.
- package/COPYRIGHT.txt +38 -0
- package/FAQ.md +633 -0
- package/README.md +19 -1
- package/dist/littlejs.d.ts +354 -55
- package/dist/littlejs.esm.js +1531 -489
- package/dist/littlejs.esm.min.js +1 -1
- package/dist/littlejs.js +1499 -476
- package/dist/littlejs.min.js +1 -1
- package/dist/littlejs.release.js +1493 -470
- package/package.json +8 -1
- package/plugins/box2d.js +32 -7
- package/{src/engineMedals.js → plugins/medalSystem.js} +248 -196
- package/plugins/pathFinder.js +758 -0
- package/plugins/pluginExport.js +19 -0
- package/plugins/{tween.js → tweenSystem.js} +7 -7
- package/plugins/uiSystem.js +186 -25
- package/src/engine.js +2 -141
- package/src/engineAudio.js +3 -3
- package/src/engineBuild.mjs +4 -2
- package/src/engineDebug.js +7 -7
- package/src/engineDraw.js +26 -8
- package/src/engineExport.js +13 -13
- package/src/engineInput.js +8 -8
- package/src/engineLogo.js +146 -0
- package/src/engineMath.js +15 -6
- package/src/engineObject.js +11 -5
- package/src/engineParticles.js +6 -3
- package/src/engineRelease.js +1 -1
- package/src/engineSettings.js +0 -47
- package/src/engineUtilities.js +13 -8
- package/src/engineWebGL.js +34 -7
- package/src/jsconfig.json +3 -1
- package/.github/workflows/test.yml +0 -17
- package/AI.md +0 -173
- package/CLAUDE.md +0 -1
- package/examples/box2d/game.js +0 -192
- package/examples/box2d/gameObjects.js +0 -564
- package/examples/box2d/index.html +0 -9
- package/examples/box2d/scenes.js +0 -194
- package/examples/box2d/tiles.png +0 -0
- package/examples/breakout/game.js +0 -175
- package/examples/breakout/gameObjects.js +0 -147
- package/examples/breakout/index.html +0 -8
- package/examples/breakout/tiles.png +0 -0
- package/examples/breakoutTutorial/README.md +0 -521
- package/examples/breakoutTutorial/game.js +0 -191
- package/examples/breakoutTutorial/images/1.png +0 -0
- package/examples/breakoutTutorial/images/10.png +0 -0
- package/examples/breakoutTutorial/images/11.png +0 -0
- package/examples/breakoutTutorial/images/2.png +0 -0
- package/examples/breakoutTutorial/images/3.png +0 -0
- package/examples/breakoutTutorial/images/4.png +0 -0
- package/examples/breakoutTutorial/images/5.png +0 -0
- package/examples/breakoutTutorial/images/6.png +0 -0
- package/examples/breakoutTutorial/images/7.png +0 -0
- package/examples/breakoutTutorial/images/8.png +0 -0
- package/examples/breakoutTutorial/images/9.png +0 -0
- package/examples/breakoutTutorial/index.html +0 -8
- package/examples/electron/build.mjs +0 -127
- package/examples/electron/electron.js +0 -35
- package/examples/electron/game.js +0 -54
- package/examples/electron/index.html +0 -13
- package/examples/electron/package.json +0 -5
- package/examples/electron/tiles.png +0 -0
- package/examples/empty/game.js +0 -51
- package/examples/empty/index.html +0 -5
- package/examples/empty/tiles.png +0 -0
- package/examples/favicon.png +0 -0
- package/examples/games.jpg +0 -0
- package/examples/htmlMenu/game.js +0 -82
- package/examples/htmlMenu/index.html +0 -45
- package/examples/htmlMenu/tiles.png +0 -0
- package/examples/index.html +0 -62
- package/examples/logo.png +0 -0
- package/examples/logo2.png +0 -0
- package/examples/module/build.mjs +0 -124
- package/examples/module/game.js +0 -132
- package/examples/module/index.html +0 -10
- package/examples/module/tiles.png +0 -0
- package/examples/particles/index.html +0 -426
- package/examples/particles/tiles.png +0 -0
- package/examples/platformer/data/gameLevelData.tmx +0 -143
- package/examples/platformer/data/gameLevelData.tsx +0 -4
- package/examples/platformer/game.js +0 -147
- package/examples/platformer/gameCharacter.js +0 -309
- package/examples/platformer/gameEffects.js +0 -278
- package/examples/platformer/gameLevel.js +0 -205
- package/examples/platformer/gameLevelData.json +0 -171
- package/examples/platformer/gameObjects.js +0 -378
- package/examples/platformer/gamePlayer.js +0 -35
- package/examples/platformer/index.html +0 -9
- package/examples/platformer/tiles.png +0 -0
- package/examples/platformer/tilesLevel.png +0 -0
- package/examples/puzzle/game.js +0 -331
- package/examples/puzzle/index.html +0 -8
- package/examples/puzzle/tiles.png +0 -0
- package/examples/screenshot.jpg +0 -0
- package/examples/shorts/animation.js +0 -18
- package/examples/shorts/base.html +0 -54
- package/examples/shorts/blending.js +0 -14
- package/examples/shorts/box2d.js +0 -51
- package/examples/shorts/box2dCar.js +0 -59
- package/examples/shorts/box2dPool.js +0 -107
- package/examples/shorts/box2dTileLayer.js +0 -48
- package/examples/shorts/cameraDrag.js +0 -22
- package/examples/shorts/clock.js +0 -21
- package/examples/shorts/colors.js +0 -25
- package/examples/shorts/debugDraw.js +0 -37
- package/examples/shorts/empty.js +0 -30
- package/examples/shorts/flappyGame.js +0 -55
- package/examples/shorts/fontImage.js +0 -17
- package/examples/shorts/fps.js +0 -90
- package/examples/shorts/helloWorld.js +0 -11
- package/examples/shorts/hillGlideGame.js +0 -63
- package/examples/shorts/input.js +0 -64
- package/examples/shorts/landerGame.js +0 -57
- package/examples/shorts/maze.js +0 -48
- package/examples/shorts/medals.js +0 -51
- package/examples/shorts/music.js +0 -78
- package/examples/shorts/musicPlayer.js +0 -137
- package/examples/shorts/nineSlice.js +0 -40
- package/examples/shorts/parallax.js +0 -72
- package/examples/shorts/particles.js +0 -28
- package/examples/shorts/piano.js +0 -44
- package/examples/shorts/platformer.js +0 -45
- package/examples/shorts/pongGame.js +0 -41
- package/examples/shorts/postProcess.js +0 -62
- package/examples/shorts/sequencer.js +0 -124
- package/examples/shorts/shader.js +0 -29
- package/examples/shorts/shapes.js +0 -21
- package/examples/shorts/slidingPuzzle.js +0 -52
- package/examples/shorts/song.mp3 +0 -0
- package/examples/shorts/sound.js +0 -36
- package/examples/shorts/spaceGame.js +0 -58
- package/examples/shorts/spriteAtlas.js +0 -31
- package/examples/shorts/starfield.js +0 -15
- package/examples/shorts/texture.js +0 -16
- package/examples/shorts/textureWrapped.js +0 -12
- package/examples/shorts/tileLayer.js +0 -48
- package/examples/shorts/tileRaycast.js +0 -39
- package/examples/shorts/tiles.png +0 -0
- package/examples/shorts/tiltedView.js +0 -63
- package/examples/shorts/timers.js +0 -53
- package/examples/shorts/topDown.js +0 -38
- package/examples/shorts/tween.js +0 -24
- package/examples/shorts/uiSystem.js +0 -61
- package/examples/shorts/video.webm +0 -0
- package/examples/shorts/videoPlayer.js +0 -34
- package/examples/shorts.js +0 -743
- package/examples/starter/build.bat +0 -7
- package/examples/starter/build.mjs +0 -126
- package/examples/starter/game.js +0 -137
- package/examples/starter/index.html +0 -35
- package/examples/starter/tiles.png +0 -0
- package/examples/stress/index.html +0 -173
- package/examples/style.css +0 -150
- package/examples/tweenSystem/game.js +0 -171
- package/examples/tweenSystem/index.html +0 -10
- package/examples/tweenSystem/tiles.png +0 -0
- package/examples/typescript/build.mjs +0 -60
- package/examples/typescript/game.js +0 -100
- package/examples/typescript/game.ts +0 -132
- package/examples/typescript/index.html +0 -10
- package/examples/typescript/tiles.png +0 -0
- package/examples/typescript/tsconfig.json +0 -17
- package/examples/uiSystem/game.js +0 -139
- package/examples/uiSystem/index.html +0 -10
- package/examples/uiSystem/tiles.png +0 -0
- package/jsconfig.json +0 -12
- package/plugins/desktop.ini +0 -2
- package/reference.md +0 -448
- package/test/math.test.mjs +0 -774
- package/test/setup.mjs +0 -22
- package/test/smoke.test.mjs +0 -274
- package/test/tween.test.mjs +0 -576
- package/test/util.test.mjs +0 -80
package/src/engineDraw.js
CHANGED
|
@@ -217,8 +217,9 @@ class TextureInfo
|
|
|
217
217
|
* Create a TextureInfo, called automatically by the engine
|
|
218
218
|
* @param {HTMLImageElement|OffscreenCanvas} image
|
|
219
219
|
* @param {boolean} [useWebGL] - Should use WebGL if available?
|
|
220
|
+
* @param {boolean} [wrap] - Should the texture wrap (REPEAT) or clamp (CLAMP_TO_EDGE)?
|
|
220
221
|
*/
|
|
221
|
-
constructor(image, useWebGL=true)
|
|
222
|
+
constructor(image, useWebGL=true, wrap=false)
|
|
222
223
|
{
|
|
223
224
|
/** @property {HTMLImageElement|OffscreenCanvas} - image source */
|
|
224
225
|
this.image = image;
|
|
@@ -228,6 +229,8 @@ class TextureInfo
|
|
|
228
229
|
this.sizeInverse = image ? vec2(1/image.width, 1/image.height) : vec2();
|
|
229
230
|
/** @property {WebGLTexture} - WebGL texture */
|
|
230
231
|
this.glTexture = undefined;
|
|
232
|
+
/** @property {boolean} - true for REPEAT wrap mode, false for CLAMP_TO_EDGE */
|
|
233
|
+
this.wrap = wrap;
|
|
231
234
|
useWebGL && this.createWebGLTexture();
|
|
232
235
|
}
|
|
233
236
|
|
|
@@ -240,6 +243,14 @@ class TextureInfo
|
|
|
240
243
|
/** Check if the texture is webgl enabled
|
|
241
244
|
* @return {boolean} */
|
|
242
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
|
+
}
|
|
243
254
|
}
|
|
244
255
|
|
|
245
256
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -301,8 +312,13 @@ function drawTile(pos, size=vec2(1), tileInfo, color=WHITE,
|
|
|
301
312
|
}
|
|
302
313
|
else
|
|
303
314
|
{
|
|
304
|
-
// if no tile info, force untextured
|
|
305
|
-
|
|
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());
|
|
306
322
|
}
|
|
307
323
|
}
|
|
308
324
|
else
|
|
@@ -441,6 +457,8 @@ function drawTextureWrapped(pos, size, wrapCount, texture=0, color=WHITE,
|
|
|
441
457
|
const textureInfo = typeof texture === 'number' ? textureInfos[texture] : texture;
|
|
442
458
|
ASSERT(textureInfo instanceof TextureInfo, 'texture not loaded');
|
|
443
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');
|
|
444
462
|
|
|
445
463
|
if (useWebGL && glEnable)
|
|
446
464
|
{
|
|
@@ -792,15 +810,15 @@ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, f
|
|
|
792
810
|
* @memberof Draw */
|
|
793
811
|
function drawTextScreen(text, pos, size, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, angle=0, context=drawContext)
|
|
794
812
|
{
|
|
795
|
-
ASSERT(
|
|
813
|
+
ASSERT(isStringLike(text), 'text must be a string');
|
|
796
814
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
797
815
|
ASSERT(isNumber(size), 'size must be a number');
|
|
798
816
|
ASSERT(isColor(color), 'color must be a color');
|
|
799
817
|
ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
|
|
800
818
|
ASSERT(isColor(lineColor), 'lineColor must be a color');
|
|
801
819
|
ASSERT(['left','center','right'].includes(textAlign), 'align must be left, center, or right');
|
|
802
|
-
ASSERT(
|
|
803
|
-
ASSERT(
|
|
820
|
+
ASSERT(isStringLike(font), 'font must be a string');
|
|
821
|
+
ASSERT(isStringLike(fontStyle), 'fontStyle must be a string');
|
|
804
822
|
ASSERT(isNumber(angle), 'angle must be a number');
|
|
805
823
|
|
|
806
824
|
context.fillStyle = color.toString();
|
|
@@ -837,7 +855,7 @@ async function loadTexture(textureIndex, src)
|
|
|
837
855
|
{
|
|
838
856
|
ASSERT(isNumber(textureIndex), 'textureIndex must be a number');
|
|
839
857
|
ASSERT(!textureInfos[textureIndex], 'textureIndex is already loaded!');
|
|
840
|
-
ASSERT(!src ||
|
|
858
|
+
ASSERT(!src || isStringLike(src), 'image src must be a string');
|
|
841
859
|
|
|
842
860
|
const image = new Image;
|
|
843
861
|
if (src)
|
|
@@ -1225,7 +1243,7 @@ class FontImage
|
|
|
1225
1243
|
*/
|
|
1226
1244
|
drawTextScreen(text, pos, size, center=true, color=WHITE, useWebGL=glEnable, context)
|
|
1227
1245
|
{
|
|
1228
|
-
ASSERT(
|
|
1246
|
+
ASSERT(isStringLike(text), 'text must be a string');
|
|
1229
1247
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
1230
1248
|
ASSERT(isVector2(size) || typeof size === 'number', 'size must be a vec2 or number');
|
|
1231
1249
|
ASSERT(isColor(color), 'color must be a color');
|
package/src/engineExport.js
CHANGED
|
@@ -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,
|
|
@@ -77,11 +79,14 @@ export
|
|
|
77
79
|
gravity,
|
|
78
80
|
particleEmitRateScale,
|
|
79
81
|
glEnable,
|
|
82
|
+
glCircleSides,
|
|
80
83
|
gamepadsEnable,
|
|
81
84
|
gamepadDirectionEmulateStick,
|
|
82
85
|
inputWASDEmulateDirection,
|
|
86
|
+
touchInputEnable,
|
|
83
87
|
touchGamepadEnable,
|
|
84
88
|
touchGamepadCenterButtonSize,
|
|
89
|
+
touchGamepadButtonCount,
|
|
85
90
|
touchGamepadAnalog,
|
|
86
91
|
touchGamepadSize,
|
|
87
92
|
touchGamepadAlpha,
|
|
@@ -91,9 +96,6 @@ export
|
|
|
91
96
|
soundVolume,
|
|
92
97
|
soundDefaultRange,
|
|
93
98
|
soundDefaultTaper,
|
|
94
|
-
medalDisplayTime,
|
|
95
|
-
medalDisplaySlideTime,
|
|
96
|
-
medalDisplaySize,
|
|
97
99
|
|
|
98
100
|
// Setters for globals
|
|
99
101
|
setCameraPos,
|
|
@@ -125,6 +127,7 @@ export
|
|
|
125
127
|
setObjectMaxSpeed,
|
|
126
128
|
setGravity,
|
|
127
129
|
setParticleEmitRateScale,
|
|
130
|
+
setGLCircleSides,
|
|
128
131
|
setTouchInputEnable,
|
|
129
132
|
setGamepadsEnable,
|
|
130
133
|
setGamepadDirectionEmulateStick,
|
|
@@ -140,10 +143,6 @@ export
|
|
|
140
143
|
setSoundVolume,
|
|
141
144
|
setSoundDefaultRange,
|
|
142
145
|
setSoundDefaultTaper,
|
|
143
|
-
setMedalDisplayTime,
|
|
144
|
-
setMedalDisplaySlideTime,
|
|
145
|
-
setMedalDisplaySize,
|
|
146
|
-
setMedalsPreventUnlock,
|
|
147
146
|
setDebugWatermark,
|
|
148
147
|
setDebugKey,
|
|
149
148
|
|
|
@@ -173,6 +172,7 @@ export
|
|
|
173
172
|
percentLerp,
|
|
174
173
|
smoothStep,
|
|
175
174
|
nearestPowerOfTwo,
|
|
175
|
+
isPowerOfTwo,
|
|
176
176
|
isOverlapping,
|
|
177
177
|
isIntersecting,
|
|
178
178
|
lineTest,
|
|
@@ -208,7 +208,7 @@ export
|
|
|
208
208
|
isColor,
|
|
209
209
|
isVector2,
|
|
210
210
|
isNumber,
|
|
211
|
-
|
|
211
|
+
isStringLike,
|
|
212
212
|
isArray,
|
|
213
213
|
|
|
214
214
|
// Default Colors
|
|
@@ -252,6 +252,7 @@ export
|
|
|
252
252
|
drawLineList,
|
|
253
253
|
drawLine,
|
|
254
254
|
drawPoly,
|
|
255
|
+
drawRegularPoly,
|
|
255
256
|
drawEllipse,
|
|
256
257
|
drawCircle,
|
|
257
258
|
drawCanvas2D,
|
|
@@ -265,6 +266,7 @@ export
|
|
|
265
266
|
toggleFullscreen,
|
|
266
267
|
setCursor,
|
|
267
268
|
getCameraSize,
|
|
269
|
+
isOnScreen,
|
|
268
270
|
|
|
269
271
|
// WebGL
|
|
270
272
|
glCanvas,
|
|
@@ -272,6 +274,7 @@ export
|
|
|
272
274
|
glAntialias,
|
|
273
275
|
glClearCanvas,
|
|
274
276
|
glSetTexture,
|
|
277
|
+
glSetTextureWrap,
|
|
275
278
|
glCompileShader,
|
|
276
279
|
glCreateProgram,
|
|
277
280
|
glCreateTexture,
|
|
@@ -313,6 +316,7 @@ export
|
|
|
313
316
|
gamepadWasPressed,
|
|
314
317
|
gamepadWasReleased,
|
|
315
318
|
gamepadStick,
|
|
319
|
+
gamepadStickCount,
|
|
316
320
|
gamepadDpad,
|
|
317
321
|
gamepadConnected,
|
|
318
322
|
gamepadVibrate,
|
|
@@ -327,6 +331,7 @@ export
|
|
|
327
331
|
audioContext,
|
|
328
332
|
audioMasterGain,
|
|
329
333
|
audioDefaultSampleRate,
|
|
334
|
+
audioIsRunning,
|
|
330
335
|
Sound,
|
|
331
336
|
SoundInstance,
|
|
332
337
|
speak,
|
|
@@ -354,9 +359,4 @@ export
|
|
|
354
359
|
ParticleEmitter,
|
|
355
360
|
Particle,
|
|
356
361
|
|
|
357
|
-
// Medals
|
|
358
|
-
medals,
|
|
359
|
-
medalsPreventUnlock,
|
|
360
|
-
medalsInit,
|
|
361
|
-
Medal,
|
|
362
362
|
}
|
package/src/engineInput.js
CHANGED
|
@@ -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
|
|
@@ -103,7 +103,7 @@ function inputClear()
|
|
|
103
103
|
* @memberof Input */
|
|
104
104
|
function keyIsDown(key, device=0)
|
|
105
105
|
{
|
|
106
|
-
ASSERT(
|
|
106
|
+
ASSERT(isStringLike(key), 'key must be a number or string');
|
|
107
107
|
ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
|
|
108
108
|
return !!(inputData[device]?.[key] & 1);
|
|
109
109
|
}
|
|
@@ -115,7 +115,7 @@ function keyIsDown(key, device=0)
|
|
|
115
115
|
* @memberof Input */
|
|
116
116
|
function keyWasPressed(key, device=0)
|
|
117
117
|
{
|
|
118
|
-
ASSERT(
|
|
118
|
+
ASSERT(isStringLike(key), 'key must be a number or string');
|
|
119
119
|
ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
|
|
120
120
|
return !!(inputData[device]?.[key] & 2);
|
|
121
121
|
}
|
|
@@ -127,7 +127,7 @@ function keyWasPressed(key, device=0)
|
|
|
127
127
|
* @memberof Input */
|
|
128
128
|
function keyWasReleased(key, device=0)
|
|
129
129
|
{
|
|
130
|
-
ASSERT(
|
|
130
|
+
ASSERT(isStringLike(key), 'key must be a number or string');
|
|
131
131
|
ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
|
|
132
132
|
return !!(inputData[device]?.[key] & 4);
|
|
133
133
|
}
|
|
@@ -141,10 +141,10 @@ function keyWasReleased(key, device=0)
|
|
|
141
141
|
* @memberof Input */
|
|
142
142
|
function keyDirection(up='ArrowUp', down='ArrowDown', left='ArrowLeft', right='ArrowRight')
|
|
143
143
|
{
|
|
144
|
-
ASSERT(
|
|
145
|
-
ASSERT(
|
|
146
|
-
ASSERT(
|
|
147
|
-
ASSERT(
|
|
144
|
+
ASSERT(isStringLike(up), 'up key must be a string');
|
|
145
|
+
ASSERT(isStringLike(down), 'down key must be a string');
|
|
146
|
+
ASSERT(isStringLike(left), 'left key must be a string');
|
|
147
|
+
ASSERT(isStringLike(right), 'right key must be a string');
|
|
148
148
|
const k = (key)=> keyIsDown(key) ? 1 : 0;
|
|
149
149
|
return vec2(k(right) - k(left), k(up) - k(down));
|
|
150
150
|
}
|
|
@@ -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
|
-
{
|
|
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
|
|
@@ -288,13 +292,15 @@ function oscillate(frequency=1, amplitude=1, t=time, offset=0, type=0)
|
|
|
288
292
|
function isNumber(n) { return typeof n === 'number' && !isNaN(n); }
|
|
289
293
|
|
|
290
294
|
/**
|
|
291
|
-
* Check if
|
|
295
|
+
* Check if a value is stringifiable — i.e. it has a toString that returns
|
|
296
|
+
* a string. Use this for ASSERTs and inputs that will be coerced to text;
|
|
297
|
+
* use `typeof x === 'string'` inline if you need strict-string semantics.
|
|
292
298
|
* - Returns true for strings, numbers, and most objects
|
|
293
299
|
* - Returns false for null and undefined
|
|
294
300
|
* @param {any} s
|
|
295
301
|
* @return {boolean}
|
|
296
302
|
* @memberof Math */
|
|
297
|
-
function
|
|
303
|
+
function isStringLike(s) { return s != null && typeof s?.toString() === 'string'; }
|
|
298
304
|
|
|
299
305
|
/**
|
|
300
306
|
* Check if object is an array
|
|
@@ -470,6 +476,7 @@ class RandomGenerator
|
|
|
470
476
|
* @param {number} [seed] - Starting seed or engine default seed */
|
|
471
477
|
constructor(seed = 123456789)
|
|
472
478
|
{
|
|
479
|
+
ASSERT(seed !== 0, 'RandomGenerator seed must be non-zero (xorshift is fixed at 0)');
|
|
473
480
|
/** @property {number} - random seed */
|
|
474
481
|
this.seed = seed;
|
|
475
482
|
}
|
|
@@ -763,8 +770,10 @@ class Vector2
|
|
|
763
770
|
* @return {Vector2} */
|
|
764
771
|
floor() { return new Vector2(floor(this.x), floor(this.y)); }
|
|
765
772
|
|
|
766
|
-
/** Returns a copy of this vector snapped to a grid
|
|
767
|
-
*
|
|
773
|
+
/** Returns a copy of this vector snapped to a grid. Note that `grid` is
|
|
774
|
+
* the number of snap steps per unit (so `grid=2` snaps to halves and
|
|
775
|
+
* `grid=0.5` snaps to twos), not the cell size.
|
|
776
|
+
* @param {number} grid - snap steps per unit
|
|
768
777
|
* @return {Vector2} */
|
|
769
778
|
snap(grid)
|
|
770
779
|
{
|
|
@@ -1035,7 +1044,7 @@ class Color
|
|
|
1035
1044
|
* @return {Color} */
|
|
1036
1045
|
setHex(hex)
|
|
1037
1046
|
{
|
|
1038
|
-
ASSERT(
|
|
1047
|
+
ASSERT(isStringLike(hex), 'Color hex code must be a string');
|
|
1039
1048
|
ASSERT(hex[0] === '#', 'Color hex code must start with #');
|
|
1040
1049
|
ASSERT([4,5,7,9].includes(hex.length), 'Invalid hex');
|
|
1041
1050
|
|
package/src/engineObject.js
CHANGED
|
@@ -132,10 +132,18 @@ class EngineObject
|
|
|
132
132
|
const parent = this.parent;
|
|
133
133
|
if (parent)
|
|
134
134
|
{
|
|
135
|
-
//
|
|
135
|
+
// compose with parent transform inline to avoid intermediate vector allocs
|
|
136
136
|
const mirror = parent.getMirrorSign();
|
|
137
|
-
|
|
138
|
-
|
|
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;
|
package/src/engineParticles.js
CHANGED
|
@@ -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.
|
|
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
|
}
|
package/src/engineRelease.js
CHANGED
package/src/engineSettings.js
CHANGED
|
@@ -322,33 +322,6 @@ let soundDefaultRange = 40;
|
|
|
322
322
|
* @memberof Settings */
|
|
323
323
|
let soundDefaultTaper = .7;
|
|
324
324
|
|
|
325
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
326
|
-
// Medals settings
|
|
327
|
-
|
|
328
|
-
/** How long to show medals for in seconds
|
|
329
|
-
* @type {number}
|
|
330
|
-
* @default
|
|
331
|
-
* @memberof Settings */
|
|
332
|
-
let medalDisplayTime = 5;
|
|
333
|
-
|
|
334
|
-
/** How quickly to slide on/off medals in seconds
|
|
335
|
-
* @type {number}
|
|
336
|
-
* @default
|
|
337
|
-
* @memberof Settings */
|
|
338
|
-
let medalDisplaySlideTime = .5;
|
|
339
|
-
|
|
340
|
-
/** Size of medal display
|
|
341
|
-
* @type {Vector2}
|
|
342
|
-
* @default Vector2(640,80)
|
|
343
|
-
* @memberof Settings */
|
|
344
|
-
let medalDisplaySize = vec2(640, 80);
|
|
345
|
-
|
|
346
|
-
/** Set to stop medals from being unlockable (like if cheats are enabled)
|
|
347
|
-
* @type {boolean}
|
|
348
|
-
* @default
|
|
349
|
-
* @memberof Settings */
|
|
350
|
-
let medalsPreventUnlock = false;
|
|
351
|
-
|
|
352
325
|
///////////////////////////////////////////////////////////////////////////////
|
|
353
326
|
// Setters for global variables
|
|
354
327
|
|
|
@@ -610,26 +583,6 @@ function setSoundDefaultRange(range) { soundDefaultRange = range; }
|
|
|
610
583
|
* @memberof Settings */
|
|
611
584
|
function setSoundDefaultTaper(taper) { soundDefaultTaper = taper; }
|
|
612
585
|
|
|
613
|
-
/** Set how long to show medals for in seconds
|
|
614
|
-
* @param {number} time
|
|
615
|
-
* @memberof Settings */
|
|
616
|
-
function setMedalDisplayTime(time) { medalDisplayTime = time; }
|
|
617
|
-
|
|
618
|
-
/** Set how quickly to slide on/off medals in seconds
|
|
619
|
-
* @param {number} time
|
|
620
|
-
* @memberof Settings */
|
|
621
|
-
function setMedalDisplaySlideTime(time) { medalDisplaySlideTime = time; }
|
|
622
|
-
|
|
623
|
-
/** Set size of medal display
|
|
624
|
-
* @param {Vector2} size
|
|
625
|
-
* @memberof Settings */
|
|
626
|
-
function setMedalDisplaySize(size) { medalDisplaySize = size.copy(); }
|
|
627
|
-
|
|
628
|
-
/** Set to stop medals from being unlockable
|
|
629
|
-
* @param {boolean} preventUnlock
|
|
630
|
-
* @memberof Settings */
|
|
631
|
-
function setMedalsPreventUnlock(preventUnlock) { medalsPreventUnlock = preventUnlock; }
|
|
632
|
-
|
|
633
586
|
/** Set if watermark with FPS should be shown
|
|
634
587
|
* @param {boolean} show
|
|
635
588
|
* @memberof Debug */
|
package/src/engineUtilities.js
CHANGED
|
@@ -155,8 +155,8 @@ function saveCanvas(canvas, filename='screenshot', type='image/png')
|
|
|
155
155
|
* @memberof Utilities */
|
|
156
156
|
function saveDataURL(url, filename='download', revokeTime)
|
|
157
157
|
{
|
|
158
|
-
ASSERT(
|
|
159
|
-
ASSERT(
|
|
158
|
+
ASSERT(isStringLike(url), 'saveDataURL requires url string');
|
|
159
|
+
ASSERT(isStringLike(filename), 'saveDataURL requires filename string');
|
|
160
160
|
|
|
161
161
|
// create link for saving screenshots
|
|
162
162
|
const link = document.createElement('a');
|
|
@@ -174,8 +174,8 @@ function saveDataURL(url, filename='download', revokeTime)
|
|
|
174
174
|
* @memberof Utilities */
|
|
175
175
|
function shareURL(title, url, callback)
|
|
176
176
|
{
|
|
177
|
-
ASSERT(
|
|
178
|
-
ASSERT(
|
|
177
|
+
ASSERT(isStringLike(title), 'shareURL requires title string');
|
|
178
|
+
ASSERT(isStringLike(url), 'shareURL requires url string');
|
|
179
179
|
navigator.share?.({title, url}).then(()=>callback?.());
|
|
180
180
|
}
|
|
181
181
|
|
|
@@ -188,11 +188,16 @@ function shareURL(title, url, callback)
|
|
|
188
188
|
* @memberof Utilities */
|
|
189
189
|
function readSaveData(saveName, defaultSaveData)
|
|
190
190
|
{
|
|
191
|
-
ASSERT(
|
|
191
|
+
ASSERT(isStringLike(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
|
-
|
|
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
|
|
|
@@ -202,6 +207,6 @@ function readSaveData(saveName, defaultSaveData)
|
|
|
202
207
|
* @memberof Utilities */
|
|
203
208
|
function writeSaveData(saveName, saveData)
|
|
204
209
|
{
|
|
205
|
-
ASSERT(
|
|
210
|
+
ASSERT(isStringLike(saveName), 'saveData requires saveName string');
|
|
206
211
|
localStorage[saveName] = JSON.stringify(saveData);
|
|
207
212
|
}
|