littlejsengine 1.24.1 → 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.
Files changed (44) hide show
  1. package/CHANGELOG.md +78 -1
  2. package/EDITOR.md +2 -0
  3. package/FAQ.md +13 -2
  4. package/README.md +1 -1
  5. package/REFERENCE.md +22 -7
  6. package/dist/littlejs.d.ts +204 -118
  7. package/dist/littlejs.esm.js +2479 -1983
  8. package/dist/littlejs.esm.min.js +3 -3
  9. package/dist/littlejs.js +2476 -1983
  10. package/dist/littlejs.min.js +2 -2
  11. package/dist/littlejs.release.js +2434 -1990
  12. package/package.json +1 -1
  13. package/plugins/box2d.js +9 -5
  14. package/plugins/gltf.js +67 -9
  15. package/plugins/lightSystem.js +1 -1
  16. package/plugins/math3d.js +1014 -1013
  17. package/plugins/medalSystem.js +7 -11
  18. package/plugins/particleEffects.js +2 -2
  19. package/plugins/pathFinder.js +2 -2
  20. package/plugins/pluginExport.js +1 -0
  21. package/plugins/render3d.js +131 -55
  22. package/plugins/render3dEditor.js +24 -10
  23. package/plugins/render3dExtras.js +41 -25
  24. package/plugins/render3dLevel.js +21 -12
  25. package/plugins/render3dVoxels.js +2 -2
  26. package/plugins/textureSheet.js +109 -7
  27. package/plugins/tweakables.js +1 -1
  28. package/plugins/tweenSystem.js +2 -2
  29. package/plugins/uiSystem.js +6 -2
  30. package/src/engine.js +60 -23
  31. package/src/engineAudio.js +16 -6
  32. package/src/engineBuild.mjs +49 -2
  33. package/src/engineDebug.js +4 -1
  34. package/src/engineDraw.js +40 -25
  35. package/src/engineEditor.js +82 -50
  36. package/src/engineExport.js +2 -0
  37. package/src/engineInput.js +21 -4
  38. package/src/engineMath.js +6 -2
  39. package/src/engineObject.js +675 -675
  40. package/src/engineParticles.js +1 -1
  41. package/src/engineSettings.js +16 -0
  42. package/src/engineTileLayer.js +85 -22
  43. package/src/engineUtilities.js +20 -8
  44. package/src/engineWebGL.js +15 -11
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "littlejsengine",
3
- "version": "1.24.1",
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
@@ -371,11 +371,24 @@ async function loadGLTF(url)
371
371
  }
372
372
 
373
373
  /** Parse a model from GLB bytes or glTF JSON, fetching the buffers and images it refers to
374
+ * - A .gltf names its .bin and image files, which are fetched from baseUrl, or found among files: what a game
375
+ * has in hand, like the files dropped on the page, by their paths in the drop; with files, baseUrl is the
376
+ * .gltf's own folder among them ('' or 'models/house/'), its names are read from there, and a name found nowhere
377
+ * there is taken by its file name alone only when one file of the drop has it; only an http, https or blob uri
378
+ * is fetched then
379
+ * - A file the model needs that is not found is named in the error, and an image that can not be read is named in
380
+ * a warning and left out
374
381
  * @param {ArrayBuffer|Object|string} data - GLB bytes, or the glTF JSON as bytes, text or an object
375
382
  * @param {string} [baseUrl] - Where the .bin and image files are, with its trailing slash; loadGLTF passes the file's folder
383
+ * @param {Map<string, Blob>} [files] - The files it refers to, by their paths, in place of fetching them
376
384
  * @return {Promise<GLTFModel>}
385
+ * @example
386
+ * // the files of a drop, a .gltf with its .bin and textures, by their names
387
+ * const files = new Map([...dataTransfer.files].map((file)=> [file.name, file]));
388
+ * const gltf = [...files.values()].find((file)=> file.name.endsWith('.gltf'));
389
+ * const model = await parseGLTF(await gltf.text(), '', files);
377
390
  * @memberof GLTF */
