littlejsengine 1.25.0 → 1.26.1
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 +99 -0
- package/FAQ.md +49 -11
- package/README.md +6 -2
- package/REFERENCE.md +129 -21
- package/dist/littlejs.d.ts +487 -102
- package/dist/littlejs.esm.js +2898 -719
- package/dist/littlejs.esm.min.js +3 -3
- package/dist/littlejs.js +2888 -716
- package/dist/littlejs.min.js +2 -2
- package/dist/littlejs.release.js +2702 -527
- package/package.json +4 -4
- package/plugins/box2d.js +110 -34
- package/plugins/gltf.js +74 -22
- package/plugins/lightSystem.js +410 -28
- package/plugins/math3d.js +4 -2
- package/plugins/medalSystem.js +19 -7
- package/plugins/newgrounds.js +4 -2
- package/plugins/parallax.js +1 -0
- package/plugins/particleEffects.js +44 -10
- package/plugins/pathFinder.js +9 -11
- package/plugins/pluginExport.js +9 -0
- package/plugins/postProcess.js +9 -3
- package/plugins/render3d.js +499 -2389
- package/plugins/render3dDebug.js +5 -8
- package/plugins/render3dEditor.js +81 -6
- package/plugins/render3dExtras.js +103 -31
- package/plugins/render3dLevel.js +53 -5
- 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 +76 -51
- package/plugins/uiSystem.js +37 -8
- package/plugins/wavedash.js +229 -0
- package/src/engine.js +199 -40
- package/src/engineAudio.js +37 -21
- package/src/engineBuild.mjs +6 -1
- package/src/engineDebug.js +22 -162
- package/src/engineDraw.js +142 -34
- package/src/engineEditor.js +79 -18
- package/src/engineExport.js +1 -3
- package/src/engineInput.js +34 -24
- package/src/engineMath.js +35 -14
- package/src/engineObject.js +60 -41
- package/src/engineParticles.js +58 -11
- package/src/engineRelease.js +1 -5
- package/src/engineSettings.js +48 -9
- package/src/engineTileLayer.js +11 -4
- package/src/engineUtilities.js +41 -9
- package/src/engineWebGL.js +37 -13
package/src/engineInput.js
CHANGED
|
@@ -198,9 +198,11 @@ function inputClear()
|
|
|
198
198
|
* @memberof Input */
|
|
199
199
|
function keyIsDown(key, device=0)
|
|
200
200
|
{
|
|
201
|
-
ASSERT(isStringLike(key), 'key must be a
|
|
202
|
-
ASSERT(typeof key !== 'string' || key.length > 1,
|
|
203
|
-
|
|
201
|
+
ASSERT(isStringLike(key), 'keyIsDown: key must be a code like \'KeyA\' or a number', key);
|
|
202
|
+
ASSERT(typeof key !== 'string' || key.length > 1,
|
|
203
|
+
"keyIsDown: keys are codes like 'KeyW' or 'Space', not characters", key);
|
|
204
|
+
ASSERT(device > 0 || typeof key !== 'number' || key < 5,
|
|
205
|
+
"keyIsDown: keyboard keys are codes like 'KeyA' or 'Space', the numbers 0 to 4 are mouse buttons", key);
|
|
204
206
|
return !!(inputData[device]?.[key] & 1) && !inputCaptureHides(device);
|
|
205
207
|
}
|
|
206
208
|
|
|
@@ -211,9 +213,11 @@ function keyIsDown(key, device=0)
|
|
|
211
213
|
* @memberof Input */
|
|
212
214
|
function keyWasPressed(key, device=0)
|
|
213
215
|
{
|
|
214
|
-
ASSERT(isStringLike(key), 'key must be a
|
|
215
|
-
ASSERT(typeof key !== 'string' || key.length > 1,
|
|
216
|
-
|
|
216
|
+
ASSERT(isStringLike(key), 'keyWasPressed: key must be a code like \'KeyA\' or a number', key);
|
|
217
|
+
ASSERT(typeof key !== 'string' || key.length > 1,
|
|
218
|
+
"keyWasPressed: keys are codes like 'KeyW' or 'Space', not characters", key);
|
|
219
|
+
ASSERT(device > 0 || typeof key !== 'number' || key < 5,
|
|
220
|
+
"keyWasPressed: keyboard keys are codes like 'KeyA' or 'Space', the numbers 0 to 4 are mouse buttons", key);
|
|
217
221
|
return !!(inputData[device]?.[key] & 2) && !inputCaptureHides(device);
|
|
218
222
|
}
|
|
219
223
|
|
|
@@ -224,9 +228,11 @@ function keyWasPressed(key, device=0)
|
|
|
224
228
|
* @memberof Input */
|
|
225
229
|
function keyWasReleased(key, device=0)
|
|
226
230
|
{
|
|
227
|
-
ASSERT(isStringLike(key), 'key must be a
|
|
228
|
-
ASSERT(typeof key !== 'string' || key.length > 1,
|
|
229
|
-
|
|
231
|
+
ASSERT(isStringLike(key), 'keyWasReleased: key must be a code like \'KeyA\' or a number', key);
|
|
232
|
+
ASSERT(typeof key !== 'string' || key.length > 1,
|
|
233
|
+
"keyWasReleased: keys are codes like 'KeyW' or 'Space', not characters", key);
|
|
234
|
+
ASSERT(device > 0 || typeof key !== 'number' || key < 5,
|
|
235
|
+
"keyWasReleased: keyboard keys are codes like 'KeyA' or 'Space', the numbers 0 to 4 are mouse buttons", key);
|
|
230
236
|
return !!(inputData[device]?.[key] & 4) && !inputCaptureHides(device);
|
|
231
237
|
}
|
|
232
238
|
|
|
@@ -253,7 +259,7 @@ function keyDirection(up='ArrowUp', down='ArrowDown', left='ArrowLeft', right='A
|
|
|
253
259
|
* @memberof Input */
|
|
254
260
|
function mouseIsDown(button)
|
|
255
261
|
{
|
|
256
|
-
ASSERT(isNumber(button), '
|
|
262
|
+
ASSERT(isNumber(button), 'mouseIsDown: button is 0 (left), 1 (middle) or 2 (right)', button);
|
|
257
263
|
return keyIsDown(button);
|
|
258
264
|
}
|
|
259
265
|
|
|
@@ -263,7 +269,7 @@ function mouseIsDown(button)
|
|
|
263
269
|
* @memberof Input */
|
|
264
270
|
function mouseWasPressed(button)
|
|
265
271
|
{
|
|
266
|
-
ASSERT(isNumber(button), '
|
|
272
|
+
ASSERT(isNumber(button), 'mouseWasPressed: button is 0 (left), 1 (middle) or 2 (right)', button);
|
|
267
273
|
return keyWasPressed(button);
|
|
268
274
|
}
|
|
269
275
|
|
|
@@ -273,7 +279,7 @@ function mouseWasPressed(button)
|
|
|
273
279
|
* @memberof Input */
|
|
274
280
|
function mouseWasReleased(button)
|
|
275
281
|
{
|
|
276
|
-
ASSERT(isNumber(button), '
|
|
282
|
+
ASSERT(isNumber(button), 'mouseWasReleased: button is 0 (left), 1 (middle) or 2 (right)', button);
|
|
277
283
|
return keyWasReleased(button);
|
|
278
284
|
}
|
|
279
285
|
|
|
@@ -500,16 +506,19 @@ function inputInit()
|
|
|
500
506
|
// keys typed into an html text field are the player's typing, not game input;
|
|
501
507
|
// a key already down still releases on keyup, which only lets go of keys that are down
|
|
502
508
|
const typing = isTextInput(e.target) || isTextInput(document.activeElement);
|
|
503
|
-
|
|
509
|
+
// a key already held, a keydown the browser did not mark as a repeat, is not pressed again, but with Cmd down
|
|
510
|
+
// it is: a Mac sends no keyup for a key let go while Cmd is held, so each tap of Cmd+Z comes as a keydown alone
|
|
511
|
+
if (!e.repeat && !typing && (!inputKeysHeld.has(e.code) || e.metaKey))
|
|
504
512
|
{
|
|
505
513
|
inputKeysHeld.add(e.code);
|
|
506
|
-
// an arrow its alias already holds down is not pressed again, like its release waits for both
|
|
514
|
+
// an arrow its alias already holds down is not pressed again, like its release waits for both; a release
|
|
515
|
+
// earlier this frame is kept, so a quick release and press reads as both
|
|
507
516
|
if (!(inputWASDEmulateDirection && inputKeysHeld.has(inputArrowToWASD[e.code]) && inputData[0][e.code] & 1))
|
|
508
|
-
inputData[0][e.code] = 3;
|
|
517
|
+
inputData[0][e.code] = 3 | inputData[0][e.code] & 4;
|
|
509
518
|
// an alias presses its arrow's slot too, unless the arrow itself already holds it down
|
|
510
519
|
const remap = remapKey(e.code);
|
|
511
520
|
if (remap !== e.code && !(inputData[0][remap] & 1))
|
|
512
|
-
inputData[0][remap] = 3;
|
|
521
|
+
inputData[0][remap] = 3 | inputData[0][remap] & 4;
|
|
513
522
|
}
|
|
514
523
|
|
|
515
524
|
// try to prevent default browser handling of input
|
|
@@ -587,7 +596,7 @@ function inputInit()
|
|
|
587
596
|
// pointer lock the mouse stays where the lock began, which may be in a bar, and every click is the game's
|
|
588
597
|
if (!pointerLockIsActive() && !inCanvas(e.x, e.y))
|
|
589
598
|
return;
|
|
590
|
-
inputData[0][e.button] = 3;
|
|
599
|
+
inputData[0][e.button] = 3 | inputData[0][e.button] & 4; // a release earlier this frame is kept
|
|
591
600
|
|
|
592
601
|
const mousePosScreenLast = mousePosScreen;
|
|
593
602
|
mousePosScreen = mouseEventToScreen(vec2(e.x,e.y));
|
|
@@ -647,8 +656,9 @@ function inputInit()
|
|
|
647
656
|
}
|
|
648
657
|
function onContextMenu(e)
|
|
649
658
|
{
|
|
650
|
-
// prevent right click menu, but a text field keeps its copy and paste menu
|
|
651
|
-
|
|
659
|
+
// prevent right click menu, but a text field keeps its copy and paste menu and a link its open in new tab
|
|
660
|
+
const target = /** @type {HTMLElement} */ (e.target);
|
|
661
|
+
if (inputPreventDefault && !isTextInput(target) && !target?.closest?.('a[href]'))
|
|
652
662
|
e.preventDefault();
|
|
653
663
|
}
|
|
654
664
|
function onBlur()
|
|
@@ -669,6 +679,10 @@ function inputInit()
|
|
|
669
679
|
// handle all touch events the same way
|
|
670
680
|
function handleTouch(e)
|
|
671
681
|
{
|
|
682
|
+
// fix stalled audio requiring user interaction, with touch input off too since the page's sound needs it
|
|
683
|
+
if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
|
|
684
|
+
audioResume();
|
|
685
|
+
|
|
672
686
|
if (!touchInputEnable)
|
|
673
687
|
{
|
|
674
688
|
// turned off mid touch, the finger that drove the mouse lets go of it
|
|
@@ -679,10 +693,6 @@ function inputInit()
|
|
|
679
693
|
}
|
|
680
694
|
inputLastTouchTime = performance.now();
|
|
681
695
|
|
|
682
|
-
// fix stalled audio requiring user interaction
|
|
683
|
-
if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
|
|
684
|
-
audioResume();
|
|
685
|
-
|
|
686
696
|
// when the touch gamepad is enabled it owns touch input: suppress the
|
|
687
697
|
// touch->mouse passthrough entirely unless touchGamepadPassthrough is set
|
|
688
698
|
// (its own zones drive gameplay via pointer events)
|
|
@@ -741,7 +751,7 @@ function inputInit()
|
|
|
741
751
|
else if (inputWasTouching && touch.identifier === inputTouchIdentifier)
|
|
742
752
|
mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
|
|
743
753
|
else if (pressTouch && inCanvas(pressTouch.clientX, pressTouch.clientY))
|
|
744
|
-
inputData[0][button] = 3; // a tap in the bars
|
|
754
|
+
inputData[0][button] = 3 | inputData[0][button] & 4; // a tap in the bars is not a press
|
|
745
755
|
// the finger left after a pinch moves the mouse from where it is, with no jump and no press
|
|
746
756
|
inputTouchIdentifier = pinching ? undefined : touch.identifier;
|
|
747
757
|
}
|
package/src/engineMath.js
CHANGED
|
@@ -68,7 +68,7 @@ const sign = (x) => Math.sign(x);
|
|
|
68
68
|
* @param {...number} values
|
|
69
69
|
* @return {number}
|
|
70
70
|
* @memberof Math */
|
|
71
|
-
const hypot =
|
|
71
|
+
const hypot = Math.hypot; // not a function of its own, which would gather its arguments into an array each call
|
|
72
72
|
|
|
73
73
|
/** Returns log2 of value passed in
|
|
74
74
|
* @param {number} x
|
|
@@ -366,7 +366,7 @@ function oscillate(frequency=1, amplitude=1, t=time, offset=0, type=0)
|
|
|
366
366
|
* @param {any} n
|
|
367
367
|
* @return {boolean}
|
|
368
368
|
* @memberof Math */
|
|
369
|
-
function isNumber(n) { return typeof n === 'number' &&
|
|
369
|
+
function isNumber(n) { return typeof n === 'number' && n === n; } // NaN is the one number not equal to itself
|
|
370
370
|
|
|
371
371
|
/**
|
|
372
372
|
* Check if a value is stringifiable — i.e. it has a toString that returns
|
|
@@ -687,8 +687,8 @@ function vec2(x=0, y) { return new Vector2(x, y ?? x); }
|
|
|
687
687
|
function isVector2(v) { return v instanceof Vector2 && v.isValid(); }
|
|
688
688
|
|
|
689
689
|
// vector2 asserts
|
|
690
|
-
function ASSERT_VECTOR2_VALID(v) { ASSERT(isVector2(v), '
|
|
691
|
-
function ASSERT_NUMBER_VALID(n) { ASSERT(isNumber(n), '
|
|
690
|
+
function ASSERT_VECTOR2_VALID(v) { ASSERT(isVector2(v), 'expected a vec2', v); }
|
|
691
|
+
function ASSERT_NUMBER_VALID(n) { ASSERT(isNumber(n), 'expected a number', n); }
|
|
692
692
|
|
|
693
693
|
/**
|
|
694
694
|
* 2D Vector object with vector math library
|
|
@@ -711,7 +711,8 @@ class Vector2
|
|
|
711
711
|
this.x = x;
|
|
712
712
|
/** @property {number} - Y axis location */
|
|
713
713
|
this.y = y;
|
|
714
|
-
ASSERT(this.isValid(), '
|
|
714
|
+
ASSERT(this.isValid(), 'vec2: x and y must be numbers (add, subtract, multiply and divide take a vec2, scale a number)',
|
|
715
|
+
this); // one literal, as it is built at every vector made in a debug build
|
|
715
716
|
}
|
|
716
717
|
|
|
717
718
|
/** Sets values of this vector and returns self
|
|
@@ -942,7 +943,21 @@ class Vector2
|
|
|
942
943
|
* @return {Color}
|
|
943
944
|
* @memberof Math
|
|
944
945
|
*/
|
|
945
|
-
function rgb(r, g, b, a)
|
|
946
|
+
function rgb(r, g, b, a)
|
|
947
|
+
{
|
|
948
|
+
debug && (r > 2 || g > 2 || b > 2) && colorRangeWarn('rgb(255, 0, 0) is rgb(1, 0, 0)');
|
|
949
|
+
return new Color(r, g, b, a);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// a color given as 0 to 255 or as percents, as other tools take them, said once in a debug build; a value a little
|
|
953
|
+
// past 1 is a bright color, which is why only past 2 counts
|
|
954
|
+
let colorRangeWarned = false;
|
|
955
|
+
function colorRangeWarn(example)
|
|
956
|
+
{
|
|
957
|
+
if (colorRangeWarned) return;
|
|
958
|
+
colorRangeWarned = true;
|
|
959
|
+
console.warn('color values are 0 to 1 here, not 0 to 255 or percents: ' + example);
|
|
960
|
+
}
|
|
946
961
|
|
|
947
962
|
/**
|
|
948
963
|
* Create a color object with HSLA values, white by default
|
|
@@ -952,7 +967,11 @@ function rgb(r, g, b, a) { return new Color(r, g, b, a); }
|
|
|
952
967
|
* @param {number} [a=1] - alpha
|
|
953
968
|
* @return {Color}
|
|
954
969
|
* @memberof Math */
|
|
955
|
-
function hsl(h, s, l, a)
|
|
970
|
+
function hsl(h, s, l, a)
|
|
971
|
+
{
|
|
972
|
+
debug && (s > 2 || l > 2) && colorRangeWarn('hsl(.5, 100, 50) is hsl(.5, 1, .5)');
|
|
973
|
+
return new Color().setHSLA(h, s, l, a);
|
|
974
|
+
}
|
|
956
975
|
|
|
957
976
|
/**
|
|
958
977
|
* Check if object is a valid Color
|
|
@@ -962,7 +981,7 @@ function hsl(h, s, l, a) { return new Color().setHSLA(h, s, l, a); }
|
|
|
962
981
|
function isColor(c) { return c instanceof Color && c.isValid(); }
|
|
963
982
|
|
|
964
983
|
// color asserts
|
|
965
|
-
function ASSERT_COLOR_VALID(c) { ASSERT(isColor(c), '
|
|
984
|
+
function ASSERT_COLOR_VALID(c) { ASSERT(isColor(c), 'expected a color', c); }
|
|
966
985
|
|
|
967
986
|
/**
|
|
968
987
|
* Color object (red, green, blue, alpha) with some helpful functions
|
|
@@ -991,7 +1010,7 @@ class Color
|
|
|
991
1010
|
this.b = b;
|
|
992
1011
|
/** @property {number} - Alpha */
|
|
993
1012
|
this.a = a;
|
|
994
|
-
ASSERT(this.isValid(), '
|
|
1013
|
+
ASSERT(this.isValid(), 'Color: r, g, b and a must be numbers', this);
|
|
995
1014
|
}
|
|
996
1015
|
|
|
997
1016
|
/** Sets values of this color and returns self
|
|
@@ -1153,10 +1172,12 @@ class Color
|
|
|
1153
1172
|
* @return {string} */
|
|
1154
1173
|
toString(useAlpha = true)
|
|
1155
1174
|
{
|
|
1156
|
-
if (
|
|
1157
|
-
return '#000';
|
|
1158
|
-
|
|
1159
|
-
|
|
1175
|
+
if (!this.isValid())
|
|
1176
|
+
return '#000'; // in release too, as a canvas would keep its last color for #NaN
|
|
1177
|
+
|
|
1178
|
+
// two hex digits of a channel of 0 to 1, kept in here as the script build shares its top level with the game
|
|
1179
|
+
const colorHex = (c)=> ((c = round(clamp(c) * 255)) < 16 ? '0' : '') + c.toString(16);
|
|
1180
|
+
return '#' + colorHex(this.r) + colorHex(this.g) + colorHex(this.b) + (useAlpha ? colorHex(this.a) : '');
|
|
1160
1181
|
}
|
|
1161
1182
|
|
|
1162
1183
|
/** Set this color from a hex code
|
|
@@ -1166,7 +1187,7 @@ class Color
|
|
|
1166
1187
|
{
|
|
1167
1188
|
ASSERT(isStringLike(hex), 'Color hex code must be a string');
|
|
1168
1189
|
ASSERT(hex[0] === '#', 'Color hex code must start with #');
|
|
1169
|
-
ASSERT([4,5,7,9].includes(hex.length), '
|
|
1190
|
+
ASSERT([4,5,7,9].includes(hex.length), 'setHex: use #rgb, #rgba, #rrggbb or #rrggbbaa', hex);
|
|
1170
1191
|
|
|
1171
1192
|
if (hex.length < 6)
|
|
1172
1193
|
{
|
package/src/engineObject.js
CHANGED
|
@@ -80,6 +80,9 @@ class EngineObject
|
|
|
80
80
|
this.shader = undefined;
|
|
81
81
|
/** @property {boolean} - Does this object draw into the light system's shadow map; false for a floor layer, a background, a pickup */
|
|
82
82
|
this.castShadow = true;
|
|
83
|
+
/** @property {boolean} - Does this object draw into the light system's background map, which a
|
|
84
|
+
* DirectionalLight lights only at its edges facing the light; for a background layer, with castShadow false */
|
|
85
|
+
this.castBackgroundShadow = false;
|
|
83
86
|
/** @property {number} - With the light system, how much it lights itself: 0 lit only by the lights, 1 full
|
|
84
87
|
* brightness in its own colors whatever the lights do, between partly; drawn into the lightmap through
|
|
85
88
|
* renderEmissive, as 3D's emissive. Exact for solid pixels; a partly transparent one is self lit by its alpha
|
|
@@ -179,23 +182,18 @@ class EngineObject
|
|
|
179
182
|
child.updateTransforms();
|
|
180
183
|
}
|
|
181
184
|
|
|
182
|
-
/** Update the object physics, called automatically by engine once each frame. Can be overridden to stop or change how physics works for an object
|
|
185
|
+
/** Update the object physics, called automatically by engine once each frame. Can be overridden to stop or change how physics works for an object
|
|
186
|
+
* - With many solids each mover finds those near it through a grid, which follows a solid after its own physics
|
|
187
|
+
* update: a solid an override moves for another object, a lift carrying a crate, is found there from the next
|
|
188
|
+
* update on */
|
|
183
189
|
updatePhysics()
|
|
184
190
|
{
|
|
185
191
|
// child objects do not have physics
|
|
186
|
-
ASSERT(!this.parent);
|
|
192
|
+
ASSERT(!this.parent, 'updatePhysics: a child has no physics of its own, its parent moves it');
|
|
187
193
|
|
|
188
194
|
// bail if a collision callback destroyed us mid-frame
|
|
189
195
|
if (this.destroyed) return;
|
|
190
196
|
|
|
191
|
-
// limit max speed to prevent missing collisions, only for what collides: with solids, or with tiles while it
|
|
192
|
-
// has a mass, which tile collision needs; anything else moves as fast as it is told
|
|
193
|
-
if (this.clampSpeed && enablePhysicsSolver && (this.collideSolidObjects || this.collideLevel && this.mass))
|
|
194
|
-
{
|
|
195
|
-
this.velocity.x = clamp(this.velocity.x, -objectMaxSpeed, objectMaxSpeed);
|
|
196
|
-
this.velocity.y = clamp(this.velocity.y, -objectMaxSpeed, objectMaxSpeed);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
197
|
// physics sanity checks
|
|
200
198
|
ASSERT(this.angleDamping >= 0 && this.angleDamping <= 1, 'angleDamping must be 0 to 1, the fraction kept each frame');
|
|
201
199
|
ASSERT(this.damping >= 0 && this.damping <= 1, 'damping must be 0 to 1, the fraction of velocity kept each frame');
|
|
@@ -211,6 +209,14 @@ class EngineObject
|
|
|
211
209
|
this.velocity.x += gravity.x * this.gravityScale;
|
|
212
210
|
this.velocity.y += gravity.y * this.gravityScale;
|
|
213
211
|
}
|
|
212
|
+
// limit max speed to prevent missing collisions, after gravity so no move is past it, only for what collides:
|
|
213
|
+
// with solids, or with tiles while it has a mass, which tile collision needs; anything else moves as fast as
|
|
214
|
+
// it is told
|
|
215
|
+
if (this.clampSpeed && enablePhysicsSolver && (this.collideSolidObjects || this.collideLevel && this.mass))
|
|
216
|
+
{
|
|
217
|
+
this.velocity.x = clamp(this.velocity.x, -objectMaxSpeed, objectMaxSpeed);
|
|
218
|
+
this.velocity.y = clamp(this.velocity.y, -objectMaxSpeed, objectMaxSpeed);
|
|
219
|
+
}
|
|
214
220
|
this.pos.x += this.velocity.x;
|
|
215
221
|
this.pos.y += this.velocity.y;
|
|
216
222
|
this.angle += this.angleVelocity *= this.angleDamping;
|
|
@@ -235,7 +241,8 @@ class EngineObject
|
|
|
235
241
|
{
|
|
236
242
|
// check collisions against solid objects
|
|
237
243
|
const epsilon = .001; // necessary to push slightly outside of the collision
|
|
238
|
-
|
|
244
|
+
// with many solids, only those near it, as a grid finds them, in the same order
|
|
245
|
+
for (const o of engineCollideGrid ? engineCollideGridWalk(this) : engineObjectsCollideStaticLast)
|
|
239
246
|
{
|
|
240
247
|
// skip destroyed, child objects, self collision, or objects with no box
|
|
241
248
|
if (o.destroyed || o.parent || o === this || !o.size.x || !o.size.y) continue;
|
|
@@ -256,6 +263,8 @@ class EngineObject
|
|
|
256
263
|
// notify objects of collision and check if should be resolved
|
|
257
264
|
const collide1 = this.collideWithObject(o);
|
|
258
265
|
const collide2 = o.collideWithObject(this);
|
|
266
|
+
// a callback may have moved either one, the grid follows
|
|
267
|
+
engineCollideGrid?.built && engineCollideGridPlace(engineCollideGrid.built, o);
|
|
259
268
|
if (!collide1 || !collide2)
|
|
260
269
|
{
|
|
261
270
|
engineObjectsCollidePairAdd(this, o);
|
|
@@ -406,34 +415,38 @@ class EngineObject
|
|
|
406
415
|
return;
|
|
407
416
|
}
|
|
408
417
|
|
|
409
|
-
// move
|
|
410
|
-
|
|
418
|
+
// move against the wall and bounce, its side on the tile edge it moved toward, rounded in the
|
|
419
|
+
// layer's space as its collision test is; back to its previous X when that spot is not clear
|
|
420
|
+
const snapEpsilon = .0001, layerX = hitLayer.pos.x, offsetX = this.size.x/2 + snapEpsilon;
|
|
421
|
+
// never back past where it was: a leading edge already on a grid line would step back by
|
|
422
|
+
// the epsilon and take its trailing edge into the tile behind, which nothing tests, since
|
|
423
|
+
// only X is blocked; between there and the wall it covers no column it did not cover before
|
|
424
|
+
const movingLeft = this.pos.x < oldPos.x;
|
|
425
|
+
const snap = layerX + (movingLeft ?
|
|
426
|
+
floor(oldPos.x - layerX - this.size.x/2) + offsetX :
|
|
427
|
+
ceil( oldPos.x - layerX + this.size.x/2) - offsetX);
|
|
428
|
+
const x = movingLeft ? min(snap, oldPos.x) : max(snap, oldPos.x);
|
|
429
|
+
this.pos.x = tileCollisionTest(vec2(x, oldPos.y), this.size, this) ? oldPos.x : x;
|
|
411
430
|
this.velocity.x *= -restitution;
|
|
412
431
|
}
|
|
413
432
|
if (isBlockedY || !isBlockedX)
|
|
414
433
|
{
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
else
|
|
432
|
-
{
|
|
433
|
-
// move to previous Y position
|
|
434
|
-
this.pos.y = oldPos.y;
|
|
435
|
-
this.groundObject = undefined;
|
|
436
|
-
}
|
|
434
|
+
// adjust position to slightly away from the nearest tile, the floor or the ceiling it moved
|
|
435
|
+
// toward, which prevents a gap between them; rounded in the layer's space as its collision
|
|
436
|
+
// test is, or a bottom a hair below a grid line would round to the row under it, inside the
|
|
437
|
+
// floor, and fall through; back to its previous Y when that spot is not clear
|
|
438
|
+
const epsilon = .0001;
|
|
439
|
+
const offset = this.size.y/2 + epsilon;
|
|
440
|
+
const layerY = hitLayer.pos.y;
|
|
441
|
+
const y = layerY + (this.pos.y < oldPos.y ?
|
|
442
|
+
floor(oldPos.y - layerY - this.size.y/2) + offset :
|
|
443
|
+
ceil( oldPos.y - layerY + this.size.y/2) - offset);
|
|
444
|
+
const isClear = this.pos.y !== oldPos.y && !tileCollisionTest(vec2(this.pos.x, y), this.size, this);
|
|
445
|
+
this.pos.y = isClear ? y : oldPos.y;
|
|
446
|
+
|
|
447
|
+
// set ground object for tile collision
|
|
448
|
+
this.groundObject = wasFalling ? hitLayer : undefined;
|
|
449
|
+
|
|
437
450
|
// bounce velocity
|
|
438
451
|
this.velocity.y *= -restitution;
|
|
439
452
|
}
|
|
@@ -443,7 +456,7 @@ class EngineObject
|
|
|
443
456
|
}
|
|
444
457
|
}
|
|
445
458
|
|
|
446
|
-
/** Update the object, called automatically by engine once each frame. Does nothing by default
|
|
459
|
+
/** Update the object, called automatically by engine once each frame. Does nothing by default */
|
|
447
460
|
update() {}
|
|
448
461
|
|
|
449
462
|
/** Render the object, draws a tile by default, automatically called each frame, sorted by renderOrder */
|
|
@@ -453,10 +466,11 @@ class EngineObject
|
|
|
453
466
|
drawTile(this.pos, this.drawSize || this.size, this.tileInfo, this.color, this.angle, this.mirror, this.additiveColor, glEnable, false);
|
|
454
467
|
}
|
|
455
468
|
|
|
456
|
-
/** Optional hook called during the light system plugin's lightmap pass to draw this object's lightmap contribution. Does nothing by default
|
|
469
|
+
/** Optional hook called during the light system plugin's lightmap pass to draw this object's lightmap contribution. Does nothing by default */
|
|
457
470
|
renderLight() {}
|
|
458
471
|
|
|
459
|
-
/** Draw this object into the light system's shadow map, called during its shadow pass when castShadow is set
|
|
472
|
+
/** Draw this object into the light system's shadow map, called during its shadow pass when castShadow is set, and into
|
|
473
|
+
* its background map when castBackgroundShadow is set and a DirectionalLight is out.
|
|
460
474
|
* Calls render() by default so the object casts its own shape; override to cast a different one, like a blob at a character's feet so its body stays lit;
|
|
461
475
|
* screen space WebGL draws in render() are skipped during the pass */
|
|
462
476
|
renderShadow() { this.render(); }
|
|
@@ -502,20 +516,24 @@ class EngineObject
|
|
|
502
516
|
* @return {Vector2} */
|
|
503
517
|
worldToLocalVector(vec) { return vec.rotate(-this.angle); }
|
|
504
518
|
|
|
505
|
-
/** Called to check if a tile collision should be resolved. Return true for physics to resolve the collision or false to ignore and resolve it manually
|
|
519
|
+
/** Called to check if a tile collision should be resolved. Return true for physics to resolve the collision or false to ignore and resolve it manually
|
|
506
520
|
* - Called for each solid tile the physics tests, which can be several times a frame for the same tile, and for
|
|
507
521
|
* positions it only tries, so keep it free of side effects or guard them to once a frame
|
|
508
522
|
* - this.pos has already moved, so a check on where it came from, like a one way platform, needs the position
|
|
509
523
|
* saved in update, as the platformer example does
|
|
524
|
+
* - For the point of impact, like sparks where a bullet hit, raycast from where this frame's move started,
|
|
525
|
+
* this.pos minus this.velocity, to this.pos with tileCollisionRaycast, as the platformer's Bullet does
|
|
510
526
|
* @param {number} tileData - the value of the tile at the position
|
|
511
527
|
* @param {Vector2} pos - the tile's bottom left corner in world space
|
|
512
528
|
* @return {boolean} - true if the collision should be resolved by modifying it's position and velocity */
|
|
513
529
|
collideWithTile(tileData, pos) { return tileData > 0; }
|
|
514
530
|
|
|
515
|
-
/** Called by the engine to check if an object collision should be resolved. Return true for physics to resolve the collision or false to ignore and resolve it manually
|
|
531
|
+
/** Called by the engine to check if an object collision should be resolved. Return true for physics to resolve the collision or false to ignore and resolve it manually
|
|
516
532
|
* - Both objects of a touching pair are asked once a frame, whichever order they update in; an object that
|
|
517
533
|
* destroys itself here is gone at the end of the frame and is still asked about the pairs left this frame, so a
|
|
518
534
|
* bullet that should hit one thing checks its own destroyed flag first
|
|
535
|
+
* - With many solids each mover finds those near it through a grid, which follows the other object after this
|
|
536
|
+
* returns; a third object moved here is found where it is now from the next update on
|
|
519
537
|
* @param {EngineObject} object - the object to test against
|
|
520
538
|
* @param {Vector3} [push] - what it would take to move this object clear, a Vector3 from the 3D plugin, undefined in 2D
|
|
521
539
|
* @return {boolean} - true if the collision should be resolved by modifying it's position and velocity
|
|
@@ -603,7 +621,8 @@ class EngineObject
|
|
|
603
621
|
* @param {EngineObject} child */
|
|
604
622
|
removeChild(child)
|
|
605
623
|
{
|
|
606
|
-
ASSERT(child.parent === this && this.children.includes(child)
|
|
624
|
+
ASSERT(child.parent === this && this.children.includes(child), 'removeChild: that object is not a child of this one',
|
|
625
|
+
child);
|
|
607
626
|
const i = this.children.indexOf(child);
|
|
608
627
|
if (i < 0) return; // not a child of this one, release has no assert
|
|
609
628
|
this.children.splice(i, 1);
|
package/src/engineParticles.js
CHANGED
|
@@ -67,7 +67,7 @@ class ParticleEmitter extends EngineObject
|
|
|
67
67
|
* @param {number} [angleSpeed] - How fast are particles rotating, in radians per frame (at 60fps)
|
|
68
68
|
* @param {number} [damping] - How much to dampen particle speed, per-frame velocity multiplier (1 = no damping, .9 = lose 10% speed each frame)
|
|
69
69
|
* @param {number} [angleDamping] - How much to dampen particle angular speed, per-frame multiplier (1 = no damping)
|
|
70
|
-
* @param {number} [gravityScale] - How much gravity
|
|
70
|
+
* @param {number} [gravityScale] - How much gravity affects particles
|
|
71
71
|
* @param {number} [particleConeAngle] - Half angle each side of the emitter's angle for a particle's start angle, PI is any angle
|
|
72
72
|
* @param {number} [fadeRate] - Fraction of life spent fading: half at fade-in (start), half at fade-out (end). e.g. .2 = 10% fade-in, 80% full opacity, 10% fade-out
|
|
73
73
|
* @param {number} [randomness] - Apply extra randomness percent
|
|
@@ -109,13 +109,34 @@ class ParticleEmitter extends EngineObject
|
|
|
109
109
|
)
|
|
110
110
|
{
|
|
111
111
|
super(pos, vec2(), tileInfo, angle, undefined, renderOrder);
|
|
112
|
+
if (debug)
|
|
113
|
+
{
|
|
114
|
+
// the arguments come in a long row, so one out of place is named rather than emitting NaN particles
|
|
115
|
+
ASSERT(typeof emitSize === 'number' || isVector2(emitSize), 'ParticleEmitter: emitSize must be a number or vec2',
|
|
116
|
+
emitSize);
|
|
117
|
+
ASSERT(!tileInfo || tileInfo instanceof TileInfo, 'ParticleEmitter: tileInfo must be a TileInfo or undefined',
|
|
118
|
+
tileInfo);
|
|
119
|
+
const numbers = {emitTime, emitRate, emitConeAngle, particleTime, sizeStart, sizeEnd, speed, angleSpeed,
|
|
120
|
+
damping, angleDamping, gravityScale, particleConeAngle, fadeRate, randomness};
|
|
121
|
+
// plain strings, as a message is built at every emitter made in a debug build
|
|
122
|
+
for (const name in numbers)
|
|
123
|
+
ASSERT(isNumber(numbers[name]),
|
|
124
|
+
'ParticleEmitter: an argument is not a number, they may be out of order; its name and value:',
|
|
125
|
+
name, numbers[name]);
|
|
126
|
+
const colors = {colorStartA, colorStartB, colorEndA, colorEndB};
|
|
127
|
+
for (const name in colors)
|
|
128
|
+
ASSERT(isColor(colors[name]),
|
|
129
|
+
'ParticleEmitter: an argument is not a color, they may be out of order; its name and value:',
|
|
130
|
+
name, colors[name]);
|
|
131
|
+
}
|
|
112
132
|
|
|
113
133
|
// emitter settings
|
|
114
134
|
/** @property {boolean} - Should particles be emitted in a circle */
|
|
115
135
|
this.emitCircle = typeof emitSize === 'number';
|
|
116
136
|
/** @property {Vector2} - World space size of the emitter, x is the diameter when emitCircle is set */
|
|
117
137
|
this.emitSize = typeof emitSize === 'number' ? vec2(emitSize) : emitSize.copy();
|
|
118
|
-
/** @property {number} - How long to
|
|
138
|
+
/** @property {number} - How long to emit for in seconds, 0 is forever; raised while its particles are still
|
|
139
|
+
* alive, it emits again for the added time */
|
|
119
140
|
this.emitTime = emitTime;
|
|
120
141
|
/** @property {number} - How many particles per second to spawn, does not emit if 0 */
|
|
121
142
|
this.emitRate = emitRate;
|
|
@@ -188,6 +209,9 @@ class ParticleEmitter extends EngineObject
|
|
|
188
209
|
this.velocityInheritance = 0;
|
|
189
210
|
/** @property {number} - Particles owed to the emit rate, starts at one so the first comes out at once */
|
|
190
211
|
this.emitTimeBuffer = 1;
|
|
212
|
+
/** @property {number} - Seconds of its emit time it has emitted for, counted by its updates
|
|
213
|
+
* @type {number} */
|
|
214
|
+
this.emitElapsed = 0;
|
|
191
215
|
/** @property {Array<Particle>} - Array of particles for this emitter
|
|
192
216
|
* @type {Array<Particle>} */
|
|
193
217
|
this.particles = [];
|
|
@@ -196,6 +220,10 @@ class ParticleEmitter extends EngineObject
|
|
|
196
220
|
/** @type {Vector2|undefined} */
|
|
197
221
|
this.previousPos = undefined;
|
|
198
222
|
this.previousAngle = this.angle;
|
|
223
|
+
// where it was last update, and how far back toward it the particle being made starts, 0 to 1: a moving
|
|
224
|
+
// emitter spreads an update's particles along its move, where they came out in a clump where it is
|
|
225
|
+
this.emitFrom = vec2();
|
|
226
|
+
this.emitBehind = 0;
|
|
199
227
|
}
|
|
200
228
|
|
|
201
229
|
/** Update the emitter to spawn particles, called automatically by engine once each frame */
|
|
@@ -221,20 +249,37 @@ class ParticleEmitter extends EngineObject
|
|
|
221
249
|
}
|
|
222
250
|
// tracked even while velocityInheritance is off, so turning it on does not jump
|
|
223
251
|
this.previousAngle = this.angle;
|
|
252
|
+
this.emitFrom.x = this.previousPos.x;
|
|
253
|
+
this.emitFrom.y = this.previousPos.y;
|
|
224
254
|
this.previousPos.x = this.pos.x;
|
|
225
255
|
this.previousPos.y = this.pos.y;
|
|
226
256
|
|
|
227
257
|
// update emitter
|
|
228
258
|
if (this.isActive())
|
|
229
259
|
{
|
|
230
|
-
// emit particles
|
|
260
|
+
// emit particles for the part of this update inside the emit time, counted by its own updates rather than
|
|
261
|
+
// the clock, so a one shot gives the same count whichever frame it is made on
|
|
231
262
|
const rate = this.emitRate * particleEmitRateScale;
|
|
263
|
+
const step = this.emitTime ? min(timeDelta, this.emitTime - this.emitElapsed) : timeDelta;
|
|
264
|
+
this.emitElapsed += timeDelta;
|
|
232
265
|
if (rate > 0 && rate < Infinity)
|
|
233
266
|
{
|
|
234
|
-
// counted in particles, so a new rate applies at once
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
267
|
+
// counted in particles, so a new rate applies at once; the update that ends the emit time adds a
|
|
268
|
+
// hair, as the steps' sum comes out a rounding under it and lost the last particle
|
|
269
|
+
this.emitTimeBuffer += rate * step + (this.emitTime && this.emitElapsed >= this.emitTime ? 1e-6 : 0);
|
|
270
|
+
// spread along the move since the last update, the last one where it is now; a jump further than
|
|
271
|
+
// anything travels in an update, twice objectMaxSpeed, a teleport or a respawn, spreads nothing
|
|
272
|
+
const travelled = this.emitFrom && this.pos.distance(this.emitFrom) <= 2 * objectMaxSpeed;
|
|
273
|
+
const count = floor(this.emitTimeBuffer);
|
|
274
|
+
try
|
|
275
|
+
{
|
|
276
|
+
for (let i = count; i--; --this.emitTimeBuffer)
|
|
277
|
+
{
|
|
278
|
+
this.emitBehind = travelled ? i / count : 0;
|
|
279
|
+
this.emitParticle();
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
finally { this.emitBehind = 0; } // a create callback that throws leaves no offset for the next
|
|
238
283
|
}
|
|
239
284
|
}
|
|
240
285
|
else if (this.particles.length === 0)
|
|
@@ -279,8 +324,9 @@ class ParticleEmitter extends EngineObject
|
|
|
279
324
|
{
|
|
280
325
|
// into the world: a local space particle is turned with the emitter when it draws instead
|
|
281
326
|
this.emitCircle || (pos = pos.rotate(this.angle));
|
|
282
|
-
|
|
283
|
-
pos.
|
|
327
|
+
const behind = this.emitBehind; // back along its move, as the update spreads them
|
|
328
|
+
pos.x += this.pos.x + (this.emitFrom.x - this.pos.x) * behind;
|
|
329
|
+
pos.y += this.pos.y + (this.emitFrom.y - this.pos.y) * behind;
|
|
284
330
|
angle += this.angle;
|
|
285
331
|
}
|
|
286
332
|
|
|
@@ -338,10 +384,11 @@ class ParticleEmitter extends EngineObject
|
|
|
338
384
|
|
|
339
385
|
/** is emitter actively spawning
|
|
340
386
|
* @return {boolean} */
|
|
341
|
-
isActive() { return !this.emitTime || this.
|
|
387
|
+
isActive() { return !this.emitTime || this.emitElapsed < this.emitTime; }
|
|
342
388
|
|
|
343
389
|
/** Destroy the particle emitter
|
|
344
|
-
* @param {boolean} [immediate] - true removes
|
|
390
|
+
* @param {boolean} [immediate] - true removes it and its particles at once, with no particleDestroyCallback for
|
|
391
|
+
* them, false stops emitting and lets the particles finish first */
|
|
345
392
|
destroy(immediate=false)
|
|
346
393
|
{
|
|
347
394
|
if (this.destroyed) return;
|
package/src/engineRelease.js
CHANGED
|
@@ -29,7 +29,6 @@ function LOG (){}
|
|
|
29
29
|
function debugInit (){}
|
|
30
30
|
function debugUpdate (){}
|
|
31
31
|
function debugRender (){}
|
|
32
|
-
function debugRenderPost (){}
|
|
33
32
|
function debugRect (){}
|
|
34
33
|
function debugPoly (){}
|
|
35
34
|
function debugCircle (){}
|
|
@@ -41,10 +40,7 @@ function debugClear (){}
|
|
|
41
40
|
function debugScreenshot (){}
|
|
42
41
|
function debugShowErrors(){}
|
|
43
42
|
function setDebugOverlay(){}
|
|
44
|
-
function
|
|
45
|
-
function debugVideoCaptureStart (){}
|
|
46
|
-
function debugVideoCaptureStop (){}
|
|
47
|
-
function debugProtectConstant(o){ return o; }
|
|
43
|
+
function debugProtectConstant(o){ return Object.freeze(o); } // a color constant stays as it is, in release too
|
|
48
44
|
|
|
49
45
|
// the tweakables and the level editor are debug only
|
|
50
46
|
function tweak(){}
|