littlejsengine 1.24.3 → 1.26.0

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 (53) hide show
  1. package/CHANGELOG.md +123 -0
  2. package/EDITOR.md +2 -2
  3. package/FAQ.md +55 -11
  4. package/README.md +6 -2
  5. package/REFERENCE.md +203 -57
  6. package/dist/littlejs.d.ts +933 -313
  7. package/dist/littlejs.esm.js +5143 -2419
  8. package/dist/littlejs.esm.min.js +38 -24
  9. package/dist/littlejs.js +5132 -2416
  10. package/dist/littlejs.min.js +37 -23
  11. package/dist/littlejs.release.js +4905 -2232
  12. package/package.json +4 -4
  13. package/plugins/box2d.js +141 -56
  14. package/plugins/drawUtilities.js +374 -380
  15. package/plugins/gltf.js +383 -60
  16. package/plugins/lightSystem.js +412 -30
  17. package/plugins/math3d.js +1015 -1014
  18. package/plugins/medalSystem.js +19 -7
  19. package/plugins/newgrounds.js +5 -6
  20. package/plugins/parallax.js +1 -0
  21. package/plugins/particleEffects.js +47 -14
  22. package/plugins/pathFinder.js +60 -38
  23. package/plugins/pluginExport.js +11 -0
  24. package/plugins/postProcess.js +200 -67
  25. package/plugins/render3d.js +493 -2391
  26. package/plugins/render3dDebug.js +5 -8
  27. package/plugins/render3dEditor.js +84 -9
  28. package/plugins/render3dExtras.js +122 -33
  29. package/plugins/render3dLevel.js +67 -8
  30. package/plugins/render3dMesh.js +1158 -0
  31. package/plugins/render3dObject.js +1451 -0
  32. package/plugins/render3dVoxels.js +15 -3
  33. package/plugins/textureSheet.js +9 -8
  34. package/plugins/threejs.js +1 -1
  35. package/plugins/tweenSystem.js +117 -87
  36. package/plugins/uiSystem.js +62 -28
  37. package/plugins/wavedash.js +229 -0
  38. package/src/engine.js +192 -40
  39. package/src/engineAudio.js +47 -27
  40. package/src/engineBuild.mjs +6 -1
  41. package/src/engineDebug.js +23 -162
  42. package/src/engineDraw.js +148 -43
  43. package/src/engineEditor.js +153 -40
  44. package/src/engineExport.js +0 -3
  45. package/src/engineInput.js +31 -25
  46. package/src/engineMath.js +38 -20
  47. package/src/engineObject.js +68 -50
  48. package/src/engineParticles.js +66 -18
  49. package/src/engineRelease.js +18 -15
  50. package/src/engineSettings.js +48 -9
  51. package/src/engineTileLayer.js +44 -18
  52. package/src/engineUtilities.js +56 -11
  53. package/src/engineWebGL.js +68 -22
@@ -458,7 +458,11 @@ let soundPauseWhenHidden = true;
458
458
  /** Set position of camera in world space
459
459
  * @param {Vector2} pos
460
460
  * @memberof Settings */
461
- function setCameraPos(pos) { cameraPos = pos.copy(); }
461
+ function setCameraPos(pos)
462
+ {
463
+ ASSERT(isVector2(pos), 'setCameraPos: pos is a vec2, like vec2(5, 2)', pos);
464
+ cameraPos = pos.copy();
465
+ }
462
466
 
463
467
  /** Set angle of camera in world space
464
468
  * @param {number} angle
@@ -472,6 +476,7 @@ function setCameraScale(scale) { cameraScale = scale; }
472
476
 
473
477
  /** Set scale applied to engine time
474
478
  * - 0 freezes the game like a pause, gameUpdatePost and input still run so the game can set it back
479
+ * - Above 1 the fixed step runs that many updates each frame, so a large scale costs as many updates
475
480
  * @param {number} scale - 0 or more
476
481
  * @memberof Settings */
477
482
  function setTimeScale(scale)
@@ -491,12 +496,20 @@ function setCanvasColorTiles(colorTiles) { canvasColorTiles = colorTiles; }
491
496
  /** Set color to clear the canvas to before render, does not clear if alpha is 0
492
497
  * @param {Color} color
493
498
  * @memberof Settings */
494
- function setCanvasClearColor(color) { canvasClearColor = color.copy(); }
499
+ function setCanvasClearColor(color)
500
+ {
501
+ ASSERT(isColor(color), 'setCanvasClearColor: color is a Color, like hsl(.6, .5, .2)', color);
502
+ canvasClearColor = color.copy();
503
+ }
495
504
 
496
505
  /** Set max size of the canvas
497
506
  * @param {Vector2} size
498
507
  * @memberof Settings */
499
- function setCanvasMaxSize(size) { canvasMaxSize = size.copy(); }
508
+ function setCanvasMaxSize(size)
509
+ {
510
+ ASSERT(isVector2(size), 'setCanvasMaxSize: size is a vec2, like vec2(1920, 1080)', size);
511
+ canvasMaxSize = size.copy();
512
+ }
500
513
 