378
- async function parseGLTF(data, baseUrl='')
391
+ async function parseGLTF(data, baseUrl='', files)
379
392
  {
380
393
  let json = data, glbBuffer;
381
394
  if (data instanceof ArrayBuffer)
@@ -419,7 +432,7 @@ async function parseGLTF(data, baseUrl='')
419
432
  ASSERT(glbBuffer && !i, 'a buffer without a uri is the GLB chunk, and only the first can be');
420
433
  return glbBuffer;
421
434
  }
422
- return gltfFetch(buffer.uri, baseUrl).then(r=> r.arrayBuffer());
435
+ return gltfFetch(buffer.uri, baseUrl, files).then(r=> r.arrayBuffer());
423
436
  }));
424
437
 
425
438
  // the textures, decoded together first; none without WebGL, and a failed image only logs; the base color, normal
@@ -438,7 +451,7 @@ async function parseGLTF(data, baseUrl='')
438
451
  const image = json.images[source], sampler = json.samplers?.[texture.sampler] || {};
439
452
  let blob;
440
453
  if (image.uri)
441
- blob = await gltfFetch(image.uri, baseUrl).then(r=> r.blob());
454
+ blob = await gltfFetch(image.uri, baseUrl, files).then(r=> r.blob());
442
455
  else
443
456
  {
444
457
  const view = json.bufferViews[image.bufferView];
@@ -455,14 +468,18 @@ async function parseGLTF(data, baseUrl='')
455
468
  // REPEAT by default, and hard edged only when its sampler says NEAREST, not as the game's tiles are
456
469
  return new TextureInfo(bitmap, true, [sampler.wrapS ?? 10497, sampler.wrapT ?? 10497], sampler.magFilter === 9728);
457
470
  }
458
- catch (e) { LOG('glTF image not loaded', e); }
471
+ catch (e) { console.warn('glTF image not loaded, left out: ' + (e?.message || e)); }
459
472
  }));
460
473
 
461
474
  // the parts: the scene's nodes walked with their transforms, every primitive of a node's mesh placed by it;
462
475
  // each node's parent and resting place are kept, so an animation can move a part from where it rests
463
- const parts = [], parents = [], restInverse = [], restPose = [];
476
+ const parts = [], parents = [], restInverse = [], restPose = [], visited = new Set;
464
477
  const visit = (index, parentMatrix, parentIndex, parentRest)=>
465
478
  {
479
+ // nodes are trees, so one reached again is a cycle or a node with two parents, a file problem
480
+ if (visited.has(index))
481
+ throw new Error('glTF node ' + index + ' is reached twice, nodes must form trees');
482
+ visited.add(index);
466
483
  const node = json.nodes[index], local = gltfNodeMatrix(node);
467
484
  let matrix = parentMatrix ? parentMatrix.copy().multiply(local) : local;
468
485
  let rest = parentRest && parentRest.copy().multiply(local); // where it rests, when that is not where it is baked
@@ -645,8 +662,42 @@ function gltfSample(channel, time, out)
645
662
  }
646
663
 
647
664
  // fetch a uri beside the model, or decode a data uri without going out
648
- function gltfFetch(uri, baseUrl)
665
+ function gltfFetch(uri, baseUrl, files)
649
666
  {
667
+ // among the files given, by its path from the model's folder, or by its name when only one file has it; a uri
668
+ // of the web is still fetched, and no other kind is
669
+ if (files && !/^(data|https?|blob):/i.test(uri))
670
+ {
671
+ let name = uri.replace(/[?#].*$/, '');
672
+ try { name = decodeURIComponent(name); } catch {}
673
+ // a path with its . and .. steps taken, the same for the keys, which may start with ./ or / or use \
674
+ const clean = (text)=>
675
+ {
676
+ const parts = [];
677
+ for (const part of text.replace(/\\/g, '/').split('/'))
678
+ if (part === '..' ? !parts.pop() : part && part !== '.' && !parts.push(part))
679
+ return; // above the top of the files
680
+ return parts.join('/');
681
+ };
682
+ const path = clean(baseUrl + name);
683
+ if (path === undefined)
684
+ return Promise.reject(new Error('glTF needs ' + name + ', which is above the files given'));
685
+ const lower = path.toLowerCase(), file = lower.slice(lower.lastIndexOf('/') + 1);
686
+ const byPath = new Map([...files].map(([key, value])=> [clean(key)?.toLowerCase(), value]));
687
+ const found = files.get(path) ?? byPath.get(lower), keys = [...byPath.keys()];
688
+ if (found)
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)));
695
+ const named = new Set(keys.filter((k)=> k?.slice(k.lastIndexOf('/') + 1) === file).map((k)=> byPath.get(k)));
696
+ if (named.size === 1)
697
+ return Promise.resolve(new Response([...named][0]));
698
+ return Promise.reject(new Error('glTF needs ' + path + (named.size ? ', and more than one file has its name' :
699
+ ', which is not among the files given')));
700
+ }
650
701
  if (uri.startsWith('data:'))
