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.
@@ -35,7 +35,7 @@ const engineName = 'LittleJS';
35
35
  * @type {string}
36
36
  * @default
37
37
  * @memberof Engine */
38
- const engineVersion = '1.24.2';
38
+ const engineVersion = '1.24.3';
39
39
 
40
40
  /** Frames per second to update
41
41
  * @type {number}
@@ -168,7 +168,7 @@ let timeFixedStart = 0, frameFixedStart = 0;
168
168
  let windowWidthLast = 0, windowHeightLast = 0, windowPixelRatioLast = 0;
169
169
  let engineUpdateInternal; // assigned by engineInit so engineStep can drive it
170
170
  let engineFrameScheduled = false; // a frame of the loop is asked for and has not run yet
171
- let engineFrameErrors = 0; // errors a release build's loop went on past
171
+ let engineFrameErrorLast; // the last error a release build's loop went on past, as text
172
172
 
173
173
  // the pairs of objects asked about a collision this update, so the other's own physics does not ask again: each
174
174
  // asker's others, with true for a pair both said to resolve, and false for one left overlapping, ignored or only
@@ -422,20 +422,30 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
422
422
  frameTimeBufferMS = 0;
423
423
  }
424
424
 
425
- // update multiple frames if necessary in case of slow framerate
426
- for (; frameTimeBufferMS >= 0; frameTimeBufferMS -= 1e3 / frameRate)
425
+ // update multiple frames if necessary in case of slow framerate; a tick that throws still spends its time
426
+ // and the smoothing still comes back, so a release build going on past an error each tick keeps its rate
427
+ try
427
428
  {
428
- // read again each tick, so a pause set by the game stops the rest of this frame's catch-up ticks
429
- const frozenTick = paused || !(timeScale * debugScale);
429
+ while (frameTimeBufferMS >= 0)
430
+ {
431
+ try
432
+ {
433
+ // read again each tick, so a pause set by the game stops the rest of this frame's catch-up ticks
434
+ const frozenTick = paused || !(timeScale * debugScale);
430
435
 
431
- // increment frame and update time, frozen does not advance time
432
- if (!frozenTick)
433
- time = timeFixedStart + (frame++ - frameFixedStart) / frameRate;
434
- engineTick(frozenTick);
436
+ // increment frame and update time, frozen does not advance time
437
+ if (!frozenTick)
438
+ time = timeFixedStart + (frame++ - frameFixedStart) / frameRate;
439
+ engineTick(frozenTick);
440
+ }
441
+ finally { frameTimeBufferMS -= 1e3 / frameRate; }
442
+ }
443
+ }
444
+ finally
445
+ {
446
+ // add the time smoothing back in
447
+ frameTimeBufferMS += deltaSmooth;
435
448
  }
436
-
437
- // add the time smoothing back in
438
- frameTimeBufferMS += deltaSmooth;
439
449
  }
440
450
 
441
451
  // one tick of the loop: update game and objects, when frozen update everything except them
@@ -732,9 +742,13 @@ function engineScheduleFrame()
732
742
  try { engineUpdateInternal(frameTimeMS); }
733
743
  catch (error)
734
744
  {
735
- // a release build goes on past an error in a frame, a frozen game is the worst a player can get; only the
736
- // first is logged, one every frame would flood the console
737
- engineFrameErrors++ || console.error(error);
745
+ // a release build goes on past an error in a frame, a frozen game is the worst a player can get; an error
746
+ // is logged when it is not the last one again, one every frame would flood the console
747
+ const text = String(error);
748
+ text === engineFrameErrorLast || console.error(error);
749
+ engineFrameErrorLast = text;
750
+ // the frame's input is cleared as its tick would have, or a key press that threw would be pressed again
751
+ inputUpdatePost();
738
752
  engineScheduleFrame();
739
753
  }
740
754
  };
@@ -4311,10 +4325,13 @@ function tile(index=0, size=tileDefaultSize, texture=0, padding=tileDefaultPaddi
4311
4325
  // create tile info object
4312
4326
  const textureInfo = typeof texture === 'number' ?
4313
4327
  textureInfos[texture] : texture;
4314
- if (headlessMode && !textureInfo?.size.x)
4315
- return new TileInfo(new Vector2, size.copy(), textureInfo, padding, bleed); // no image loaded, no place in it
4316
- false&&ASSERT(textureInfo instanceof TextureInfo, 'tile texture is not loaded');
4317
- false&&ASSERT(textureInfo.size.x > 0, 'tile texture is not loaded');
4328
+ if (!textureInfo?.size.x)
4329
+ {
4330
+ // no image: headless loads none and keeps the size; an image that failed to load, whose warning named it, gives
4331
+ // a tile of no size, which draws nothing; a slot never given an image is a mistake a debug build points out
4332
+ false&&ASSERT(headlessMode || textureInfo instanceof TextureInfo, 'tile texture is not loaded', texture);
4333
+ return new TileInfo(new Vector2, headlessMode ? size.copy() : new Vector2, textureInfo, padding, bleed);
4334
+ }
4318
4335
 
4319
4336
  // get the position of the tile
4320
4337
  const sizePaddedX = size.x + padding*2;
@@ -4782,8 +4799,8 @@ function drawTile(pos, size=vec2(1), tileInfo, color=WHITE,
4782
4799
  if (headlessMode && !context) return; // headless has no canvas, only a context passed in is drawn to
4783
4800
 
4784
4801
  const textureInfo = tileInfo?.textureInfo;
4785
- if (textureInfo && !(tileInfo.size.x && tileInfo.size.y))
4786
- return; // a tile with no area draws nothing, like a sprite still loading
4802
+ if (textureInfo && !(tileInfo.size.x && tileInfo.size.y && textureInfo.size.x))
4803
+ return; // a tile with no area draws nothing, like a sprite still loading, and nor does an image that failed
4787
4804
  const bleed = tileInfo?.bleed ?? 0;
4788
4805
  if (useWebGL && glEnable)
4789
4806
  {
@@ -4877,7 +4894,7 @@ function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
4877
4894
  * @param {boolean} [screenSpace=drawScreenSpace]
4878
4895
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
4879
4896
  * @memberof Draw */