501
514
  /** Set minimum aspect ratio of the canvas (width/height), unused if 0
502
515
  * @param {number} aspect
@@ -511,7 +524,11 @@ function setCanvasMaxAspect(aspect) { canvasMaxAspect = aspect; }
511
524
  /** Set fixed size of the canvas
512
525
  * @param {Vector2} size
513
526
  * @memberof Settings */
514
- function setCanvasFixedSize(size) { canvasFixedSize = size.copy(); }
527
+ function setCanvasFixedSize(size)
528
+ {
529
+ ASSERT(isVector2(size), 'setCanvasFixedSize: size is a vec2, like vec2(1280, 720)', size);
530
+ canvasFixedSize = size.copy();
531
+ }
515
532
 
516
533
  /** Use nearest scaling algorithm for canvas for more pixelated look
517
534
  * @param {boolean} pixelated
@@ -553,6 +570,7 @@ function setCanvasPixelRatio(pixelRatio) { canvasPixelRatio = pixelRatio; }
553
570
  function getCanvasPixelRatio() { return canvasPixelRatio ?? (devicePixelRatio || 1); }
554
571
 
555
572
  /** Set default font used for text rendering
573
+ * - A family name with spaces or digits goes in quotes, as "'Press Start 2P'", or the canvas does not take it
556
574
  * @param {string} font
557
575
  * @memberof Settings */
558
576
  function setFontDefault(font) { fontDefault = font; }
@@ -641,7 +659,11 @@ function setGLCircleSides(sides)
641
659
  /** Set default size of tiles in pixels
642
660
  * @param {Vector2} size
643
661
  * @memberof Settings */
644
- function setTileDefaultSize(size) { tileDefaultSize = size.copy(); }
662
+ function setTileDefaultSize(size)
663
+ {
664
+ ASSERT(isVector2(size), 'setTileDefaultSize: size is a vec2, like vec2(16)', size);
665
+ tileDefaultSize = size.copy();
666
+ }
645
667
 
646
668
  /** Default padding pixels around tiles
647
669
  * @param {number} padding
@@ -666,12 +688,23 @@ function setObjectDefaultMass(mass) { objectDefaultMass = mass; }
666
688
  /** Set the fraction of velocity objects keep each frame, 1 keeps all of it, 0 stops at once
667
689
  * @param {number} damp
668
690
  * @memberof Settings */
669
- function setObjectDefaultDamping(damp) { objectDefaultDamping = damp; }
691
+ function setObjectDefaultDamping(damp)
692
+ {
693
+ ASSERT(damp >= 0 && damp <= 1,
694
+ 'setObjectDefaultDamping: damping is 0 to 1, the fraction of velocity kept each frame', damp);
695
+ objectDefaultDamping = damp;
696
+ }
670
697
 
671
698
  /** Set the fraction of angular velocity objects keep each frame, 1 keeps all of it, 0 stops at once
672
699
  * @param {number} damp
673
700
  * @memberof Settings */
674
- function setObjectDefaultAngleDamping(damp) { objectDefaultAngleDamping = damp; }
701
+ function setObjectDefaultAngleDamping(damp)
702
+ {
703
+ ASSERT(damp >= 0 && damp <= 1,
704
+ 'setObjectDefaultAngleDamping: angle damping is 0 to 1, the fraction of angular velocity kept each frame',
705
+ damp);
706
+ objectDefaultAngleDamping = damp;
707
+ }
675
708
 
676
709
  /** Set how much to bounce when a collision occurs
677
710
  * @param {number} restitution
@@ -691,7 +724,11 @@ function setObjectMaxSpeed(speed) { objectMaxSpeed = speed; }
691
724
  /** Set how much gravity to apply to objects
692
725
  * @param {Vector2} newGravity
693
726
  * @memberof Settings */
694
- function setGravity(newGravity) { gravity = newGravity.copy(); }
727
+ function setGravity(newGravity)
728
+ {
729
+ ASSERT(isVector2(newGravity), 'setGravity: newGravity is a vec2, like vec2(0, -.01)', newGravity);
730
+ gravity = newGravity.copy();
731
+ }
695
732
 
696
733
  /** Set the scale for the emit rate of particles, 0 disables particle emitters
697
734
  * @param {number} scale
@@ -821,7 +858,8 @@ function setTouchGamepadVibration(ms) { touchGamepadVibration = ms; }
821
858
  * @memberof Settings */
822
859
  function setVibrateEnable(enable) { vibrateEnable = enable; }
823
860
 
824
- /** Set if audio is enabled, false turns all sound off
861
+ /** Set if audio is enabled, false turns all sound off; a setting for before sounds are made, since a sound made
862
+ * while it is off has nothing to play and one already playing goes on; to mute while the game runs, setSoundVolume(0)
825
863
  * @param {boolean} enable
826
864
  * @memberof Settings */
