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.
- package/CHANGELOG.md +123 -0
- package/EDITOR.md +2 -2
- package/FAQ.md +55 -11
- package/README.md +6 -2
- package/REFERENCE.md +203 -57
- package/dist/littlejs.d.ts +933 -313
- package/dist/littlejs.esm.js +5143 -2419
- package/dist/littlejs.esm.min.js +38 -24
- package/dist/littlejs.js +5132 -2416
- package/dist/littlejs.min.js +37 -23
- package/dist/littlejs.release.js +4905 -2232
- package/package.json +4 -4
- package/plugins/box2d.js +141 -56
- package/plugins/drawUtilities.js +374 -380
- package/plugins/gltf.js +383 -60
- package/plugins/lightSystem.js +412 -30
- package/plugins/math3d.js +1015 -1014
- package/plugins/medalSystem.js +19 -7
- package/plugins/newgrounds.js +5 -6
- package/plugins/parallax.js +1 -0
- package/plugins/particleEffects.js +47 -14
- package/plugins/pathFinder.js +60 -38
- package/plugins/pluginExport.js +11 -0
- package/plugins/postProcess.js +200 -67
- package/plugins/render3d.js +493 -2391
- package/plugins/render3dDebug.js +5 -8
- package/plugins/render3dEditor.js +84 -9
- package/plugins/render3dExtras.js +122 -33
- package/plugins/render3dLevel.js +67 -8
- package/plugins/render3dMesh.js +1158 -0
- package/plugins/render3dObject.js +1451 -0
- package/plugins/render3dVoxels.js +15 -3
- package/plugins/textureSheet.js +9 -8
- package/plugins/threejs.js +1 -1
- package/plugins/tweenSystem.js +117 -87
- package/plugins/uiSystem.js +62 -28
- package/plugins/wavedash.js +229 -0
- package/src/engine.js +192 -40
- package/src/engineAudio.js +47 -27
- package/src/engineBuild.mjs +6 -1
- package/src/engineDebug.js +23 -162
- package/src/engineDraw.js +148 -43
- package/src/engineEditor.js +153 -40
- package/src/engineExport.js +0 -3
- package/src/engineInput.js +31 -25
- package/src/engineMath.js +38 -20
- package/src/engineObject.js +68 -50
- package/src/engineParticles.js +66 -18
- package/src/engineRelease.js +18 -15
- package/src/engineSettings.js +48 -9
- package/src/engineTileLayer.js +44 -18
- package/src/engineUtilities.js +56 -11
- package/src/engineWebGL.js +68 -22
package/src/engineSettings.js
CHANGED
|
@@ -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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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
|
}
|
package/src/engineTileLayer.js
CHANGED
|
@@ -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
|
|
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
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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));
|
package/src/engineUtilities.js
CHANGED
|
@@ -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), '
|
|
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
|
|
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<
|
|
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
|
|
124
|
+
const response = await loadFetch(url, 'fetchJSON');
|
|
125
125
|
if (!response.ok)
|
|
126
|
-
throw new Error(`
|
|
127
|
-
const
|
|
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 {
|
|
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
|
-
|
|
260
|
-
|
|
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}
|
package/src/engineWebGL.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
468
|
-
|
|
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
|
-
|
|
473
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
734
|
-
* @param {number} [rgbaAdditive
|
|
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
|
{
|