4880
- function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=CLEAR_WHITE, angle=0, useWebGL=glEnable, screenSpace=drawScreenSpace, context)
4897
+ function drawRectGradient(pos, size=vec2(1), colorTop=WHITE, colorBottom=CLEAR_WHITE, angle=0, useWebGL=glEnable, screenSpace=drawScreenSpace, context)
4881
4898
  {
4882
4899
  false&&ASSERT(isVector2(pos), 'pos must be a vec2');
4883
4900
  false&&ASSERT(isVector2(size), 'size must be a vec2');
@@ -5872,8 +5889,7 @@ function bakeTintedImage(image, color, additiveColor)
5872
5889
  * @param {number} dHeight
5873
5890
  * @param {Color} color
5874
5891
  * @param {Color} [additiveColor]
5875
- * @param {number} [bleed] - How many pixels to shrink the source, used to fix bleeding
5876
- * @memberof Draw */
5892
+ * @param {number} [bleed] - How many pixels to shrink the source, used to fix bleeding */
5877
5893
  function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight, color, additiveColor, bleed=0)
5878
5894
  {
5879
5895
  const sx2 = bleed;
@@ -6699,8 +6715,9 @@ function inputInit()
6699
6715
  if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
6700
6716
  audioContext.resume();
6701
6717
 
6702
- // a press in the bars around a letterboxed canvas is not one on its edge; a drag out of it still moves
6703
- if (!inCanvas(e.x, e.y))
6718
+ // a press in the bars around a letterboxed canvas is not one on its edge; a drag out of it still moves; under
6719
+ // pointer lock the mouse stays where the lock began, which may be in a bar, and every click is the game's
6720
+ if (!pointerLockIsActive() && !inCanvas(e.x, e.y))
6704
6721
  return;
6705
6722
  inputData[0][e.button] = 3;
6706
6723
 
@@ -7117,9 +7134,11 @@ function inputUpdate()
7117
7134
  (gamepadIsDown(12,i)&&1) - (gamepadIsDown(13,i)&&1));
7118
7135
  }
7119
7136
 
7120
- // copy dpad to left analog stick when pressed
7137
+ // copy dpad to left analog stick when pressed; a gamepad with no sticks has that one, at rest when not
7121
7138
  if (gamepadDirectionEmulateStick && (dpad.x || dpad.y))
7122
7139
  sticks[0] = dpad.clampLength();
7140
+ else if (gamepadDirectionEmulateStick && !sticks.length)
7141
+ sticks[0] = vec2();
7123
7142
  }
7124
7143
 
7125
7144
  // disable touch gamepad if using real gamepad
@@ -7866,7 +7885,7 @@ class Sound
7866
7885
  */
7867
7886
  constructor(asset, randomness, range, taper=soundDefaultTaper, onloadCallback)
7868
7887
  {
7869
- if (!soundEnable || headlessMode)
7888
+ if (!soundEnable || headlessMode || !audioContext) // a browser with no audio makes none either
7870
7889
  {
7871
7890
  // no sound is made: it counts as loaded, so a game that waits for its sounds goes on
7872
7891
  this.loadedPercent = 1;
@@ -7936,8 +7955,8 @@ class Sound
7936
7955
  // load the audio file, a URL object as bundlers give works like its string;
7937
7956
  // report failures rather than leaving an unhandled rejection, the sound just stays unloaded and silent
7938
7957
  const filename = asset + '';
7939
- engineAddLoad(this.loadSound(filename).catch(e=>
7940
- false&&LOG('Sound load failed for', filename, '-', e.message))); // startup waits for it
7958
+ engineAddLoad(this.loadSound(filename).catch(e=> // startup waits for it, and a release build says why too
7959
+ console.warn('Sound load failed for', filename, '-', e?.message ?? e)));
7941
7960
  }
7942
7961
  }