651
702
  {
652
703
  const comma = uri.indexOf(','), bytes = atob(uri.slice(comma + 1)), data = new Uint8Array(bytes.length);
@@ -661,7 +712,7 @@ function gltfFetch(uri, baseUrl)
661
712
  const url = base ? new URL(uri, base).href : baseUrl + uri;
662
713
  return fetch(url).then(r=>
663
714
  {
664
- if (!r.ok) throw new Error('glTF file not found: ' + url);
715
+ if (!r.ok) throw new Error('glTF needs ' + uri + ', not found at ' + url);
665
716
  return r;
666
717
  });
667
718
  }
@@ -692,11 +743,18 @@ function gltfAccessor(json, buffers, index)
692
743
  const size = Type.BYTES_PER_ELEMENT;
693
744
  const scales = /** @type {Array<[Object, number]>} */ ([[Int8Array, 127], [Uint8Array, 255], [Int16Array, 32767], [Uint16Array, 65535]]);
694
745
  const scale = a.normalized ? new Map(scales).get(Type) || 1 : 1;
746
+ // with a buffer under it the count is bounded by the buffer, without one it is all zeros and a sparse few,
747
+ // which no real model makes millions of
748
+ if (!view && !(a.count <= 1 << 20))
749
+ throw new Error('glTF accessor ' + index + ' has no buffer and a count of ' + a.count);
750
+ // its elements must lie in its buffer before so many floats are made for them
751
+ const viewEnd = view && (buffers[view.buffer]?.byteLength ?? 0), stride = view?.byteStride || components * size;
752
+ if (view && !((view.byteOffset || 0) + (a.byteOffset || 0) + (a.count - 1) * stride + components * size <= viewEnd))
753
+ throw new Error('glTF accessor ' + index + ' reaches past its buffer');
695
754
  const out = new Float32Array(a.count * components);
696
755
  if (view)
697
756
  {
698
757
  const buffer = buffers[view.buffer], offset = (view.byteOffset || 0) + (a.byteOffset || 0);
699
- const stride = view.byteStride || components * size;
700
758
  if (stride === components * size)
701
759
  out.set(new Type(buffer, offset, a.count * components)); // packed, one view over all of it
702
760
  else
@@ -795,7 +853,7 @@ function gltfPart(json, buffers, textures, primitive, matrix, name)
795
853
  let gltfWhiteTextureInfo;
796
854
  function gltfWhiteTexture()
797
855
  {
798
- if (gltfWhiteTextureInfo || !glContext || typeof OffscreenCanvas == 'undefined') return gltfWhiteTextureInfo;
856
+ if (gltfWhiteTextureInfo || !glContext || !canvasAvailable()) return gltfWhiteTextureInfo;
799
857
  const context = createCanvasContext(1);
800
858
  context.fillStyle = '#fff';
801
859
  context.fillRect(0, 0, 1, 1);
@@ -151,7 +151,7 @@ class LightSystemPlugin
151
151
  }
152
152
 
153
153
  // where Canvas2D draws go during the shadow and emissive passes
154
- lightSystem.shadowContext ||= new OffscreenCanvas(1, 1).getContext('2d');
154
+ lightSystem.shadowContext ||= createCanvasContext(1);
155
155
 
156
156
  // resolve texture size default at init time (mainCanvasSize may
157
157
  // not be set yet at the moment the constructor first ran), and