827
865
  function setSoundEnable(enable) { soundEnable = enable; }
@@ -840,6 +878,7 @@ function setSoundIgnoreSilentSwitch(ignore)
840
878
  * @memberof Settings */
841
879
  function setSoundVolume(volume)
842
880
  {
881
+ ASSERT(isNumber(volume) && abs(volume) < Infinity, 'setSoundVolume: volume must be a finite number, 1 is full', volume);
843
882
  soundVolume = volume;
844
883
  audioUpdateVolume(); // update gain immediately, sound off or not
845
884
  }
@@ -7,7 +7,7 @@
7
7
  * - TileLayer for rendering, TileCollisionLayer for physics
8
8
  * - Collision callbacks for tile interactions with objects
9
9
  * - Optimized raycast support for tile-based physics
10
- * - Integration with Box2D physics via Box2DTileLayer plugin
10
+ * - Integration with Box2D physics via the Box2dTileLayer plugin
11
11
  * @namespace TileLayers
12
12
  */
13
13
 
@@ -16,6 +16,28 @@
16
16
  ///////////////////////////////////////////////////////////////////////////////
17
17
  // Tile Layer System
18
18
 
19
+ /** A Tiled map as Tiled saves it as JSON, what tileLayersLoad and objectLayersLoad take and tileLayersFromLDtk
20
+ * makes; the fields read are listed, and the rest of the file is kept as it is
21
+ * @typedef {Object} TiledMap
22
+ * @property {number} width - Cells across
23
+ * @property {number} height - Cells up
24
+ * @property {number} [tilewidth] - A tile's width in pixels
25
+ * @property {number} [tileheight] - A tile's height in pixels
26
+ * @property {Array<Object>} layers - Tile layers, object layers and groups of them, bottom first
27
+ * @property {Array<Object>} [tilesets] - The first one's margin, spacing and columns are read
28
+ * @property {number} [nextlayerid]
29
+ * @property {number} [nextobjectid]
30
+ * @property {string} [orientation]
31
+ * @property {string} [renderorder]
32
+ * @property {boolean} [infinite]
33
+ * @property {string} [type]
34
+ * @property {string|number} [version] - A number in files from Tiled before 1.6
35
+ * @property {string} [tiledversion]
36
+ * @property {number} [compressionlevel]
37
+ * @property {string} [backgroundcolor]
38
+ * @property {Array<Object>} [properties]
39
+ * @memberof TileLayers */
40
+
19
41
  /** Keep track of all tile layers with collision
20
42
  * @type {Array<TileCollisionLayer>}
21
43
  * @memberof TileLayers */
@@ -90,7 +112,7 @@ function tileCollisionTest(pos, size=vec2(), callbackObject, solidOnly=true)
90
112
  * @param {Vector2} posEnd
91
113
  * @param {EngineObject|TileCollisionCallback} [callbackObject] - Callback, engine object, or undefined
92
114
  * @param {Vector2} [normal] - Optional normal of the surface hit
93
- * @param {boolean} [solidOnly=true] - Only check solid layers?
115
+ * @param {boolean} [solidOnly] - Only check solid layers?
94
116
  * @return {Vector2|undefined} - where the ray meets the first tile hit, nudged just inside it, or undefined if no hit
95
117
  * @memberof TileLayers */
96
118
  function tileCollisionRaycast(posStart, posEnd, callbackObject, normal, solidOnly=true)
@@ -151,7 +173,7 @@ function tileLayerTileInfo(t, tile)
151
173
  * - An object or image layer keeps its index, with its slot in the returned array left empty
152
174
  * - A hidden layer (visible false) is loaded, its collision included, but not drawn; its render
153
175
  * is a no-op, delete that and call redraw() to show it
154
- * @param {Object} tileMapData - Level data from exported data
176
+ * @param {TiledMap} [tileMapData] - Level data from exported data, a 50 by 50 empty level when left out
155
177
  * @param {TileInfo} [tileInfo] - Default tile info (used for size and texture), tile() by default, none when no image is loaded
156
178
  * @param {number} [renderOrder] - Render order of the top layer
157
179
  * @param {number|string} [collisionLayer] - Layer to use for collision if any, by its index or its name