7943
7962
 
@@ -10986,7 +11005,7 @@ function glSetTextureWrap(texture, wrap=true)
10986
11005
  /** Compile WebGL shader of the given type, will throw errors if in debug mode
10987
11006
  * @param {string} source
10988
11007
  * @param {number} type
10989
- * @return {WebGLShader}
11008
+ * @return {WebGLShader|undefined} - undefined with no WebGL
10990
11009
  * @memberof WebGL */
10991
11010
  function glCompileShader(source, type)
10992
11011
  {
@@ -11006,7 +11025,7 @@ function glCompileShader(source, type)
11006
11025
  /** Create WebGL program with given shaders
11007
11026
  * @param {string} vsSource
11008
11027
  * @param {string} fsSource
11009
- * @return {WebGLProgram}
11028
+ * @return {WebGLProgram|undefined} - undefined with no WebGL
11010
11029
  * @memberof WebGL */
11011
11030
  function glCreateProgram(vsSource, fsSource)
11012
11031
  {
@@ -11054,7 +11073,7 @@ function glShaderProgram(shader)
11054
11073
  * @param {HTMLImageElement|HTMLCanvasElement|OffscreenCanvas|ImageBitmap} [image]
11055
11074
  * @param {boolean|Array<number>} [wrap] - true for REPEAT, false for CLAMP_TO_EDGE, or the WebGL modes across and down
11056
11075
  * @param {boolean} [pixelated] - Hard edged or smooth, undefined follows tilesPixelated
11057
- * @return {WebGLTexture}
11076
+ * @return {WebGLTexture|undefined} - undefined with no WebGL
11058
11077
  * @memberof WebGL */
11059
11078
  function glCreateTexture(image, wrap=false, pixelated=tilesPixelated)
11060
11079
  {
@@ -16914,7 +16933,7 @@ class Box2dObject extends EngineObject
16914
16933
  * @param {number} angle */
16915
16934
  setTransform(pos, angle)
16916
16935
  {
16917
- this.pos = pos.copy();
16936
+ this.pos.set(pos.x, pos.y); // its own pos kept, as an EngineObject's is
16918
16937
  this.angle = angle;
16919
16938
  // box2d uses reverse angle
16920
16939
  const x = pos.x, y = pos.y;
@@ -16930,7 +16949,7 @@ class Box2dObject extends EngineObject
16930
16949
  * @param {Vector2} pos */
16931
16950
  setPosition(pos)
16932
16951
  {
16933
- this.pos = pos.copy();
16952
+ this.pos.set(pos.x, pos.y); // its own pos kept, as an EngineObject's is
16934
16953
  const x = pos.x, y = pos.y;
16935
16954
  box2dWhenUnlocked(()=>
16936
16955
  {
@@ -18431,7 +18450,8 @@ class Box2dPlugin
18431
18450
  return box2d.raycastAll(start, end, includeSensors)[0];
18432
18451
  }
18433
18452
 
18434
- /** box aabb cast and return all the objects
18453
+ /** Every object whose shapes' bounding boxes overlap a box, not the shapes themselves, so near the corner of a
18454
+ * turned box or a circle it finds one the box does not touch; use pointCast or a raycast for exact
18435
18455
  * @param {Vector2} pos
18436
18456
  * @param {Vector2} size
18437
18457
  * @param {boolean} [includeSensors] - Also find sensors, trigger zones are passed through by default
@@ -18461,7 +18481,8 @@ class Box2dPlugin
18461
18481
  return queryObjects;
18462
18482
  }
18463
18483
 
18464
- /** box aabb cast and return the first object
18484
+ /** The first object whose shapes' bounding boxes overlap a box, not the shapes themselves, so near the corner of
18485
+ * a turned box or a circle it finds one the box does not touch; use pointCast or a raycast for exact
18465
18486
  * @param {Vector2} pos
18466
18487
  * @param {Vector2} size
18467
18488
  * @param {boolean} [includeSensors] - Also find sensors, trigger zones are passed through by default
@@ -18721,8 +18742,10 @@ async function box2dInit()
18721
18742
  {
18722
18743
  if (o.body)
18723
18744
  {
18745
+ // moved in place, as the engine moves an EngineObject's pos, so what holds it follows the body;
18724
18746
  // box2d uses reverse angle
18725
- o.pos = box2d.vec2From(o.body.GetPosition());
18747
+ const p = o.body.GetPosition();
18748
+ o.pos.set(p.get_x(), p.get_y());
18726
18749
  o.angle = -o.body.GetAngle();
18727
18750
  }
18728
18751
  }
@@ -19218,6 +19241,8 @@ let textureSheetPendingCount = 0;
19218
19241
  const textureSheetLoaded = new Map;
19219
19242
  // a load's key: what it was given, a vector by its numbers
19220
19243
  const textureSheetLoadKey = (...parts)=> parts.map((p)=> isVector2(p) ? p.x + ',' + p.y : String(p)).join('|');
19244
+ // a load whose file failed is forgotten, so loading it again tries again, as when the server comes back
19245
+ const textureSheetLoadFailed = (key, loaded)=> textureSheetLoaded.get(key) === loaded && textureSheetLoaded.delete(key);
19221
19246
 
19222
19247
  /**
19223
19248
  * Texture Sheet - A texture that images are packed into as they load
@@ -19374,7 +19399,8 @@ class TextureSheet
19374
19399
  * - Pass frameSize for animations, then step through them with TileInfo.frame
19375
19400
  * - Grid images keep their layout and frames wrap down to the next row
19376
19401
  * - Pass sourcePadding if the source image has padding baked in around frames
19377
- * - The same image loaded again with the same settings gives back what the first load did, packed once
19402
+ * - The same image loaded again with the same settings gives back what the first load did, packed once, unless
19403
+ * that load failed
19378
19404
  * @param {string} src - Image source path
19379
19405
  * @param {Vector2|number} [frameSize] - Size of each animation frame in pixels, or the whole image less its
19380
19406
  * source padding if not passed
@@ -19429,7 +19455,7 @@ function loadSprite(src, frameSize, padding=textureSheetPadding, sourcePadding=0
19429
19455
  if (!added)
19430
19456
  {
19431
19457
  // leave the tile empty, no sheet can hold it
19432
- false&&LOG('loadSprite image is too large to fit on a texture sheet:', src);
19458
+ console.warn('loadSprite image is too large to fit on a texture sheet:', src);
19433
19459
  return;
19434
19460
  }
19435
19461
  Object.assign(tileInfo, added.tile);
@@ -19438,7 +19464,8 @@ function loadSprite(src, frameSize, padding=textureSheetPadding, sourcePadding=0
19438
19464
  else
19439
19465
  {
19440
19466
  // leave the tile empty if the image failed to load
19441
- false&&LOG('loadSprite failed to load image:', src);
19467
+ console.warn('loadSprite failed to load image:', src);
19468
+ textureSheetLoadFailed(key, tileInfo);
19442
19469
  }
19443
19470
  });
19444
19471
 
@@ -19456,7 +19483,8 @@ function loadSprite(src, frameSize, padding=textureSheetPadding, sourcePadding=0
19456
19483
  * is not read, cut it into its tiles first
19457
19484
  * - An image that fails to load, or that no sheet can hold, adds no tiles and says so in the console, so the tiles
19458
19485
  * of the images after it move up
19459
- * - The same images loaded again with the same settings give back what the first load did, packed once
19486
+ * - The same images loaded again with the same settings give back what the first load did, packed once, unless
19487
+ * an image of it failed
19460
19488
  * @param {Array<string>} sources - Image source paths
19461
19489
  * @param {Vector2|number} [tileSize] - Size of a tile in pixels
19462
19490
  * @param {number} [padding] - How many pixels padding around each tile on the sheet
@@ -19498,6 +19526,7 @@ function loadTiles(sources, tileSize=tileDefaultSize, padding=textureSheetPaddin
19498
19526
  // the whole tiles of it, an edge past the last one left out
19499
19527
  const columns = image.width / size.x | 0, rows = image.height / size.y | 0, count = columns * rows;
19500
19528
  const added = count && textureSheetAdd(vec2(columns * size.x, rows * size.y), size, padding, 0);
19529
+ image.width || textureSheetLoadFailed(key, set);
19501
19530
  if (!added)
19502
19531
  return console.warn('loadTiles: ' + src + (count ? ' does not fit on a texture sheet' :
19503
19532
  ' failed to load, or is smaller than a tile') + ', its tiles are left out');
@@ -19516,7 +19545,8 @@ function loadTiles(sources, tileSize=tileDefaultSize, padding=textureSheetPaddin
19516
19545
  * - Aseprite frame tags become animations, so do names like run_0, run_1
19517
19546
  * - Trimmed frames are restored to their full source size when packed
19518
19547
  * - Rotated frames are rotated back upright when packed
19519
- * - The same atlas loaded again by its paths, with the same padding, gives back what the first load did, packed once
19548
+ * - The same atlas loaded again by its paths, with the same padding, gives back what the first load did, packed
19549
+ * once, unless that load failed
19520
19550
  * @param {string} imageSrc - Atlas image path
19521
19551
  * @param {string|Object} jsonSrc - Atlas json path, or already parsed json data
19522
19552
  * @param {number} [padding] - How many pixels padding around each frame
@@ -19571,7 +19601,7 @@ function loadAtlas(imageSrc, jsonSrc, padding=textureSheetPadding)
19571
19601
  const added = textureSheetAdd(blockSize, sourceSize, padding);
19572
19602
  if (!added)
19573
19603
  {
19574
- false&&LOG('loadAtlas frames are too large to fit on a texture sheet:', group.name);
19604
+ console.warn('loadAtlas frames are too large to fit on a texture sheet:', group.name);
19575
19605
  continue;
19576
19606
  }
19577
19607
  const {sheet, tile} = added;
@@ -19605,7 +19635,8 @@ function loadAtlas(imageSrc, jsonSrc, padding=textureSheetPadding)
19605
19635
  else
19606
19636
  {
19607
19637
  // leave the atlas empty if either file failed to load
19608
- false&&LOG('loadAtlas failed to load:', imageSrc, jsonSrc);
19638
+ console.warn('loadAtlas failed to load:', imageSrc, jsonSrc);
19639
+ textureSheetLoadFailed(key, atlas);
19609
19640
  }
19610
19641
  });
19611
19642
 
@@ -28048,9 +28079,9 @@ class HeightMap extends EngineObject3D
28048
28079
  levelSegment3D(from, to)
28049
28080
  {
28050
28081
  const m = this.pos3D, size = this.mapSize;
28051
- if (abs(to.x - m.x) > size.x / 2 || abs(to.z - m.z) > size.y / 2) return; // off the map
28082
+ if (abs(to.x - m.x) > size.x / 2 || abs(to.z - m.z) > size.y / 2) return undefined; // off the map
28052
28083
  const a = from.y - this.getHeight(from.x, from.z), b = to.y - this.getHeight(to.x, to.z);
28053
- if (a < 0 || b >= 0) return; // above all the way, or under from the start
28084
+ if (a < 0 || b >= 0) return undefined; // above all the way, or under from the start
28054
28085
  const distance = a / (a - b);
28055
28086
  return {distance, normal: this.getNormal(from.x + (to.x - from.x) * distance, from.z + (to.z - from.z) * distance)};
28056
28087
  }
@@ -29088,9 +29119,11 @@ class LensFlare3D extends EngineObject3D
29088
29119
  return this.made;
29089
29120
  }
29090
29121
 
29091
- // what the flare is of, seen from the camera: the way to it, how far it is, Infinity for the sun and for a
29092
- // directional light, which shines from its place toward the origin, and a point to find it on the screen by;
29093
- // undefined with no direction, or a light that is gone or at the camera
29122
+ /** What the flare is of, seen from the camera: the way to it, how far it is, Infinity for the sun and for a
29123
+ * directional light, which shines from its place toward the origin, and a point to find it on the screen by;
29124
+ * undefined with no direction, or a light that is gone or at the camera
29125
+ * @return {{direction: Vector3, distance: number, pos: Vector3}|undefined}
29126
+ * @ignore */
29094
29127
  flareSource()
29095
29128
  {
29096
29129
  const camera = render3D.camera.pos, light = this.light;
@@ -29106,9 +29139,11 @@ class LensFlare3D extends EngineObject3D
29106
29139
  return distance ? {direction: offset.scale(1 / distance), distance, pos} : undefined;
29107
29140
  }
29108
29141
 
29109
- // how the flare would show with nothing in the way: where its source is on the screen, how strong it is there,
29110
- // fading as it leaves the screen, its tint and the height its sizes are parts of; undefined when it would not
29111
- // show at all, behind the camera, off the screen, or of a light that is off or seen from outside its cone
29142
+ /** How the flare would show with nothing in the way: where its source is on the screen, how strong it is there,
29143
+ * fading as it leaves the screen, its tint and the height its sizes are parts of; undefined when it would not
29144
+ * show at all, behind the camera, off the screen, or of a light that is off or seen from outside its cone
29145
+ * @return {{sun: Vector2, center: Vector2, strength: number, tint: Color, height: number}|undefined}
29146
+ * @ignore */
29112
29147
  flareLook()
29113
29148
  {
29114
29149
  const source = this.flareSource(), center = mainCanvasSize.scale(.5);
@@ -32409,11 +32444,16 @@ function gltfFetch(uri, baseUrl, files)
32409
32444
  const path = clean(baseUrl + name);
32410
32445
  if (path === undefined)
32411
32446
  return Promise.reject(new Error('glTF needs ' + name + ', which is above the files given'));
32412
- const lower = path.toLowerCase(), file = path.slice(path.lastIndexOf('/') + 1);
32447
+ const lower = path.toLowerCase(), file = lower.slice(lower.lastIndexOf('/') + 1);
32413
32448
  const byPath = new Map([...files].map(([key, value])=> [clean(key)?.toLowerCase(), value]));
32414
32449
  const found = files.get(path) ?? byPath.get(lower), keys = [...byPath.keys()];
32415
32450
  if (found)
32416
32451
  return Promise.resolve(new Response(found));
32452
+ // what is in a folder dropped, not the folder, leaves the folder's own name out of the paths: the longest key
32453
+ // the path ends with, a folder and more, as a name alone is only taken when one file has it
32454
+ const end = keys.filter((k)=> k?.includes('/') && lower.endsWith('/' + k)).sort((a, b)=> b.length - a.length)[0];
32455
+ if (end)
32456
+ return Promise.resolve(new Response(byPath.get(end)));
32417
32457
  const named = new Set(keys.filter((k)=> k?.slice(k.lastIndexOf('/') + 1) === file).map((k)=> byPath.get(k)));
32418
32458
  if (named.size === 1)
32419
32459
  return Promise.resolve(new Response([...named][0]));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "littlejsengine",
3
- "version": "1.24.2",
3
+ "version": "1.24.3",
4
4
  "description": "LittleJS - Tiny and Fast HTML5 Game Engine",
5
5
  "main": "dist/littlejs.esm.js",
6
6
  "types": "dist/littlejs.d.ts",
package/plugins/box2d.js CHANGED
@@ -627,7 +627,7 @@ class Box2dObject extends EngineObject
627
627
  * @param {number} angle */
628
628
  setTransform(pos, angle)
629
629
  {
630
- this.pos = pos.copy();
630
+ this.pos.set(pos.x, pos.y); // its own pos kept, as an EngineObject's is
631
631
  this.angle = angle;
632
632
  // box2d uses reverse angle
633
633
  const x = pos.x, y = pos.y;
@@ -643,7 +643,7 @@ class Box2dObject extends EngineObject
643
643
  * @param {Vector2} pos */
644
644
  setPosition(pos)
645
645
  {
646
- this.pos = pos.copy();
646
+ this.pos.set(pos.x, pos.y); // its own pos kept, as an EngineObject's is
647
647
  const x = pos.x, y = pos.y;
648
648
  box2dWhenUnlocked(()=>
649
649
  {
@@ -2144,7 +2144,8 @@ class Box2dPlugin
2144
2144
  return box2d.raycastAll(start, end, includeSensors)[0];
2145
2145
  }
2146
2146
 
2147
- /** box aabb cast and return all the objects
2147
+ /** Every object whose shapes' bounding boxes overlap a box, not the shapes themselves, so near the corner of a
2148
+ * turned box or a circle it finds one the box does not touch; use pointCast or a raycast for exact
2148
2149
  * @param {Vector2} pos
2149
2150
  * @param {Vector2} size
2150
2151
  * @param {boolean} [includeSensors] - Also find sensors, trigger zones are passed through by default
@@ -2174,7 +2175,8 @@ class Box2dPlugin
2174
2175
  return queryObjects;
2175
2176
  }
2176
2177
 
2177
- /** box aabb cast and return the first object
2178
+ /** The first object whose shapes' bounding boxes overlap a box, not the shapes themselves, so near the corner of
2179
+ * a turned box or a circle it finds one the box does not touch; use pointCast or a raycast for exact
2178
2180
  * @param {Vector2} pos
2179
2181
  * @param {Vector2} size
2180
2182
  * @param {boolean} [includeSensors] - Also find sensors, trigger zones are passed through by default
@@ -2434,8 +2436,10 @@ async function box2dInit()
2434
2436
  {
2435
2437
  if (o.body)
2436
2438
  {
2439
+ // moved in place, as the engine moves an EngineObject's pos, so what holds it follows the body;
2437
2440
  // box2d uses reverse angle
2438
- o.pos = box2d.vec2From(o.body.GetPosition());
2441
+ const p = o.body.GetPosition();
2442
+ o.pos.set(p.get_x(), p.get_y());
2439
2443
  o.angle = -o.body.GetAngle();
2440
2444
  }
2441
2445
  }
package/plugins/gltf.js CHANGED
@@ -682,11 +682,16 @@ function gltfFetch(uri, baseUrl, files)
682
682
  const path = clean(baseUrl + name);
683
683
  if (path === undefined)
684
684
  return Promise.reject(new Error('glTF needs ' + name + ', which is above the files given'));
685
- const lower = path.toLowerCase(), file = path.slice(path.lastIndexOf('/') + 1);
685
+ const lower = path.toLowerCase(), file = lower.slice(lower.lastIndexOf('/') + 1);
686
686
  const byPath = new Map([...files].map(([key, value])=> [clean(key)?.toLowerCase(), value]));
687
687
  const found = files.get(path) ?? byPath.get(lower), keys = [...byPath.keys()];
688
688
  if (found)
689
689
  return Promise.resolve(new Response(found));
690
+ // what is in a folder dropped, not the folder, leaves the folder's own name out of the paths: the longest key
691
+ // the path ends with, a folder and more, as a name alone is only taken when one file has it
692
+ const end = keys.filter((k)=> k?.includes('/') && lower.endsWith('/' + k)).sort((a, b)=> b.length - a.length)[0];
693
+ if (end)
694
+ return Promise.resolve(new Response(byPath.get(end)));
690
695
  const named = new Set(keys.filter((k)=> k?.slice(k.lastIndexOf('/') + 1) === file).map((k)=> byPath.get(k)));
691
696
  if (named.size === 1)
692
697
  return Promise.resolve(new Response([...named][0]));
@@ -631,9 +631,9 @@ class HeightMap extends EngineObject3D
631
631
  levelSegment3D(from, to)
632
632
  {
633
633
  const m = this.pos3D, size = this.mapSize;
634
- if (abs(to.x - m.x) > size.x / 2 || abs(to.z - m.z) > size.y / 2) return; // off the map
634
+ if (abs(to.x - m.x) > size.x / 2 || abs(to.z - m.z) > size.y / 2) return undefined; // off the map
635
635
  const a = from.y - this.getHeight(from.x, from.z), b = to.y - this.getHeight(to.x, to.z);
636
- if (a < 0 || b >= 0) return; // above all the way, or under from the start
636
+ if (a < 0 || b >= 0) return undefined; // above all the way, or under from the start
637
637
  const distance = a / (a - b);
638
638
  return {distance, normal: this.getNormal(from.x + (to.x - from.x) * distance, from.z + (to.z - from.z) * distance)};
639
639
  }
@@ -1671,9 +1671,11 @@ class LensFlare3D extends EngineObject3D
1671
1671
  return this.made;
1672
1672
  }
1673
1673
 
1674
- // what the flare is of, seen from the camera: the way to it, how far it is, Infinity for the sun and for a
1675
- // directional light, which shines from its place toward the origin, and a point to find it on the screen by;
1676
- // undefined with no direction, or a light that is gone or at the camera
1674
+ /** What the flare is of, seen from the camera: the way to it, how far it is, Infinity for the sun and for a
1675
+ * directional light, which shines from its place toward the origin, and a point to find it on the screen by;
1676
+ * undefined with no direction, or a light that is gone or at the camera
1677
+ * @return {{direction: Vector3, distance: number, pos: Vector3}|undefined}
1678
+ * @ignore */
1677
1679
  flareSource()
1678
1680
  {
1679
1681
  const camera = render3D.camera.pos, light = this.light;
@@ -1689,9 +1691,11 @@ class LensFlare3D extends EngineObject3D
1689
1691
  return distance ? {direction: offset.scale(1 / distance), distance, pos} : undefined;
1690
1692
  }
1691
1693
 
1692
- // how the flare would show with nothing in the way: where its source is on the screen, how strong it is there,
1693
- // fading as it leaves the screen, its tint and the height its sizes are parts of; undefined when it would not
1694
- // show at all, behind the camera, off the screen, or of a light that is off or seen from outside its cone
1694
+ /** How the flare would show with nothing in the way: where its source is on the screen, how strong it is there,
1695
+ * fading as it leaves the screen, its tint and the height its sizes are parts of; undefined when it would not
1696
+ * show at all, behind the camera, off the screen, or of a light that is off or seen from outside its cone
1697
+ * @return {{sun: Vector2, center: Vector2, strength: number, tint: Color, height: number}|undefined}
1698
+ * @ignore */
1695
1699
  flareLook()
1696
1700
  {
1697
1701
  const source = this.flareSource(), center = mainCanvasSize.scale(.5);
@@ -37,6 +37,8 @@ let textureSheetPendingCount = 0;
37
37
  const textureSheetLoaded = new Map;
38
38
  // a load's key: what it was given, a vector by its numbers
39
39
  const textureSheetLoadKey = (...parts)=> parts.map((p)=> isVector2(p) ? p.x + ',' + p.y : String(p)).join('|');
40
+ // a load whose file failed is forgotten, so loading it again tries again, as when the server comes back
41
+ const textureSheetLoadFailed = (key, loaded)=> textureSheetLoaded.get(key) === loaded && textureSheetLoaded.delete(key);
40
42
 
41
43
  /**
42
44
  * Texture Sheet - A texture that images are packed into as they load
@@ -193,7 +195,8 @@ class TextureSheet
193
195
  * - Pass frameSize for animations, then step through them with TileInfo.frame
194
196
  * - Grid images keep their layout and frames wrap down to the next row
195
197
  * - Pass sourcePadding if the source image has padding baked in around frames
196
- * - The same image loaded again with the same settings gives back what the first load did, packed once
198
+ * - The same image loaded again with the same settings gives back what the first load did, packed once, unless
199
+ * that load failed
197
200
  * @param {string} src - Image source path
198
201
  * @param {Vector2|number} [frameSize] - Size of each animation frame in pixels, or the whole image less its
199
202
  * source padding if not passed
@@ -248,7 +251,7 @@ function loadSprite(src, frameSize, padding=textureSheetPadding, sourcePadding=0
248
251
  if (!added)
249
252
  {
250
253
  // leave the tile empty, no sheet can hold it
251
- LOG('loadSprite image is too large to fit on a texture sheet:', src);
254
+ console.warn('loadSprite image is too large to fit on a texture sheet:', src);
252
255
  return;
253
256
  }
254
257
  Object.assign(tileInfo, added.tile);
@@ -257,7 +260,8 @@ function loadSprite(src, frameSize, padding=textureSheetPadding, sourcePadding=0
257
260
  else
258
261
  {
259
262
  // leave the tile empty if the image failed to load
260
- LOG('loadSprite failed to load image:', src);
263
+ console.warn('loadSprite failed to load image:', src);
264
+ textureSheetLoadFailed(key, tileInfo);
261
265
  }
262
266
  });
263
267
 
@@ -275,7 +279,8 @@ function loadSprite(src, frameSize, padding=textureSheetPadding, sourcePadding=0
275
279
  * is not read, cut it into its tiles first
276
280
  * - An image that fails to load, or that no sheet can hold, adds no tiles and says so in the console, so the tiles
277
281
  * of the images after it move up
278
- * - The same images loaded again with the same settings give back what the first load did, packed once
282
+ * - The same images loaded again with the same settings give back what the first load did, packed once, unless
283
+ * an image of it failed
279
284
  * @param {Array<string>} sources - Image source paths
280
285
  * @param {Vector2|number} [tileSize] - Size of a tile in pixels
281
286
  * @param {number} [padding] - How many pixels padding around each tile on the sheet
@@ -317,6 +322,7 @@ function loadTiles(sources, tileSize=tileDefaultSize, padding=textureSheetPaddin
317
322
  // the whole tiles of it, an edge past the last one left out
318
323
  const columns = image.width / size.x | 0, rows = image.height / size.y | 0, count = columns * rows;
319
324
  const added = count && textureSheetAdd(vec2(columns * size.x, rows * size.y), size, padding, 0);
325
+ image.width || textureSheetLoadFailed(key, set);
320
326
  if (!added)
321
327
  return console.warn('loadTiles: ' + src + (count ? ' does not fit on a texture sheet' :
322
328
  ' failed to load, or is smaller than a tile') + ', its tiles are left out');
@@ -335,7 +341,8 @@ function loadTiles(sources, tileSize=tileDefaultSize, padding=textureSheetPaddin
335
341
  * - Aseprite frame tags become animations, so do names like run_0, run_1
336
342
  * - Trimmed frames are restored to their full source size when packed
337
343
  * - Rotated frames are rotated back upright when packed
338
- * - The same atlas loaded again by its paths, with the same padding, gives back what the first load did, packed once
344
+ * - The same atlas loaded again by its paths, with the same padding, gives back what the first load did, packed
345
+ * once, unless that load failed
339
346
  * @param {string} imageSrc - Atlas image path
340
347
  * @param {string|Object} jsonSrc - Atlas json path, or already parsed json data
341
348
  * @param {number} [padding] - How many pixels padding around each frame
@@ -390,7 +397,7 @@ function loadAtlas(imageSrc, jsonSrc, padding=textureSheetPadding)
390
397
  const added = textureSheetAdd(blockSize, sourceSize, padding);
391
398
  if (!added)
392
399
  {
393
- LOG('loadAtlas frames are too large to fit on a texture sheet:', group.name);
400
+ console.warn('loadAtlas frames are too large to fit on a texture sheet:', group.name);
394
401
  continue;
395
402
  }
396
403
  const {sheet, tile} = added;
@@ -424,7 +431,8 @@ function loadAtlas(imageSrc, jsonSrc, padding=textureSheetPadding)
424
431
  else
425
432
  {
426
433
  // leave the atlas empty if either file failed to load
427
- LOG('loadAtlas failed to load:', imageSrc, jsonSrc);
434
+ console.warn('loadAtlas failed to load:', imageSrc, jsonSrc);
435
+ textureSheetLoadFailed(key, atlas);
428
436
  }
429
437
  });
430
438