littlejsengine 1.24.2 → 1.24.3
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/CHANGELOG.md +19 -0
- package/REFERENCE.md +2 -1
- package/dist/littlejs.d.ts +131 -109
- package/dist/littlejs.esm.js +95 -55
- package/dist/littlejs.esm.min.js +3 -3
- package/dist/littlejs.js +95 -55
- package/dist/littlejs.min.js +2 -2
- package/dist/littlejs.release.js +95 -55
- package/package.json +1 -1
- package/plugins/box2d.js +9 -5
- package/plugins/gltf.js +6 -1
- package/plugins/render3dExtras.js +12 -8
- package/plugins/textureSheet.js +15 -7
- package/src/engine.js +31 -17
- package/src/engineAudio.js +3 -3
- package/src/engineBuild.mjs +49 -2
- package/src/engineDraw.js +11 -9
- package/src/engineInput.js +6 -3
- package/src/engineWebGL.js +3 -3
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.24.
|
|
35
|
+
const engineVersion = '1.24.3';
|
|
36
36
|
|
|
37
37
|
/** Frames per second to update
|
|
38
38
|
* @type {number}
|
|
@@ -165,7 +165,7 @@ let timeFixedStart = 0, frameFixedStart = 0;
|
|
|
165
165
|
let windowWidthLast = 0, windowHeightLast = 0, windowPixelRatioLast = 0;
|
|
166
166
|
let engineUpdateInternal; // assigned by engineInit so engineStep can drive it
|
|
167
167
|
let engineFrameScheduled = false; // a frame of the loop is asked for and has not run yet
|
|
168
|
-
let
|
|
168
|
+
let engineFrameErrorLast; // the last error a release build's loop went on past, as text
|
|
169
169
|
|
|
170
170
|
// the pairs of objects asked about a collision this update, so the other's own physics does not ask again: each
|
|
171
171
|
// asker's others, with true for a pair both said to resolve, and false for one left overlapping, ignored or only
|
|
@@ -419,20 +419,30 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
419
419
|
frameTimeBufferMS = 0;
|
|
420
420
|
}
|
|
421
421
|
|
|
422
|
-
// update multiple frames if necessary in case of slow framerate
|
|
423
|
-
|
|
422
|
+
// update multiple frames if necessary in case of slow framerate; a tick that throws still spends its time
|
|
423
|
+
// and the smoothing still comes back, so a release build going on past an error each tick keeps its rate
|
|
424
|
+
try
|
|
424
425
|
{
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
426
|
+
while (frameTimeBufferMS >= 0)
|
|
427
|
+
{
|
|
428
|
+
try
|
|
429
|
+
{
|
|
430
|
+
// read again each tick, so a pause set by the game stops the rest of this frame's catch-up ticks
|
|
431
|
+
const frozenTick = paused || !(timeScale * debugScale);
|
|
432
|
+
|
|
433
|
+
// increment frame and update time, frozen does not advance time
|
|
434
|
+
if (!frozenTick)
|
|
435
|
+
time = timeFixedStart + (frame++ - frameFixedStart) / frameRate;
|
|
436
|
+
engineTick(frozenTick);
|
|
437
|
+
}
|
|
438
|
+
finally { frameTimeBufferMS -= 1e3 / frameRate; }
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
finally
|
|
442
|
+
{
|
|
443
|
+
// add the time smoothing back in
|
|
444
|
+
frameTimeBufferMS += deltaSmooth;
|
|
432
445
|
}
|
|
433
|
-
|
|
434
|
-
// add the time smoothing back in
|
|
435
|
-
frameTimeBufferMS += deltaSmooth;
|
|
436
446
|
}
|
|
437
447
|
|
|
438
448
|
// one tick of the loop: update game and objects, when frozen update everything except them
|
|
@@ -729,9 +739,13 @@ function engineScheduleFrame()
|
|
|
729
739
|
try { engineUpdateInternal(frameTimeMS); }
|
|
730
740
|
catch (error)
|
|
731
741
|
{
|
|
732
|
-
// a release build goes on past an error in a frame, a frozen game is the worst a player can get;
|
|
733
|
-
//
|
|
734
|
-
|
|
742
|
+
// a release build goes on past an error in a frame, a frozen game is the worst a player can get; an error
|
|
743
|
+
// is logged when it is not the last one again, one every frame would flood the console
|
|
744
|
+
const text = String(error);
|
|
745
|
+
text === engineFrameErrorLast || console.error(error);
|
|
746
|
+
engineFrameErrorLast = text;
|
|
747
|
+
// the frame's input is cleared as its tick would have, or a key press that threw would be pressed again
|
|
748
|
+
inputUpdatePost();
|
|
735
749
|
engineScheduleFrame();
|
|
736
750
|
}
|
|
737
751
|
};
|
package/src/engineAudio.js
CHANGED
|
@@ -232,7 +232,7 @@ class Sound
|
|
|
232
232
|
*/
|
|
233
233
|
constructor(asset, randomness, range, taper=soundDefaultTaper, onloadCallback)
|
|
234
234
|
{
|
|
235
|
-
if (!soundEnable || headlessMode)
|
|
235
|
+
if (!soundEnable || headlessMode || !audioContext) // a browser with no audio makes none either
|
|
236
236
|
{
|
|
237
237
|
// no sound is made: it counts as loaded, so a game that waits for its sounds goes on
|
|
238
238
|
this.loadedPercent = 1;
|
|
@@ -302,8 +302,8 @@ class Sound
|
|
|
302
302
|
// load the audio file, a URL object as bundlers give works like its string;
|
|
303
303
|
// report failures rather than leaving an unhandled rejection, the sound just stays unloaded and silent
|
|
304
304
|
const filename = asset + '';
|
|
305
|
-
engineAddLoad(this.loadSound(filename).catch(e=>
|
|
306
|
-
|
|
305
|
+
engineAddLoad(this.loadSound(filename).catch(e=> // startup waits for it, and a release build says why too
|
|
306
|
+
console.warn('Sound load failed for', filename, '-', e?.message ?? e)));
|
|
307
307
|
}
|
|
308
308
|
}
|
|
309
309
|
|
package/src/engineBuild.mjs
CHANGED
|
@@ -289,17 +289,64 @@ function typeScriptBuildStep(filename)
|
|
|
289
289
|
try
|
|
290
290
|
{
|
|
291
291
|
const tsFilename = join(BUILD_FOLDER, `${ENGINE_NAME}.d.ts`);
|
|
292
|
-
|
|
292
|
+
// strictNullChecks keeps undefined in the types, where tsc would drop it from accessors and object types
|
|
293
|
+
// whatever the JSDoc says, so a game in strict mode can set render3D.shader = undefined and is told a
|
|
294
|
+
// flare may be missing; a game not in strict mode reads the same types as before
|
|
295
|
+
execSync(`npx -p typescript tsc "${filename}" --declaration --allowJs --emitDeclarationOnly --strictNullChecks --outFile "${tsFilename}"`);
|
|
293
296
|
|
|
294
297
|
// Make declare module part use the package name littlejsengine
|
|
295
298
|
let fileContent = fs.readFileSync(tsFilename, 'utf8');
|
|
296
299
|
fileContent = fileContent.replace(`${ENGINE_NAME}\.esm`, 'littlejsengine')
|
|
297
|
-
fs.writeFileSync(tsFilename, fileContent);
|
|
300
|
+
fs.writeFileSync(tsFilename, typeScriptOptionalTidy(fileContent));
|
|
298
301
|
|
|
299
302
|
}
|
|
300
303
|
catch (e) { handleError(e, 'Failed to run TypeScript build step!'); }
|
|
301
304
|
};
|
|
302
305
|
|
|
306
|
+
// strictNullChecks writes an optional parameter or field as x?: T | undefined, which is the same as x?: T, so the
|
|
307
|
+
// redundant | undefined is taken off: each ?: is followed to the end of its type, the first , ; ) or line end outside
|
|
308
|
+
// brackets, and only that type is changed; a required x: T | undefined keeps it
|
|
309
|
+
function typeScriptOptionalTidy(text)
|
|
310
|
+
{
|
|
311
|
+
let out = '', i = 0;
|
|
312
|
+
for (let at; (at = text.indexOf('?: ', i)) >= 0;)
|
|
313
|
+
{
|
|
314
|
+
const start = at + 3;
|
|
315
|
+
let end = start, depth = 0;
|
|
316
|
+
for (; end < text.length; ++end)
|
|
317
|
+
{
|
|
318
|
+
const c = text[end];
|
|
319
|
+
if (c === '=' && text[end + 1] === '>') { ++end; continue; } // an arrow is not a closing bracket
|
|
320
|
+
if ('([{<'.includes(c)) ++depth;
|
|
321
|
+
else if (')]}>'.includes(c) && depth) --depth;
|
|
322
|
+
else if (!depth && (',;)\n'.includes(c))) break;
|
|
323
|
+
}
|
|
324
|
+
let type = typeScriptOptionalTidy(text.slice(start, end)); // and the optional fields of a record type in it
|
|
325
|
+
if (type.endsWith(' | undefined'))
|
|
326
|
+
{
|
|
327
|
+
type = type.slice(0, -12);
|
|
328
|
+
// a function type was wrapped for the union, (() => void) | undefined, and needs it no more
|
|
329
|
+
if (type[0] === '(' && typeScriptCloseOf(type, 0) === type.length - 1)
|
|
330
|
+
type = type.slice(1, -1);
|
|
331
|
+
}
|
|
332
|
+
out += text.slice(i, start) + type;
|
|
333
|
+
i = end;
|
|
334
|
+
}
|
|
335
|
+
return out + text.slice(i);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// the index of the bracket that closes the one at start
|
|
339
|
+
function typeScriptCloseOf(text, start)
|
|
340
|
+
{
|
|
341
|
+
let depth = 0;
|
|
342
|
+
for (let k = start; k < text.length; ++k)
|
|
343
|
+
{
|
|
344
|
+
if (text[k] === '(') ++depth;
|
|
345
|
+
else if (text[k] === ')' && !--depth) return k;
|
|
346
|
+
}
|
|
347
|
+
return -1;
|
|
348
|
+
}
|
|
349
|
+
|
|
303
350
|
// display the error and exit
|
|
304
351
|
function handleError(e,message)
|
|
305
352
|
{
|
package/src/engineDraw.js
CHANGED
|
@@ -134,10 +134,13 @@ function tile(index=0, size=tileDefaultSize, texture=0, padding=tileDefaultPaddi
|
|
|
134
134
|
// create tile info object
|
|
135
135
|
const textureInfo = typeof texture === 'number' ?
|
|
136
136
|
textureInfos[texture] : texture;
|
|
137
|
-
if (
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
137
|
+
if (!textureInfo?.size.x)
|
|
138
|
+
{
|
|
139
|
+
// no image: headless loads none and keeps the size; an image that failed to load, whose warning named it, gives
|
|
140
|
+
// a tile of no size, which draws nothing; a slot never given an image is a mistake a debug build points out
|
|
141
|
+
ASSERT(headlessMode || textureInfo instanceof TextureInfo, 'tile texture is not loaded', texture);
|
|
142
|
+
return new TileInfo(new Vector2, headlessMode ? size.copy() : new Vector2, textureInfo, padding, bleed);
|
|
143
|
+
}
|
|
141
144
|
|
|
142
145
|
// get the position of the tile
|
|
143
146
|
const sizePaddedX = size.x + padding*2;
|
|
@@ -605,8 +608,8 @@ function drawTile(pos, size=vec2(1), tileInfo, color=WHITE,
|
|
|
605
608
|
if (headlessMode && !context) return; // headless has no canvas, only a context passed in is drawn to
|
|
606
609
|
|
|
607
610
|
const textureInfo = tileInfo?.textureInfo;
|
|
608
|
-
if (textureInfo && !(tileInfo.size.x && tileInfo.size.y))
|
|
609
|
-
return; // a tile with no area draws nothing, like a sprite still loading
|
|
611
|
+
if (textureInfo && !(tileInfo.size.x && tileInfo.size.y && textureInfo.size.x))
|
|
612
|
+
return; // a tile with no area draws nothing, like a sprite still loading, and nor does an image that failed
|
|
610
613
|
const bleed = tileInfo?.bleed ?? 0;
|
|
611
614
|
if (useWebGL && glEnable)
|
|
612
615
|
{
|
|
@@ -700,7 +703,7 @@ function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
|
|
|
700
703
|
* @param {boolean} [screenSpace=drawScreenSpace]
|
|
701
704
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
702
705
|
* @memberof Draw */
|
|
703
|
-
function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=CLEAR_WHITE, angle=0, useWebGL=glEnable, screenSpace=drawScreenSpace, context)
|
|
706
|
+
function drawRectGradient(pos, size=vec2(1), colorTop=WHITE, colorBottom=CLEAR_WHITE, angle=0, useWebGL=glEnable, screenSpace=drawScreenSpace, context)
|
|
704
707
|
{
|
|
705
708
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
706
709
|
ASSERT(isVector2(size), 'size must be a vec2');
|
|
@@ -1695,8 +1698,7 @@ function bakeTintedImage(image, color, additiveColor)
|
|
|
1695
1698
|
* @param {number} dHeight
|
|
1696
1699
|
* @param {Color} color
|
|
1697
1700
|
* @param {Color} [additiveColor]
|
|
1698
|
-
* @param {number} [bleed] - How many pixels to shrink the source, used to fix bleeding
|
|
1699
|
-
* @memberof Draw */
|
|
1701
|
+
* @param {number} [bleed] - How many pixels to shrink the source, used to fix bleeding */
|
|
1700
1702
|
function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight, color, additiveColor, bleed=0)
|
|
1701
1703
|
{
|
|
1702
1704
|
const sx2 = bleed;
|
package/src/engineInput.js
CHANGED
|
@@ -586,8 +586,9 @@ function inputInit()
|
|
|
586
586
|
if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
|
|
587
587
|
audioContext.resume();
|
|
588
588
|
|
|
589
|
-
// a press in the bars around a letterboxed canvas is not one on its edge; a drag out of it still moves
|
|
590
|
-
|
|
589
|
+
// a press in the bars around a letterboxed canvas is not one on its edge; a drag out of it still moves; under
|
|
590
|
+
// pointer lock the mouse stays where the lock began, which may be in a bar, and every click is the game's
|
|
591
|
+
if (!pointerLockIsActive() && !inCanvas(e.x, e.y))
|
|
591
592
|
return;
|
|
592
593
|
inputData[0][e.button] = 3;
|
|
593
594
|
|
|
@@ -1004,9 +1005,11 @@ function inputUpdate()
|
|
|
1004
1005
|
(gamepadIsDown(12,i)&&1) - (gamepadIsDown(13,i)&&1));
|
|
1005
1006
|
}
|
|
1006
1007
|
|
|
1007
|
-
// copy dpad to left analog stick when pressed
|
|
1008
|
+
// copy dpad to left analog stick when pressed; a gamepad with no sticks has that one, at rest when not
|
|
1008
1009
|
if (gamepadDirectionEmulateStick && (dpad.x || dpad.y))
|
|
1009
1010
|
sticks[0] = dpad.clampLength();
|
|
1011
|
+
else if (gamepadDirectionEmulateStick && !sticks.length)
|
|
1012
|
+
sticks[0] = vec2();
|
|
1010
1013
|
}
|
|
1011
1014
|
|
|
1012
1015
|
// disable touch gamepad if using real gamepad
|
package/src/engineWebGL.js
CHANGED
|
@@ -436,7 +436,7 @@ function glSetTextureWrap(texture, wrap=true)
|
|
|
436
436
|
/** Compile WebGL shader of the given type, will throw errors if in debug mode
|
|
437
437
|
* @param {string} source
|
|
438
438
|
* @param {number} type
|
|
439
|
-
* @return {WebGLShader}
|
|
439
|
+
* @return {WebGLShader|undefined} - undefined with no WebGL
|
|
440
440
|
* @memberof WebGL */
|
|
441
441
|
function glCompileShader(source, type)
|
|
442
442
|
{
|
|
@@ -456,7 +456,7 @@ function glCompileShader(source, type)
|
|
|
456
456
|
/** Create WebGL program with given shaders
|
|
457
457
|
* @param {string} vsSource
|
|
458
458
|
* @param {string} fsSource
|
|
459
|
-
* @return {WebGLProgram}
|
|
459
|
+
* @return {WebGLProgram|undefined} - undefined with no WebGL
|
|
460
460
|
* @memberof WebGL */
|
|
461
461
|
function glCreateProgram(vsSource, fsSource)
|
|
462
462
|
{
|
|
@@ -504,7 +504,7 @@ function glShaderProgram(shader)
|
|
|
504
504
|
* @param {HTMLImageElement|HTMLCanvasElement|OffscreenCanvas|ImageBitmap} [image]
|
|
505
505
|
* @param {boolean|Array<number>} [wrap] - true for REPEAT, false for CLAMP_TO_EDGE, or the WebGL modes across and down
|
|
506
506
|
* @param {boolean} [pixelated] - Hard edged or smooth, undefined follows tilesPixelated
|
|
507
|
-
* @return {WebGLTexture}
|
|
507
|
+
* @return {WebGLTexture|undefined} - undefined with no WebGL
|
|
508
508
|
* @memberof WebGL */
|
|
509
509
|
function glCreateTexture(image, wrap=false, pixelated=tilesPixelated)
|
|
510
510
|
{
|