@@ -165,10 +187,7 @@ function tileLayersLoad(tileMapData, tileInfo=tileLayerDefaultTile(), renderOrde
165
187
  {
166
188
  // default level data if loading failed
167
189
  const s = 50;
168
- tileMapData = {};
169
- tileMapData.height = tileMapData.width = s;
170
- tileMapData.layers = [{}];
171
- tileMapData.layers[0].data = new Array(s*s).fill(0);
190
+ tileMapData = {width: s, height: s, layers: [{data: new Array(s*s).fill(0)}]};
172
191
  }
173
192
 
174
193
  // the editor, in debug builds, keeps the map as the source of its edits and brings back autosaved ones
@@ -310,7 +329,7 @@ const tileLayersLDtkTypes = {Int: 'int', Float: 'float', Bool: 'bool', String: '
310
329
  * - The level editor edits the map this returns, and saves it as a Tiled map
311
330
  * @param {Object} ldtk - The LDtk project, its JSON
312
331
  * @param {number|string} [level] - Which level, by its index or its identifier
313
- * @return {Object} - A Tiled map: width, height, tilewidth, tileheight, tilesets and layers
332
+ * @return {TiledMap} - A Tiled map: width, height, tilewidth, tileheight, tilesets and layers
314
333
  * @example
315
334
  * const map = tileLayersFromLDtk(await fetchJSON('world.ldtk'), 'Level_0');
316
335
  * const layers = tileLayersLoad(map, tile(0, 16), 0, 'Collisions'); // its IntGrid layer is solid
@@ -441,7 +460,7 @@ const objectLayersTypes = new Map;
441
460
  * - Adding a name again replaces it
442
461
  * @param {string} name - The type the objects have in Tiled
443
462
  * @param {Function} make - A class made at each object's position, or a function called with it
444
- * @param {Object} [defaults] - Properties set on each one made, the level editor shows inputs for them
463
+ * @param {Object<string, any>} [defaults] - Properties set on each one made, the level editor shows inputs for them
445
464
  * @param {TileInfo} [tileInfo] - An icon for the level editor
446
465
  * @memberof TileLayers
447
466
  * @example
@@ -462,7 +481,7 @@ function objectLayersAddType(name, make, defaults={}, tileInfo)
462
481
  * - The object's properties in Tiled are set over the type's defaults: numbers, booleans, strings, and colors,
463
482
  * and for a Vector2 default the string x,y
464
483
  * - An object whose type was not added is skipped, with a warning in debug builds
465
- * @param {Object} tileMapData - The same Tiled map given to tileLayersLoad
484
+ * @param {TiledMap} tileMapData - The same Tiled map given to tileLayersLoad
466
485
  * @return {Array<any>} - What each object's type made, a function that made nothing is left out
467
486
  * @memberof TileLayers */
468
487
  function objectLayersLoad(tileMapData)
@@ -556,7 +575,7 @@ class TileLayerData
556
575
  /** Create a tile layer data object, one for each tile in a TileLayer
557
576
  * @param {number} [tile] - The tile to use, from 0 like tile(); undefined is an empty cell that draws nothing
558
577
  * @param {number} [direction] - Integer direction of tile, in 90 degree increments
559
- * @param {boolean} [mirror] - If the tile should be mirrored along the x axis
578
+ * @param {boolean} [mirror] - If the tile is flipped left to right
560
579
  * @param {Color} [color] - Color of the tile */
561
580
  constructor(tile, direction=0, mirror=false, color=new Color)
562
581
  {
@@ -565,7 +584,7 @@ class TileLayerData
565
584
  this.tile = tile;
566
585
  /** @property {number} - Integer direction of tile, in 90 degree increments */
567
586
  this.direction = direction;
568
- /** @property {boolean} - If the tile should be mirrored along the x axis */
587
+ /** @property {boolean} - If the tile is flipped left to right */
569
588
  this.mirror = mirror;
570
589
  /** @property {Color} - Color of the tile */
571
590
  this.color = color.copy();
@@ -641,7 +660,7 @@ class CanvasLayer extends EngineObject
641
660
  * @param {Vector2} [size] - Size in world space
642
661
  * @param {Color} [color] - Color to modulate with
643
662
  * @param {number} [angle] - Angle to rotate by
644
- * @param {boolean} [mirror] - If true image is flipped along the Y axis
663
+ * @param {boolean} [mirror] - If true the image is flipped left to right
645
664
  * @param {Color} [additiveColor] - Additive color to be applied if any
646
665
  * @param {boolean} [screenSpace=drawScreenSpace] - If true the pos and size are in screen space
647
666
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas 2D context to draw to */
@@ -825,7 +844,7 @@ class TileLayer extends CanvasLayer
825
844
  redrawStart(clear=false)
826
845
  {
827
846
  if (!this.context) return;
828
- ASSERT(drawContext !== this.context);
847
+ ASSERT(drawContext !== this.context, 'redrawStart: already started, call redrawEnd() first');
829
848
  clear || this.redrawIfSwitched(); // a partial redraw goes on top of the whole layer on the side in use
830
849
 
831
850
  // save current render settings
@@ -870,7 +889,7 @@ class TileLayer extends CanvasLayer
870
889
  redrawEnd()
871
890
  {
872
891
  if (!this.context) return;
873
- ASSERT(drawContext === this.context);
892
+ ASSERT(drawContext === this.context, 'redrawEnd: call redrawStart() first');
874
893
 
875
894
  // set stuff back to normal, the camera first, so a target that was drawing before gets its own transform back
876
895
  [drawContext, mainCanvasSize, cameraPos, cameraScale, cameraAngle, canvasClearColor, glCustomShader] = this.savedRenderSettings;
@@ -931,7 +950,7 @@ class TileLayer extends CanvasLayer
931
950
  * @param {TileInfo} [tileInfo] - Tile info to use, untextured if undefined
932
951
  * @param {Color} [color=WHITE] - Color to modulate with
933
952
  * @param {number} [angle] - Angle to rotate by
934
- * @param {boolean} [mirror] - Is image flipped along the Y axis?
953
+ * @param {boolean} [mirror] - Is the image flipped left to right?
935
954
  * @param {Color} [additiveColor] - Additive color to be applied if any */
936
955
  drawLayerTile(pos, size=vec2(1), tileInfo, color=WHITE,
937
956
  angle=0, mirror, additiveColor)
@@ -1066,9 +1085,9 @@ class TileCollisionLayer extends TileLayer
1066
1085
  for (let y = y0; y < y1; ++y)
1067
1086
  for (let x = x0; x < x1; ++x)
1068
1087
  {
1069
- if (!this.collisionData[y*size.x + x]) continue;
1088
+ if (!(this.collisionData[y*size.x + x] > 0)) continue; // solid is positive, a negative is a marker
1070
1089
  let end = x + 1; // a run of solid cells along the row
1071
- while (end < x1 && this.collisionData[y*size.x + end]) ++end;
1090
+ while (end < x1 && this.collisionData[y*size.x + end] > 0) ++end;
1072
1091
  const count = end - x;
1073
1092
  const tileInfo = new TileInfo(vec2(x*cellPixels.x, textureHeight - (y+1)*cellPixels.y),
1074
1093
  vec2(count*cellPixels.x, cellPixels.y), this.textureInfo, 0, 0);
@@ -1198,7 +1217,14 @@ class TileCollisionLayer extends TileLayer
1198
1217
  const hitPos = lineTest(t0 ? a.add(d.scale(t0)) : a, t1 < 1 ? a.add(d.scale(t1)) : posEnd.subtract(offset),
1199
1218
  testFunction, normal);
1200
1219
  if (hitPos)
1220
+ {
1221
+ // into the world, kept inside its tile there too, as adding the layer's place can round it onto the edge
1222
+ const cell = hitPos.floor();
1201
1223
  hitPos.x += offset.x, hitPos.y += offset.y;
1224
+ const inside = (v, low)=> v < low ? low : v < low + 1 ? v : low + 1 - max(1e-9, abs(low) * 1e-15);
1225
+ hitPos.x = inside(hitPos.x, cell.x + offset.x);
1226
+ hitPos.y = inside(hitPos.y, cell.y + offset.y);
1227
+ }
1202
1228
  if (debugRaycast && hitPos)
1203
1229
  {
1204
1230
  const tilePos = hitPos.floor().add(vec2(.5));
@@ -29,7 +29,7 @@ class Timer
29
29
  * @param {boolean} [useRealTime] - Should the timer keep running even when the game is paused? (useful for UI) */
30
30
  constructor(timeLeft, useRealTime=false)
31
31
  {
32
- ASSERT(timeLeft === undefined || isNumber(timeLeft), 'Constructed Timer is invalid.', timeLeft);
32
+ ASSERT(timeLeft === undefined || isNumber(timeLeft), 'Timer: time must be a number of seconds', timeLeft);
33
33
  this.useRealTime = useRealTime;
34
34
  const globalTime = this.getGlobalTime();
35
35
  /** @type {number|undefined} */
@@ -42,7 +42,7 @@ class Timer
42
42
  * @param {number} [timeLeft] - How much time left before the timer is elapsed in seconds */
43
43
  set(timeLeft=0)
44
44
  {
45
- ASSERT(isNumber(timeLeft), 'Timer is invalid.', timeLeft);
45
+ ASSERT(isNumber(timeLeft), 'Timer.set: time must be a number of seconds', timeLeft);
46
46
  const globalTime = this.getGlobalTime();
47
47
  this.time = globalTime + timeLeft;
48
48
  this.setTime = timeLeft;
@@ -117,18 +117,44 @@ function formatTime(t)
117
117
 
118
118
  /** Fetches a JSON file from a URL and returns the parsed JSON object. Must be used with await!
119
119
  * @param {string} url - URL of JSON file
120
- * @return {Promise<object>}
120
+ * @return {Promise<any>} - The parsed JSON, any shape, as response.json() gives it
121
121
  * @memberof Utilities */
122
122
  async function fetchJSON(url)
123
123
  {
124
- const response = await fetch(url);
124
+ const response = await loadFetch(url, 'fetchJSON');
125
125
  if (!response.ok)
126
- throw new Error(`Failed to fetch JSON from ${url}: ${response.status} ${response.statusText}`);
127
- const json = await response.json();
126
+ throw new Error(`fetchJSON: could not load ${url}, HTTP ${response.status} ${response.statusText}`);
127
+ const text = await response.text();
128
+ let json;
129
+ try { json = JSON.parse(text); }
130
+ catch (e)
131
+ {
132
+ // a dev server answers a mistyped path with its own page, which is no JSON, so the error says which file
133
+ throw new Error(`fetchJSON: ${url} is not JSON` + (loadIsWebPage(text) ?
134
+ ', it is a web page, so the path may be wrong' : ': ' + e.message));
135
+ }
128
136
  editorJSONFetched(url, json); // debug builds remember the file a level came from, to save it by that name
129
137
  return json;
130
138
  }
131
139
 
140
+ // is a file's text a web page, as a dev server sends for a path it does not have
141
+ const loadIsWebPage = (text)=> /^\s*</.test(text);
142
+
143
+ // fetch a file for a loader, a failure to reach it named with the file and who asked, and with the usual cause when
144
+ // the page was opened from a file on disk, where the browser lets it load nothing
145
+ async function loadFetch(url, who)
146
+ {
147
+ try { return await fetch(url); }
148
+ catch (error)
149
+ {
150
+ const fromDisk = globalThis.location?.protocol === 'file:';
151
+ const failed = new Error(`${who}: could not load ${url}, ${error.message}` + (fromDisk ?
152
+ ', the page was opened from a file, serve it from a local web server' : ''));
153
+ /** @type {any} */ (failed).cause = error; // what the browser said, as the error option newer libraries know
154
+ throw failed;
155
+ }
156
+ }
157
+
132
158
  ///////////////////////////////////////////////////////////////////////////////
133
159
 
134
160
  /** Save a text file to disk
@@ -238,7 +264,9 @@ function readSaveData(saveName, defaultSaveData)
238
264
  if (data)
239
265
  {
240
266
  try { loadedData = JSON.parse(data); }
241
- catch { LOG('readSaveData: corrupt JSON for', saveName, '— using defaults'); }
267
+ catch { console.warn('readSaveData: the save ' + saveName + ' can not be read, using the defaults'); }
268
+ if (!loadedData || typeof loadedData !== 'object' || isArray(loadedData))
269
+ loadedData = {}; // only an object of saved values, never a string spread into its letters
242
270
  }
243
271
  }
244
272
  catch { LOG('readSaveData: localStorage unavailable — using defaults'); }
@@ -255,9 +283,26 @@ function writeSaveData(saveName, saveData)
255
283
  ASSERT(isStringLike(saveName), 'writeSaveData requires saveName string');
256
284
  ASSERT(typeof saveData === 'object' && saveData !== null && !isArray(saveData),
257
285
  'writeSaveData: save data must be an object, readSaveData reads it back into one');
258
- // tolerate localStorage being unavailable or quota exceeded
259
- try { localStorage.setItem(saveName, JSON.stringify(saveData)); return true; }
260
- catch { LOG('writeSaveData: failed to write', saveName); return false; }
286
+ // tolerate localStorage being unavailable or quota exceeded; in release too, a player's save going nowhere is
287
+ // not silent, and the warning says which it was
288
+ let text;
289
+ try { text = JSON.stringify(saveData); }
290
+ catch (error)
291
+ {
292
+ console.warn('writeSaveData: the save ' + saveName + ' can not be written as JSON, ' + error.message);
293
+ return false;
294
+ }
295
+ if (text === undefined) // a toJSON that returns nothing, which would be stored as the text undefined
296
+ {
297
+ console.warn('writeSaveData: the save ' + saveName + ' is nothing as JSON, it is not written');
298
+ return false;
299
+ }
300
+ try { localStorage.setItem(saveName, text); return true; }
301
+ catch
302
+ {
303
+ console.warn('writeSaveData: the save ' + saveName + ' could not be written, storage is full or off');
304
+ return false;
305
+ }
261
306
  }
262
307
 
263
308
  ///////////////////////////////////////////////////////////////////////////////
@@ -285,7 +330,7 @@ function noise1D(x)
285
330
  return lerp(noiseHash(i), noiseHash(i + 1), smoothStep(x - i));
286
331
  }
287
332
 
288
- /** 2D value noise — returns a smooth value in [0, 1] for any real (x, y).
333
+ /** 2D value noise — returns a smooth value in [0, 1] for any real (x, y)
289
334
  * @param {number} x
290
335
  * @param {number} y
291
336
  * @return {number}
@@ -45,12 +45,13 @@ function glClampTextureSize(size) { return glMaxTextureSize ? min(size, glMaxTex
45
45
  let glAntialias = true;
46
46
 
47
47
  // WebGL internal variables not exposed to documentation
48
- let glMipmappedTextures = new WeakSet, glMipmapsUntilTarget = new WeakSet, glMipmapsStale = new Set, glPremultipliedTextures = new WeakSet, glShaderPremultiplied, glEnableBeforeLoss = true, glShader, glPolyShader, glPolyMode, glAdditive, glBatchAdditive, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData, glBatchCount, glTextureInfos = new Set, glInstancedVAO, glPolyVAO, glFramebuffer, glRenderTarget, glShaderObjects = [], glCustomShader, glBatchShader, glProgramCustom, glTransform, glRenderTargetSaved, glUniformLocations = new WeakMap, glCanBeEnabled = true;
48
+ let glMipmappedTextures = new WeakSet, glMipmapsUntilTarget = new WeakSet, glMipmapsStale = new Set, glPremultipliedTextures = new WeakSet, glShaderPremultiplied, glEnableBeforeLoss = true, glShader, glPolyShader, glPolyMode, glAdditive, glBatchAdditive, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData, glBatchCount, glTextureInfos = new Set, glInstancedVAO, glPolyVAO, glFramebuffer, glRenderTarget, glShaderObjects = [], glCustomShader, glBatchShader, glProgramCustom, glTransform, glRenderTargetSaved, glUniformLocations = new WeakMap, glCanBeEnabled = true, glDeviceFailed = false;
49
+ let glFailedPrograms = new WeakSet; // programs that did not build in a release build, which nothing draws with
49
50
  // ANDed onto every packed color as a draw is queued; the light system's shadow pass sets 0xff000000
50
51
  // to draw everything black with its alpha kept (rgbaInt packs alpha in the top byte)
51
52
  let glColorMask = -1;
52
53
  // ORed onto the additive color of every quad and onto every poly point's color as a draw is queued; the light system's
53
- // emissive pass sets a grey with the mask at 0xff000000, so a draw comes out that grey in its own shape
54
+ // emissive pass sets a gray with the mask at 0xff000000, so a draw comes out that gray in its own shape
54
55
  let glColorAdditive = 0;
55
56
  // a texture drawn into with the canvas's own transform and its size, the light system's lightmap while its pass
56
57
  // runs: a target set and ended inside it, like a tile layer redrawn in a renderLight, goes back to it, not the canvas
@@ -119,6 +120,7 @@ function glInit(rootElement)
119
120
  glCanvas = glContext = undefined;
120
121
  glEnable = false;
121
122
  glCanBeEnabled = false;
123
+ glDeviceFailed = true; // the device, not the game, turned it off
122
124
  return;
123
125
  }
124
126
 
@@ -218,6 +220,15 @@ function glInit(rootElement)
218
220
  '}' // end of shader
219
221
  );
220
222
 
223
+ // a device that can not build the engine's own programs draws with Canvas2D, as after a lost context
224
+ if (glFailedPrograms.has(glShader) || glFailedPrograms.has(glPolyShader))
225
+ {
226
+ console.error('LittleJS: WebGL can not draw on this device, using Canvas2D');
227
+ glEnable = glCanBeEnabled = false;
228
+ glDeviceFailed = true;
229
+ glCanvas.style.display = 'none';
230
+ }
231
+
221
232
  // init buffers
222
233
  const glInstanceData = new ArrayBuffer(gl_ARRAY_BUFFER_SIZE);
223
234
  glPositionData = new Float32Array(glInstanceData);
@@ -464,13 +475,23 @@ function glCreateProgram(vsSource, fsSource)
464
475
 
465
476
  // build the program
466
477
  const program = glContext.createProgram();
467
- glContext.attachShader(program, glCompileShader(vsSource, glContext.VERTEX_SHADER));
468
- glContext.attachShader(program, glCompileShader(fsSource, glContext.FRAGMENT_SHADER));
478
+ const vertexShader = glCompileShader(vsSource, glContext.VERTEX_SHADER);
479
+ const fragmentShader = glCompileShader(fsSource, glContext.FRAGMENT_SHADER);
480
+ glContext.attachShader(program, vertexShader);
481
+ glContext.attachShader(program, fragmentShader);
469
482
  glContext.linkProgram(program);
470
483
 
471
- // check for errors
472
- if (debug && !glContext.getProgramParameter(program, glContext.LINK_STATUS))
473
- throw glContext.getProgramInfoLog(program);
484
+ // check for errors in every build, as a driver may refuse what another takes: a debug build throws, a release
485
+ // one says why once and marks the program, which the engine's draws then do without
486
+ if (!glContext.getProgramParameter(program, glContext.LINK_STATUS))
487
+ {
488
+ const log = glContext.getProgramInfoLog(program) + '\n' +
489
+ glContext.getShaderInfoLog(vertexShader) + glContext.getShaderInfoLog(fragmentShader);
490
+ if (debug)
491
+ throw log;
492
+ console.error('LittleJS: a shader failed to build\n' + log);
493
+ glFailedPrograms.add(program);
494
+ }
474
495
  return program;
475
496
  }
476
497
 
@@ -486,17 +507,39 @@ function glUniformLocation(program, name)
486
507
  // color, then the sprite's color and additive color apply as the engine's own fragment shader does
487
508
  function glShaderProgram(shader)
488
509
  {
489
- return shader.program ||= glCreateProgram(gl_VERTEX_SOURCE,
510
+ shader.program || glShaderTrack(shader);
511
+ const program = shader.program ||= glCreateProgram(gl_VERTEX_SOURCE,
490
512
  '#version 300 es\n' +
491
513
  'precision highp float;' +
492
514
  'uniform sampler2D iChannel0;' + // the texture
493
515
  'uniform vec3 iResolution;' + // canvas size in pixels
494
516
  'uniform float iTime;' + // engine time
495
517
  'uniform bool premultipliedTexture;' + // is the texture premultiplied, a render target or a smooth image
496
- 'in vec2 v,l;in vec4 d,e;out vec4 c;\n' + // a define needs its own line
518
+ 'in vec2 v,l;in vec4 d,e;out vec4 c;' +
519
+ // main first, the snippet's mainImage declared for it, so a define in the snippet can not reach main's names
520
+ 'void mainImage(out vec4,vec2);' +
521
+ 'void main(){vec4 t;mainImage(t,v);' + gl_FRAGMENT_TINT_SOURCE + '}\n' + // a define needs its own line
497
522
  '#define localUV l\n' +
498
- shader.fragmentCode + '\n' +
499
- 'void main(){vec4 t;mainImage(t,v);' + gl_FRAGMENT_TINT_SOURCE + '}');
523
+ shader.fragmentCode + '\n');
524
+ return glFailedPrograms.has(program) ? glShader : program;
525
+ }
526
+
527
+ // a Shader compiled again after dispose is kept for a lost context again
528
+ function glShaderTrack(shader) { glShaderObjects.includes(shader) || glShaderObjects.push(shader); }
529
+
530
+ // let go of a Shader's programs, 2D and 3D, and of its place in the list; a batch drawing with it is drawn first
531
+ function glShaderDispose(shader)
532
+ {
533
+ const i = glShaderObjects.indexOf(shader);
534
+ i < 0 || glShaderObjects.splice(i, 1);
535
+ if (glContext && !headlessMode)
536
+ {
537
+ if (glBatchShader === shader) // a batch drawing with it is drawn first, the batch holds the Shader
538
+ glFlush();
539
+ for (const program of [shader.program, shader.program3D])
540
+ program && glContext.deleteProgram(program);
541
+ }
542
+ shader.program = shader.program3D = undefined;
500
543
  }
501
544
 
502
545
  /** Create WebGL texture from an image and init the texture settings
@@ -554,8 +597,16 @@ function glCreateTexture(image, wrap=false, pixelated=tilesPixelated)
554
597
  * @memberof WebGL */
555
598
  function glDeleteTexture(texture)
556
599
  {
557
- if (!glContext) return;
558
-
600
+ if (!glContext || !texture) return;
601
+
602
+ // what is batched with it is drawn first, since deleting unbinds it, and nothing keeps it after
603
+ if (texture === glActiveTexture)
604
+ {
605
+ glFlush();
606
+ glActiveTexture = undefined; // so nothing binds the deleted texture again
607
+ }
608
+ glMipmapsStale.delete(texture);
609
+ glPremultipliedTextures.delete(texture);
559
610
  glContext.deleteTexture(texture);
560
611
  }
561
612
 
@@ -631,13 +682,6 @@ function glUnregisterTextureInfo(textureInfo)
631
682
  // unset and destroy the texture, drawing what is batched with it first, since deleting unbinds it
632
683
  const glTexture = textureInfo.glTexture;
633
684
  textureInfo.glTexture = undefined;
634
- if (glTexture && glTexture === glActiveTexture)
635
- {
636
- glFlush();
637
- glActiveTexture = undefined; // so nothing binds the deleted texture again
638
- }
639
- glMipmapsStale.delete(glTexture);
640
- glPremultipliedTextures.delete(glTexture);
641
685
  glDeleteTexture(glTexture);
642
686
  }
643
687
 
@@ -673,6 +717,8 @@ function glFlush()
673
717
  const height = glRenderTarget ? mainCanvasSize.y : glCanvas.height;
674
718
  glContext.uniform3f(uniform('iResolution'), width, height, 1);
675
719
  glContext.uniform1i(uniform('premultipliedTexture'), +premultiplied);
720
+ if (program === glShader)
721
+ glShaderPremultiplied = premultiplied; // a Shader that did not build draws with the engine's
676
722
  }
677
723
  }
678
724
  if (!glPolyMode && !glBatchShader && glShaderPremultiplied !== premultiplied)
@@ -730,8 +776,8 @@ function glSetAntialias(antialias=true)
730
776
  * @param {number} [uv0Y]
731
777
  * @param {number} [uv1X]
732
778
  * @param {number} [uv1Y]
733
- * @param {number} [rgba=-1] - white is -1
734
- * @param {number} [rgbaAdditive=0] - black is 0
779
+ * @param {number} [rgba] - white is -1
780
+ * @param {number} [rgbaAdditive] - black is 0
735
781
  * @memberof WebGL */
736
782
  function glDraw(x, y, sizeX, sizeY, angle=0, uv0X=0, uv0Y=0, uv1X=1, uv1Y=1, rgba=-1, rgbaAdditive=0)
737
783
  {