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/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
|
|
@@ -413,8 +413,10 @@ function lineTest(posStart, posEnd, testFunction, normal)
|
|
|
413
413
|
const totalLength = (dx*dx + dy*dy)**.5;
|
|
414
414
|
if (!totalLength) return;
|
|
415
415
|
|
|
416
|
-
// current integer cell we are in
|
|
416
|
+
// current integer cell we are in, the one below a whole start heading down the grid, which it goes into
|
|
417
417
|
const pos = posStart.floor();
|
|
418
|
+
if (dx < 0 && pos.x === posStart.x) --pos.x;
|
|
419
|
+
if (dy < 0 && pos.y === posStart.y) --pos.y;
|
|
418
420
|
|
|
419
421
|
// normalize ray direction
|
|
420
422
|
const dirX = dx / totalLength;
|
|
@@ -685,13 +687,8 @@ function vec2(x=0, y) { return new Vector2(x, y ?? x); }
|
|
|
685
687
|
function isVector2(v) { return v instanceof Vector2 && v.isValid(); }
|
|
686
688
|
|
|
687
689
|
// vector2 asserts
|
|
688
|
-
function ASSERT_VECTOR2_VALID(v) { ASSERT(isVector2(v), '
|
|
689
|
-
function ASSERT_NUMBER_VALID(n) { ASSERT(isNumber(n), '
|
|
690
|
-
function ASSERT_VECTOR2_NORMAL(v)
|
|
691
|
-
{
|
|
692
|
-
ASSERT_VECTOR2_VALID(v);
|
|
693
|
-
ASSERT(abs(v.lengthSquared()-1) < .01, 'Vector2 is not normal.', v);
|
|
694
|
-
}
|
|
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); }
|
|
695
692
|
|
|
696
693
|
/**
|
|
697
694
|
* 2D Vector object with vector math library
|
|
@@ -714,7 +711,8 @@ class Vector2
|
|
|
714
711
|
this.x = x;
|
|
715
712
|
/** @property {number} - Y axis location */
|
|
716
713
|
this.y = y;
|
|
717
|
-
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
|
|
718
716
|
}
|
|
719
717
|
|
|
720
718
|
/** Sets values of this vector and returns self
|
|
@@ -945,7 +943,21 @@ class Vector2
|
|
|
945
943
|
* @return {Color}
|
|
946
944
|
* @memberof Math
|
|
947
945
|
*/
|
|
948
|
-
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
|
+
}
|
|
949
961
|
|
|
950
962
|
/**
|
|
951
963
|
* Create a color object with HSLA values, white by default
|
|
@@ -955,7 +967,11 @@ function rgb(r, g, b, a) { return new Color(r, g, b, a); }
|
|
|
955
967
|
* @param {number} [a=1] - alpha
|
|
956
968
|
* @return {Color}
|
|
957
969
|
* @memberof Math */
|
|
958
|
-
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
|
+
}
|
|
959
975
|
|
|
960
976
|
/**
|
|
961
977
|
* Check if object is a valid Color
|
|
@@ -965,7 +981,7 @@ function hsl(h, s, l, a) { return new Color().setHSLA(h, s, l, a); }
|
|
|
965
981
|
function isColor(c) { return c instanceof Color && c.isValid(); }
|
|
966
982
|
|
|
967
983
|
// color asserts
|
|
968
|
-
function ASSERT_COLOR_VALID(c) { ASSERT(isColor(c), '
|
|
984
|
+
function ASSERT_COLOR_VALID(c) { ASSERT(isColor(c), 'expected a color', c); }
|
|
969
985
|
|
|
970
986
|
/**
|
|
971
987
|
* Color object (red, green, blue, alpha) with some helpful functions
|
|
@@ -994,7 +1010,7 @@ class Color
|
|
|
994
1010
|
this.b = b;
|
|
995
1011
|
/** @property {number} - Alpha */
|
|
996
1012
|
this.a = a;
|
|
997
|
-
ASSERT(this.isValid(), '
|
|
1013
|
+
ASSERT(this.isValid(), 'Color: r, g, b and a must be numbers', this);
|
|
998
1014
|
}
|
|
999
1015
|
|
|
1000
1016
|
/** Sets values of this color and returns self
|
|
@@ -1156,10 +1172,12 @@ class Color
|
|
|
1156
1172
|
* @return {string} */
|
|
1157
1173
|
toString(useAlpha = true)
|
|
1158
1174
|
{
|
|
1159
|
-
if (
|
|
1160
|
-
return '#000';
|
|
1161
|
-
|
|
1162
|
-
|
|
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) : '');
|
|
1163
1181
|
}
|
|
1164
1182
|
|
|
1165
1183
|
/** Set this color from a hex code
|
|
@@ -1169,7 +1187,7 @@ class Color
|
|
|
1169
1187
|
{
|
|
1170
1188
|
ASSERT(isStringLike(hex), 'Color hex code must be a string');
|
|
1171
1189
|
ASSERT(hex[0] === '#', 'Color hex code must start with #');
|
|
1172
|
-
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);
|
|
1173
1191
|
|
|
1174
1192
|
if (hex.length < 6)
|
|
1175
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(); }
|
|
@@ -483,35 +497,43 @@ class EngineObject
|
|
|
483
497
|
}
|
|
484
498
|
|
|
485
499
|
/** Convert from local space to world space
|
|
486
|
-
* @param {Vector2} pos - local space point
|
|
500
|
+
* @param {Vector2} pos - local space point
|
|
501
|
+
* @return {Vector2} */
|
|
487
502
|
localToWorld(pos) { return this.pos.add(pos.rotate(this.angle)); }
|
|
488
503
|
|
|
489
504
|
/** Convert from world space to local space
|
|
490
|
-
* @param {Vector2} pos - world space point
|
|
505
|
+
* @param {Vector2} pos - world space point
|
|
506
|
+
* @return {Vector2} */
|
|
491
507
|
worldToLocal(pos) { return pos.subtract(this.pos).rotate(-this.angle); }
|
|
492
508
|
|
|
493
509
|
/** Convert from local space to world space for a vector (rotation only)
|
|
494
|
-
* @param {Vector2} vec - local space vector
|
|
510
|
+
* @param {Vector2} vec - local space vector
|
|
511
|
+
* @return {Vector2} */
|
|
495
512
|
localToWorldVector(vec) { return vec.rotate(this.angle); }
|
|
496
513
|
|
|
497
514
|
/** Convert from world space to local space for a vector (rotation only)
|
|
498
|
-
* @param {Vector2} vec - world space vector
|
|
515
|
+
* @param {Vector2} vec - world space vector
|
|
516
|
+
* @return {Vector2} */
|
|
499
517
|
worldToLocalVector(vec) { return vec.rotate(-this.angle); }
|
|
500
518
|
|
|
501
|
-
/** 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
|
|
502
520
|
* - Called for each solid tile the physics tests, which can be several times a frame for the same tile, and for
|
|
503
521
|
* positions it only tries, so keep it free of side effects or guard them to once a frame
|
|
504
522
|
* - this.pos has already moved, so a check on where it came from, like a one way platform, needs the position
|
|
505
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
|
|
506
526
|
* @param {number} tileData - the value of the tile at the position
|
|
507
527
|
* @param {Vector2} pos - the tile's bottom left corner in world space
|
|
508
528
|
* @return {boolean} - true if the collision should be resolved by modifying it's position and velocity */
|
|
509
529
|
collideWithTile(tileData, pos) { return tileData > 0; }
|
|
510
530
|
|
|
511
|
-
/** 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
|
|
512
532
|
* - Both objects of a touching pair are asked once a frame, whichever order they update in; an object that
|
|
513
533
|
* destroys itself here is gone at the end of the frame and is still asked about the pairs left this frame, so a
|
|
514
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
|
|
515
537
|
* @param {EngineObject} object - the object to test against
|
|
516
538
|
* @param {Vector3} [push] - what it would take to move this object clear, a Vector3 from the 3D plugin, undefined in 2D
|
|
517
539
|
* @return {boolean} - true if the collision should be resolved by modifying it's position and velocity
|
|
@@ -599,7 +621,8 @@ class EngineObject
|
|
|
599
621
|
* @param {EngineObject} child */
|
|
600
622
|
removeChild(child)
|
|
601
623
|
{
|
|
602
|
-
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);
|
|
603
626
|
const i = this.children.indexOf(child);
|
|
604
627
|
if (i < 0) return; // not a child of this one, release has no assert
|
|
605
628
|
this.children.splice(i, 1);
|
|
@@ -634,11 +657,6 @@ class EngineObject
|
|
|
634
657
|
this.collideRaycast = collideRaycast;
|
|
635
658
|
}
|
|
636
659
|
|
|
637
|
-
/** @deprecated since 1.20, use collideLevel
|
|
638
|
-
* @type {boolean} */
|
|
639
|
-
get collideTiles() { return this.collideLevel; }
|
|
640
|
-
set collideTiles(collide) { this.collideLevel = collide; }
|
|
641
|
-
|
|
642
660
|
/** Returns string containing info about this object for debugging
|
|
643
661
|
* @return {string} */
|
|
644
662
|
toString()
|
package/src/engineParticles.js
CHANGED
|
@@ -37,13 +37,13 @@
|
|
|
37
37
|
* let pos = vec2(2,3);
|
|
38
38
|
* let particleEmitter = new ParticleEmitter
|
|
39
39
|
* (
|
|
40
|
-
* pos, 0, 1, 0, 500, PI, // pos, angle, emitSize, emitTime, emitRate,
|
|
40
|
+
* pos, 0, 1, 0, 500, PI, // pos, angle, emitSize, emitTime, emitRate, emitConeAngle
|
|
41
41
|
* tile(0, 16), // tileInfo
|
|
42
42
|
* rgb(1,1,1,1), rgb(0,0,0,1), // colorStartA, colorStartB
|
|
43
43
|
* rgb(1,1,1,0), rgb(0,0,0,0), // colorEndA, colorEndB
|
|
44
|
-
* 1, .2, .2, .1, .05, // particleTime, sizeStart, sizeEnd,
|
|
45
|
-
* .99, 1, 1, PI, .05, // damping, angleDamping, gravityScale,
|
|
46
|
-
* .5, true // randomness,
|
|
44
|
+
* 1, .2, .2, .1, .05, // particleTime, sizeStart, sizeEnd, speed, angleSpeed
|
|
45
|
+
* .99, 1, 1, PI, .05, // damping, angleDamping, gravityScale, particleConeAngle, fadeRate
|
|
46
|
+
* .5, true // randomness, collideLevel
|
|
47
47
|
* );
|
|
48
48
|
*/
|
|
49
49
|
class ParticleEmitter extends EngineObject
|
|
@@ -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
|
|
|
@@ -336,11 +382,13 @@ class ParticleEmitter extends EngineObject
|
|
|
336
382
|
this.additive && setAdditiveBlendMode(false);
|
|
337
383
|
}
|
|
338
384
|
|
|
339
|
-
/** is emitter actively spawning
|
|
340
|
-
|
|
385
|
+
/** is emitter actively spawning
|
|
386
|
+
* @return {boolean} */
|
|
387
|
+
isActive() { return !this.emitTime || this.emitElapsed < this.emitTime; }
|
|
341
388
|
|
|
342
389
|
/** Destroy the particle emitter
|
|
343
|
-
* @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 */
|
|
344
392
|
destroy(immediate=false)
|
|
345
393
|
{
|
|
346
394
|
if (this.destroyed) return;
|
|
@@ -548,8 +596,8 @@ class Particle
|
|
|
548
596
|
}
|
|
549
597
|
if (isBlockedY || !isBlockedX)
|
|
550
598
|
{
|
|
551
|
-
// down is the world's gravity
|
|
552
|
-
const down =
|
|
599
|
+
// down is the way it falls, the world's gravity by its gravityScale and the emitter's own
|
|
600
|
+
const down = gravityY;
|
|
553
601
|
const wasFalling = this.velocity.y < 0 && down < 0 || this.velocity.y > 0 && down > 0;
|
|
554
602
|
if (wasFalling)
|
|
555
603
|
this.groundObject = hitLayer;
|
package/src/engineRelease.js
CHANGED
|
@@ -10,18 +10,17 @@
|
|
|
10
10
|
|
|
11
11
|
'use strict';
|
|
12
12
|
|
|
13
|
-
let debugWatermark =
|
|
13
|
+
let debugWatermark = false;
|
|
14
14
|
let debugKey = '';
|
|
15
15
|
let debugKeysAlways = false;
|
|
16
16
|
let debugTweakables = false;
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
const debugSound = 0;
|
|
17
|
+
const debug = false;
|
|
18
|
+
const debugOverlay = false;
|
|
19
|
+
const debugPhysics = false;
|
|
20
|
+
const debugParticles = false;
|
|
21
|
+
const debugRaycast = false;
|
|
22
|
+
const debugGamepads = false;
|
|
23
|
+
const debugSound = false;
|
|
25
24
|
const debugPointSize = .5;
|
|
26
25
|
|
|
27
26
|
// debug commands are automatically removed from the final build
|
|
@@ -30,7 +29,6 @@ function LOG (){}
|
|
|
30
29
|
function debugInit (){}
|
|
31
30
|
function debugUpdate (){}
|
|
32
31
|
function debugRender (){}
|
|
33
|
-
function debugRenderPost (){}
|
|
34
32
|
function debugRect (){}
|
|
35
33
|
function debugPoly (){}
|
|
36
34
|
function debugCircle (){}
|
|
@@ -42,16 +40,21 @@ function debugClear (){}
|
|
|
42
40
|
function debugScreenshot (){}
|
|
43
41
|
function debugShowErrors(){}
|
|
44
42
|
function setDebugOverlay(){}
|
|
45
|
-
function
|
|
46
|
-
function debugVideoCaptureStart (){}
|
|
47
|
-
function debugVideoCaptureStop (){}
|
|
48
|
-
function debugProtectConstant(o){ return o; }
|
|
43
|
+
function debugProtectConstant(o){ return Object.freeze(o); } // a color constant stays as it is, in release too
|
|
49
44
|
|
|
50
45
|
// the tweakables and the level editor are debug only
|
|
51
46
|
function tweak(){}
|
|
52
47
|
function tweakButton(){}
|
|
53
48
|
function tweakDivider(){}
|
|
54
49
|
function tweakEngineDefaults(){}
|
|
50
|
+
// the 2D editor's edit functions in a release build, which has no editor: there is nothing to edit and nothing
|
|
51
|
+
// changes, and a game that calls them needs no guard; edit3D stays undefined, as it is with no 3D plugins
|
|
52
|
+
const editorEdit2DRelease =
|
|
53
|
+
{
|
|
54
|
+
map: undefined, layer: undefined, hover: undefined, get objects() { return []; }, get selection() { return new Set; },
|
|
55
|
+
paint(){ return false; }, changeObjects(){ return false; }, strokeEnd(){}, strokeCancel(){}, bulk(){},
|
|
56
|
+
undo(){ return false; }, toJSON(){ return ''; },
|
|
57
|
+
};
|
|
55
58
|
class LevelEditor
|
|
56
59
|
{
|
|
57
60
|
constructor()
|
|
@@ -61,7 +64,7 @@ class LevelEditor
|
|
|
61
64
|
}
|
|
62
65
|
get isOpen() { return false; }
|
|
63
66
|
get is3D() { return false; }
|
|
64
|
-
get edit2D() { return
|
|
67
|
+
get edit2D() { return editorEdit2DRelease; }
|
|
65
68
|
get edit3D() { return undefined; }
|
|
66
69
|
open(){} close(){} addKey(){} addButton(){} addTool(){}
|
|
67
70
|
onTile(){} onRestart(){} onPlayFrom(){} onOpen(){} onClose(){} onUpdate(){} onDraw(){} onPanel(){}
|