littlejsengine 1.19.3 → 1.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/FAQ.md +841 -820
  2. package/README.md +29 -25
  3. package/dist/littlejs.d.ts +2455 -935
  4. package/dist/littlejs.esm.js +30344 -21110
  5. package/dist/littlejs.esm.min.js +4 -3
  6. package/dist/littlejs.js +30259 -21052
  7. package/dist/littlejs.min.js +2 -2
  8. package/dist/littlejs.release.js +26567 -20455
  9. package/package.json +10 -5
  10. package/plugins/audioEffects.js +28 -24
  11. package/plugins/box2d.js +2516 -2073
  12. package/plugins/drawUtilities.js +206 -31
  13. package/plugins/gltf.js +740 -0
  14. package/plugins/jsconfig.json +19 -0
  15. package/plugins/lightSystem.js +719 -332
  16. package/plugins/math3d.js +58 -17
  17. package/plugins/medalSystem.js +326 -280
  18. package/plugins/newgrounds.js +400 -199
  19. package/plugins/pathFinder.js +208 -175
  20. package/plugins/pluginExport.js +15 -0
  21. package/plugins/postProcess.js +56 -13
  22. package/plugins/render3d.js +1643 -1577
  23. package/plugins/render3dDebug.js +109 -0
  24. package/plugins/render3dExtras.js +1754 -0
  25. package/plugins/render3dVoxels.js +502 -0
  26. package/plugins/textureSheet.js +496 -458
  27. package/plugins/threejs.js +183 -163
  28. package/plugins/tweakables.js +411 -0
  29. package/plugins/tweenSystem.js +263 -145
  30. package/plugins/uiSystem.js +2024 -1687
  31. package/plugins/zzfxm.js +159 -159
  32. package/src/engine.js +191 -75
  33. package/src/engineAudio.js +229 -125
  34. package/src/engineBuild.mjs +289 -256
  35. package/src/engineDebug.js +912 -742
  36. package/src/engineDraw.js +295 -193
  37. package/src/engineEditor.js +2431 -0
  38. package/src/engineExport.js +12 -0
  39. package/src/engineInput.js +1462 -1293
  40. package/src/engineMath.js +1274 -1252
  41. package/src/engineObject.js +194 -80
  42. package/src/engineParticles.js +583 -532
  43. package/src/engineRelease.js +67 -45
  44. package/src/engineSettings.js +77 -29
  45. package/src/engineTileLayer.js +995 -741
  46. package/src/engineUtilities.js +19 -13
  47. package/src/engineWebGL.js +239 -82
  48. package/src/jsconfig.json +17 -13
  49. package/plugins/box2d.wasm.js +0 -630
  50. package/plugins/box2d.wasm.wasm +0 -0
@@ -30,6 +30,7 @@
30
30
  * - If an object starts or is moved inside tile collision, it will not collide with that tile
31
31
  * - Collision for objects can be set to be solid to block other objects
32
32
  * - Objects may get pushed into overlapping other solid objects, if so they will push away
33
+ * - A static solid (mass 0) moved by its velocity, like a door or an elevator, pushes objects out of its way
33
34
  * - Solid objects are more performance intensive and should be used sparingly
34
35
  * @memberof Engine
35
36
  * @example
@@ -61,63 +62,81 @@ class EngineObject
61
62
  this.pos = pos.copy();
62
63
  /** @property {Vector2} - World space width and height of the object */
63
64
  this.size = size.copy();
64
- /** @property {Vector2} - Size of object used for drawing, uses size if not set */
65
+ /** @property {Vector2|undefined} - Size of object used for drawing, uses size if not set
66
+ * @type {Vector2|undefined} */
65
67
  this.drawSize = undefined;
66
- /** @property {TileInfo} - Tile info to render object (undefined is untextured) */
68
+ /** @property {TileInfo|undefined} - Tile info to render object (undefined is untextured)
69
+ * @type {TileInfo|undefined} */
67
70
  this.tileInfo = tileInfo;
68
71
  /** @property {number} - Angle to rotate the object */
69
72
  this.angle = angle;
70
73
  /** @property {Color} - Color to apply when rendered */
71
74
  this.color = color.copy();
72
- /** @property {Color} - Additive color to apply when rendered */
75
+ /** @property {Color|undefined} - Additive color to apply when rendered
76
+ * @type {Color|undefined} */
73
77
  this.additiveColor = undefined;
74
78
  /** @property {Shader|undefined} - Custom shader to render with, undefined for the engine's own
75
79
  * @type {Shader|undefined} */
76
80
  this.shader = undefined;
81
+ /** @property {boolean} - Does this object draw into the light system's shadow map; false for a floor layer, a background, a pickup */
82
+ this.castShadow = true;
83
+ /** @property {number} - With the light system, how much it lights itself: 0 lit only by the lights, 1 full
84
+ * brightness in its own colors whatever the lights do, between partly; drawn into the lightmap through
85
+ * renderEmissive, as 3D's emissive. Exact for solid pixels; a partly transparent one is self lit by its alpha
86
+ * too, so a half alpha pixel shows at a quarter and a fading emissive sprite fades a little faster */
87
+ this.emissive = 0;
77
88
  /** @property {boolean} - Should the rendered tile flip along the y axis. Affects rendering and the local→world transform of attached children (a mirrored parent flips its children's localPos.x and localAngle). Does not affect this object's own physics, collision, or localToWorld/worldToLocal. */
78
89
  this.mirror = false;
79
90
  /** @property {boolean} - Has object been destroyed? */
80
91
  this.destroyed = false;
92
+ this.updatePass = 0; // the engine update pass it was last updated in, so nothing updates twice in one
81
93
 
82
94
  // physical properties
83
- /** @property {number} - How heavy the object is, static if 0 */
95
+ /** @property {number} - How heavy the object is, static if 0: a static object moves by its velocity but does not
96
+ * collide on its own, the moving ones collide with it */
84
97
  this.mass = objectDefaultMass;
85
- /** @property {number} - How much to slow down velocity each frame (0-1) */
98
+ /** @property {number} - Fraction of velocity kept each frame, 1 keeps all of it, 0 stops at once */
86
99
  this.damping = objectDefaultDamping;
87
- /** @property {number} - How much to slow down rotation each frame (0-1) */
100
+ /** @property {number} - Fraction of angular velocity kept each frame, 1 keeps all of it, 0 stops at once */
88
101
  this.angleDamping = objectDefaultAngleDamping;
89
102
  /** @property {number} - How bouncy the object is when colliding (0-1) */
90
103
  this.restitution = objectDefaultRestitution;
91
- /** @property {number} - How much friction to apply when sliding (0-1) */
104
+ /** @property {number} - Fraction of sliding speed kept each frame on the ground, 1 is no friction, 0 stops at
105
+ * once, the more slippery of the object and its ground is used */
92
106
  this.friction = objectDefaultFriction;
93
107
  /** @property {number} - How much to scale gravity by for this object */
94
108
  this.gravityScale = 1;
95
109
  /** @property {number} - Objects are sorted by render order */
96
110
  this.renderOrder = renderOrder;
97
- /** @property {Vector2} - Velocity of the object */
111
+ /** @property {Vector2} - Velocity of the object, in world units per frame */
98
112
  this.velocity = vec2();
99
- /** @property {number} - Angular velocity of the object */
113
+ /** @property {number} - Angular velocity of the object, in radians per frame */
100
114
  this.angleVelocity = 0;
101
115
  /** @property {number} - Track when object was created */
102
116
  this.spawnTime = time;
103
- /** @property {Array<EngineObject>} - List of children of this object */
117
+ /** @property {Array<EngineObject>} - List of children of this object
118
+ * @type {Array<EngineObject>} */
104
119
  this.children = [];
105
120
  /** @property {boolean} - Limit object speed along x and y axis */
106
121
  this.clampSpeed = true;
107
- /** @property {EngineObject} - Object we are standing on, if any */
122
+ /** @property {EngineObject|undefined} - Object we are standing on, if any
123
+ * @type {EngineObject|undefined} */
108
124
  this.groundObject = undefined;
109
125
 
110
126
  // parent child system
111
- /** @property {EngineObject} - Parent of object if in local space */
127
+ /** @property {EngineObject|undefined} - Parent of object if in local space
128
+ * @type {EngineObject|undefined} */
112
129
  this.parent = undefined;
113
- /** @property {Vector2} - Local position if child */
114
- this.localPos = vec2();
130
+ /** @property {Vector2|undefined} - Position relative to the parent, only while attached to one
131
+ * @type {Vector2|undefined} */
132
+ this.localPos = undefined;
115
133
  /** @property {number} - Local angle if child */
116
134
  this.localAngle = 0;
117
135
 
118
136
  // collision flags
119
- /** @property {boolean} - Object collides with the tile collision */
120
- this.collideTiles = false;
137
+ /** @property {boolean} - Object collides with the level, its tile collision layers
138
+ * @type {boolean} */
139
+ this.collideLevel = false;
121
140
  /** @property {boolean} - Object collides with solid objects */
122
141
  this.collideSolidObjects = false;
123
142
  /** @property {boolean} - Object collides with and blocks other objects */
@@ -133,8 +152,9 @@ class EngineObject
133
152
  engineObjects.push(this);
134
153
  }
135
154
 
136
- /** Update the object transform, called automatically by engine even when paused */
137
- updateTransforms()
155
+ /** Update the object transform, called automatically by engine even when paused
156
+ * @param {boolean} [updateChildren] - Also update the children's transforms */
157
+ updateTransforms(updateChildren=true)
138
158
  {
139
159
  const parent = this.parent;
140
160
  if (parent)
@@ -154,8 +174,9 @@ class EngineObject
154
174
  }
155
175
 
156
176
  // update children
157
- for (const child of this.children)
158
- child.updateTransforms();
177
+ if (updateChildren)
178
+ for (const child of this.children)
179
+ child.updateTransforms();
159
180
  }
160
181
 
161
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. */
@@ -174,8 +195,13 @@ class EngineObject
174
195
  this.velocity.y = clamp(this.velocity.y, -objectMaxSpeed, objectMaxSpeed);
175
196
  }
176
197
 
177
- // apply physics
178
- const oldPos = this.pos.copy();
198
+ // physics sanity checks
199
+ ASSERT(this.angleDamping >= 0 && this.angleDamping <= 1, 'angleDamping must be 0 to 1, the fraction kept each frame');
200
+ ASSERT(this.damping >= 0 && this.damping <= 1, 'damping must be 0 to 1, the fraction of velocity kept each frame');
201
+
202
+ // apply physics; only the solver needs where the object was, so only then is it copied
203
+ const solve = enablePhysicsSolver && this.mass;
204
+ const oldPos = solve ? this.pos.copy() : undefined;
179
205
  this.velocity.x *= this.damping;
180
206
  this.velocity.y *= this.damping;
181
207
  if (this.mass)
@@ -188,14 +214,12 @@ class EngineObject
188
214
  this.pos.y += this.velocity.y;
189
215
  this.angle += this.angleVelocity *= this.angleDamping;
190
216
 
191
- // physics sanity checks
192
- ASSERT(this.angleDamping >= 0 && this.angleDamping <= 1);
193
- ASSERT(this.damping >= 0 && this.damping <= 1);
194
-
195
217
  // don't do collision for static objects or if solver disabled
196
- if (!enablePhysicsSolver || !this.mass) return;
218
+ if (!solve) return;
197
219
 
198
- const wasFalling = this.velocity.y < 0 && gravity.y < 0 || this.velocity.y > 0 && gravity.y > 0;
220
+ // which way is down for this object, a negative gravityScale falls up and lands on ceilings
221
+ const gravityY = this.gravityScale < 0 ? -gravity.y : gravity.y;
222
+ const wasFalling = this.velocity.y < 0 && gravityY < 0 || this.velocity.y > 0 && gravityY > 0;
199
223
  if (this.groundObject)
200
224
  {
201
225
  // apply friction in local space of ground object
@@ -210,7 +234,7 @@ class EngineObject
210
234
  {
211
235
  // check collisions against solid objects
212
236
  const epsilon = .001; // necessary to push slightly outside of the collision
213
- for (const o of engineObjectsCollide)
237
+ for (const o of engineObjectsCollideStaticLast)
214
238
  {
215
239
  // skip destroyed, child objects, self collision, or objects with no box
216
240
  if (o.destroyed || o.parent || o === this || !o.size.x || !o.size.y) continue;
@@ -221,29 +245,76 @@ class EngineObject
221
245
  // check collision
222
246
  if (!this.isOverlappingObject(o)) continue;
223
247
 
224
- // notify objects of collision and check if should be resolved
225
- const collide1 = this.collideWithObject(o);
226
- const collide2 = o.collideWithObject(this);
227
- if (!collide1 || !collide2) continue;
248
+ // each moving object checks its own contacts, so a pair the other one already asked about this frame
249
+ // is not asked twice: left overlapping, ignored or only nudged apart it is skipped, and one both said
250
+ // to resolve, pushed back together since, is resolved again
251
+ const answer = engineObjectsCollidePairAnswer(o, this);
252
+ if (answer === false) continue;
253
+ if (!answer)
254
+ {
255
+ // notify objects of collision and check if should be resolved
256
+ const collide1 = this.collideWithObject(o);
257
+ const collide2 = o.collideWithObject(this);
258
+ if (!collide1 || !collide2)
259
+ {
260
+ engineObjectsCollidePairAdd(this, o);
261
+ continue;
262
+ }
263
+ }
264
+
265
+ const wasOverlapping = isOverlapping(oldPos, this.size, o.pos, o.size);
266
+ if (wasOverlapping && (!o.mass || o.groundObject))
267
+ {
268
+ // a static solid that moved into it, like a door or an elevator, pushes it out the shortest way
269
+ // at once and carries it along, it would only drift out slowly and the solid would pass through;
270
+ // an object standing on something counts as fixed too, so a stack on an elevator rides together;
271
+ // it bounces off relative to the mover, a paddle moved by setting pos bounces a ball as a wall does
272
+ const push = collideBoxBox(this.pos, this.size, o.pos, o.size);
273
+ if (push)
274
+ {
275
+ this.pos.x += push.x + sign(push.x) * epsilon;
276
+ this.pos.y += push.y + sign(push.y) * epsilon;
277
+ const restitution = max(this.restitution, o.restitution);
278
+ if (push.x)
279
+ {
280
+ const v = this.velocity.x - o.velocity.x;
281
+ if (v * push.x < 0) // moving into it
282
+ this.velocity.x = o.velocity.x - v * restitution;
283
+ }
284
+ else
285
+ {
286
+ const v = this.velocity.y - o.velocity.y;
287
+ if (v * push.y < 0) // moving into it
288
+ this.velocity.y = o.velocity.y - v * restitution;
289
+ if (push.y * gravityY < 0) // pushed up against gravity, it stands on it
290
+ this.groundObject = o;
291
+ }
292
+ }
293
+ engineObjectsCollidePairAdd(this, o);
228
294
 
229
- if (isOverlapping(oldPos, this.size, o.pos, o.size))
295
+ debugPhysics && debugOverlap(this.pos, this.size, o.pos, o.size, '#f00', undefined, false);
296
+ continue;
297
+ }
298
+ if (wasOverlapping)
230
299
  {
231
300
  // if already was touching, try to push away
232
301
  const deltaPos = oldPos.subtract(o.pos);
233
302
  const length = deltaPos.length();
234
303
  const pushAwayAccel = .001;
235
- const velocity = length < .001 ? vec2(0,1) : deltaPos.scale(pushAwayAccel/length);
304
+ const velocity = length < .001 ? vec2(0, pushAwayAccel) : deltaPos.scale(pushAwayAccel/length);
236
305
  this.velocity = this.velocity.add(velocity);
237
306
  if (o.mass) // push away other object if not fixed
238
307
  o.velocity = o.velocity.subtract(velocity);
308
+ engineObjectsCollidePairAdd(this, o);
239
309
 
240
- debugPhysics && debugOverlap(this.pos, this.size, o.pos, o.size, '#f00');
310
+ debugPhysics && debugOverlap(this.pos, this.size, o.pos, o.size, '#f00', undefined, false);
241
311
  continue;
242
312
  }
243
313
 
244
314
  // check for collision
245
315
  const sizeBoth = this.size.add(o.size);
246
- const smallStepUp = (oldPos.y - o.pos.y)*2 > sizeBoth.y + gravity.y; // prefer to push up if small delta
316
+ // prefer to push up if small delta, up away from this object's own gravity
317
+ const smallStepUp = (gravityY > 0 ? o.pos.y - oldPos.y : oldPos.y - o.pos.y)*2 > sizeBoth.y - abs(gravityY);
247
318
  const isBlockedX = abs(oldPos.y - o.pos.y)*2 < sizeBoth.y;
248
319
  const isBlockedY = abs(oldPos.x - o.pos.x)*2 < sizeBoth.x;
249
320
  const restitution = max(this.restitution, o.restitution);
@@ -258,10 +329,11 @@ class EngineObject
258
329
  if (wasFalling)
259
330
  this.groundObject = o;
260
331
 
261
- // bounce if other object is fixed or grounded
262
- this.velocity.y *= -restitution;
332
+ // bounce if other object is fixed or grounded, relative to it so a rider keeps up with a
333
+ // platform moving down instead of landing on it again every few frames
334
+ this.velocity.y = o.velocity.y - (this.velocity.y - o.velocity.y) * restitution;
263
335
  }
264
- else if (o.mass)
336
+ else // o has mass here, the other branch already handled the massless case
265
337
  {
266
338
  // inelastic collision
267
339
  const inelastic = (this.mass * this.velocity.y + o.mass * o.velocity.y) / (this.mass + o.mass);
@@ -296,13 +368,14 @@ class EngineObject
296
368
  this.velocity.x = lerp(inelastic, elastic0, restitution);
297
369
  o.velocity.x = lerp(inelastic, elastic1, restitution);
298
370
  }
299
- else // bounce if other object is fixed
300
- this.velocity.x *= -restitution;
371
+ else // bounce if other object is fixed, relative to it as a landing is
372
+ this.velocity.x = o.velocity.x - (this.velocity.x - o.velocity.x) * restitution;
301
373
  }
302
- debugPhysics && debugOverlap(this.pos, this.size, o.pos, o.size, '#f0f');
374
+ engineObjectsCollidePairAdd(this, o, true);
375
+ debugPhysics && debugOverlap(this.pos, this.size, o.pos, o.size, '#f0f', undefined, false);
303
376
  }
304
377
  }
305
- if (this.collideTiles)
378
+ if (this.collideLevel)
306
379
  {
307
380
  // check collision against tiles
308
381
  const hitLayer = tileCollisionTest(this.pos, this.size, this);
@@ -318,21 +391,17 @@ class EngineObject
318
391
  const restitution = max(this.restitution, hitLayer.restitution);
319
392
  if (isBlockedX)
320
393
  {
321
- // try to step over a 1-tile bump (direction follows gravity sign
322
- // so inverted gravity steps down off a ceiling bump instead of up;
323
- // zero gravity defaults to the normal-gravity step-up direction)
394
+ // a ledge caught less than maxMove below its top lifts the object onto it instead of stopping it,
395
+ // down off a ceiling ledge when gravity points up; zero gravity counts as down
324
396
  const epsilon = 1e-3;
325
397
  const maxMove = .1;
326
- const gravitySign = gravity.y > 0 ? -1 : 1;
327
- const y = gravitySign > 0 ?
328
- floor(oldPos.y-this.size.y/2+1) + this.size.y/2 + epsilon :
329
- ceil( oldPos.y+this.size.y/2-1) - this.size.y/2 - epsilon;
330
- const delta = abs(y - this.pos.y);
331
- if (delta < maxMove)
332
- if (!tileCollisionTest(vec2(this.pos.x, y), this.size, this))
398
+ const y = gravityY > 0 ?
399
+ ceil( oldPos.y+this.size.y/2-1) - this.size.y/2 - epsilon :
400
+ floor(oldPos.y-this.size.y/2+1) + this.size.y/2 + epsilon;
401
+ if (abs(y - this.pos.y) < maxMove && !tileCollisionTest(vec2(this.pos.x, y), this.size, this))
333
402
  {
334
403
  this.pos.y = y;
335
- debugPhysics && debugRect(this.pos, this.size, '#ff0');
404
+ debugPhysics && debugRect(this.pos, this.size, '#ff0', 0, 0, false, false);
336
405
  return;
337
406
  }
338
407
 
@@ -348,9 +417,12 @@ class EngineObject
348
417
  // this prevents gap between object and ground
349
418
  const epsilon = .0001;
350
419
  const offset = this.size.y/2 + epsilon;
351
- this.pos.y = gravity.y < 0 ?
352
- floor(oldPos.y-this.size.y/2) + offset :
353
- ceil( oldPos.y+this.size.y/2) - offset;
420
+ // rounded in the layer's space as its collision test is, or a bottom a hair below a
421
+ // grid line would round to the row under it, inside the floor, and fall through
422
+ const layerY = hitLayer.pos.y;
423
+ this.pos.y = layerY + (gravityY < 0 ?
424
+ floor(oldPos.y - layerY - this.size.y/2) + offset :
425
+ ceil( oldPos.y - layerY + this.size.y/2) - offset);
354
426
 
355
427
  // set ground object for tile collision
356
428
  this.groundObject = hitLayer;
@@ -364,7 +436,7 @@ class EngineObject
364
436
  // bounce velocity
365
437
  this.velocity.y *= -restitution;
366
438
  }
367
- debugPhysics && debugRect(this.pos, this.size, '#f00');
439
+ debugPhysics && debugRect(this.pos, this.size, '#f00', 0, 0, false, false);
368
440
  }
369
441
  }
370
442
  }
@@ -377,14 +449,24 @@ class EngineObject
377
449
  render()
378
450
  {
379
451
  // default object render
380
- drawTile(this.pos, this.drawSize || this.size, this.tileInfo, this.color, this.angle, this.mirror, this.additiveColor);
452
+ drawTile(this.pos, this.drawSize || this.size, this.tileInfo, this.color, this.angle, this.mirror, this.additiveColor, glEnable, false);
381
453
  }
382
454
 
383
455
  /** Optional hook called during the light system plugin's lightmap pass to draw this object's lightmap contribution. Does nothing by default. */
384
456
  renderLight() {}
385
457
 
458
+ /** Draw this object into the light system's shadow map, called during its shadow pass when castShadow is set.
459
+ * 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;
460
+ * screen space WebGL draws in render() are skipped during the pass */
461
+ renderShadow() { this.render(); }
462
+
463
+ /** Draw this object's shape into the light system's lightmap, called during its light pass when emissive is above
464
+ * 0; what it draws shows in its own colors that much brighter. Calls render() by default so the whole object
465
+ * glows; override to glow a part, like a robot's eyes */
466
+ renderEmissive() { this.render(); }
467
+
386
468
  /** Destroy this object, destroy its children, detach its parent, and mark it for removal
387
- * @param {boolean} [immediate] - should attached effects be allowed to die off? */
469
+ * @param {boolean} [immediate] - true removes attached effects like particle emitters at once, false lets them finish first */
388
470
  destroy(immediate=false)
389
471
  {
390
472
  if (this.destroyed) return;
@@ -416,14 +498,21 @@ class EngineObject
416
498
  worldToLocalVector(vec) { return vec.rotate(-this.angle); }
417
499
 
418
500
  /** 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.
501
+ * - Called for each solid tile the physics tests, which can be several times a frame for the same tile, and for
502
+ * positions it only tries, so keep it free of side effects or guard them to once a frame
503
+ * - this.pos has already moved, so a check on where it came from, like a one way platform, needs the position
504
+ * saved in update, as the platformer example does
419
505
  * @param {number} tileData - the value of the tile at the position
420
- * @param {Vector2} pos - tile where the collision occurred
506
+ * @param {Vector2} pos - the tile's bottom left corner in world space
421
507
  * @return {boolean} - true if the collision should be resolved by modifying it's position and velocity */
422
508
  collideWithTile(tileData, pos) { return tileData > 0; }
423
509
 
424
510
  /** 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.
511
+ * - Both objects of a touching pair are asked once a frame, whichever order they update in; an object that
512
+ * destroys itself here is gone at the end of the frame and is still asked about the pairs left this frame, so a
513
+ * bullet that should hit one thing checks its own destroyed flag first
425
514
  * @param {EngineObject} object - the object to test against
426
- * @param {Object} [push] - what it would take to move this object clear, a Vector3 from the 3D plugin, undefined in 2D
515
+ * @param {Vector3} [push] - what it would take to move this object clear, a Vector3 from the 3D plugin, undefined in 2D
427
516
  * @return {boolean} - true if the collision should be resolved by modifying it's position and velocity
428
517
  */
429
518
  collideWithObject(object, push) { return true; }
@@ -447,16 +536,19 @@ class EngineObject
447
536
  getSpeed() { return this.velocity.length(); }
448
537
 
449
538
  /** Apply acceleration to this object (adjust velocity, not affected by mass)
539
+ * - Does nothing on a static object (mass 0), set its velocity instead
450
540
  * @param {Vector2} acceleration */
451
541
  applyAcceleration(acceleration)
452
542
  { if (this.mass) this.velocity = this.velocity.add(acceleration); }
453
543
 
454
544
  /** Apply angular acceleration to this object
545
+ * - Does nothing on a static object (mass 0), set its angleVelocity instead
455
546
  * @param {number} acceleration */
456
547
  applyAngularAcceleration(acceleration)
457
548
  { if (this.mass) this.angleVelocity += acceleration; }
458
549
 
459
550
  /** Apply force to this object (adjust velocity, affected by mass)
551
+ * - Does nothing on a static object (mass 0), set its velocity instead
460
552
  * @param {Vector2} force */
461
553
  applyForce(force)
462
554
  { if (this.mass) this.applyAcceleration(force.scale(1/this.mass)); }
@@ -474,9 +566,11 @@ class EngineObject
474
566
  {
475
567
  ASSERT(!this.destroyed, 'cannot add child to destroyed object');
476
568
  if (this.destroyed) return child;
477
- ASSERT(!child.parent && !this.children.includes(child));
569
+ ASSERT(!child.parent && !this.children.includes(child), 'child already has a parent, removeChild it first or use attach');
478
570
  ASSERT(child instanceof EngineObject, 'child must be an EngineObject');
479
- ASSERT(child !== this, 'cannot add self as child');
571
+ ASSERT(!child.destroyed, 'cannot add a destroyed child');
572
+ for (let p = /** @type {EngineObject} */ (this); p; p = p.parent)
573
+ ASSERT(p !== child, 'cannot add an object as a child of itself or of its own child');
480
574
  this.children.push(child);
481
575
  child.parent = this;
482
576
  child.localPos = localPos.copy();
@@ -485,17 +579,33 @@ class EngineObject
485
579
  return child;
486
580
  }
487
581
 
488
- /** Removes a child from this one
582
+ /** Attaches a child to this without moving it: the local transform is worked out from where the child is now,
583
+ * where addChild takes one; a child of something else is moved over, returns child for chaining
584
+ * @param {EngineObject} child
585
+ * @return {EngineObject} The child object attached */
586
+ attach(child)
587
+ {
588
+ ASSERT(child instanceof EngineObject, 'child must be an EngineObject');
589
+ ASSERT(child !== this, 'cannot attach self');
590
+ child.parent?.removeChild(child);
591
+ // the local values that updateTransforms turns back into the child's current pos and angle
592
+ const mirror = this.getMirrorSign(), local = this.worldToLocal(child.pos);
593
+ local.x *= mirror;
594
+ return this.addChild(child, local, mirror * (child.angle - this.angle));
595
+ }
596
+
597
+ /** Removes a child from this one, it stays where it is in the world
489
598
  * @param {EngineObject} child */
490
599
  removeChild(child)
491
600
  {
492
601
  ASSERT(child.parent === this && this.children.includes(child));
493
- this.children.splice(this.children.indexOf(child), 1);
494
- child.parent = undefined;
602
+ const i = this.children.indexOf(child);
603
+ if (i < 0) return; // not a child of this one, release has no assert
604
+ this.children.splice(i, 1);
605
+ child.parent = child.localPos = undefined;
495
606
  }
496
607
 
497
- /** Check if overlapping another engine object
498
- * Collisions are resolved to prevent overlaps
608
+ /** Check if this object's box overlaps another object's box
499
609
  * @param {EngineObject} object
500
610
  * @return {boolean} */
501
611
  isOverlappingObject(object)
@@ -511,18 +621,23 @@ class EngineObject
511
621
  /** Set how this object collides
512
622
  * @param {boolean} [collideSolidObjects] - Does it collide with solid objects?
513
623
  * @param {boolean} [isSolid] - Does it collide with and block other objects? (expensive in large numbers)
514
- * @param {boolean} [collideTiles] - Does it collide with the tile collision?
624
+ * @param {boolean} [collideLevel] - Does it collide with the level, its tile collision layers?
515
625
  * @param {boolean} [collideRaycast] - Does it collide with raycasts? */
516
- setCollision(collideSolidObjects=true, isSolid=true, collideTiles=true, collideRaycast=true)
626
+ setCollision(collideSolidObjects=true, isSolid=true, collideLevel=true, collideRaycast=true)
517
627
  {
518
628
  ASSERT(collideSolidObjects || !isSolid, 'solid objects must be set to collide');
519
629
 
520
630
  this.collideSolidObjects = collideSolidObjects;
521
631
  this.isSolid = isSolid;
522
- this.collideTiles = collideTiles;
632
+ this.collideLevel = collideLevel;
523
633
  this.collideRaycast = collideRaycast;
524
634
  }
525
635
 
636
+ /** @deprecated since 1.20, use collideLevel
637
+ * @type {boolean} */
638
+ get collideTiles() { return this.collideLevel; }
639
+ set collideTiles(collide) { this.collideLevel = collide; }
640
+
526
641
  /** Returns string containing info about this object for debugging
527
642
  * @return {string} */
528
643
  toString()
@@ -536,8 +651,7 @@ class EngineObject
536
651
  text += '\nsize = ' + this.size;
537
652
  if (this.angle)
538
653
  text += '\nangle = ' + this.angle.toFixed(3);
539
- if (this.color)
540
- text += '\ncolor = ' + this.color;
654
+ text += '\ncolor = ' + this.color;
541
655
  return text;
542
656
  }
543
657
 
@@ -547,15 +661,15 @@ class EngineObject
547
661
  if (!debug) return;
548
662
 
549
663
  // check if there is anything to show
550
- const hasPhysics = this.collideTiles || this.collideSolidObjects || this.isSolid;
664
+ const hasPhysics = this.collideLevel || this.collideSolidObjects || this.isSolid;
551
665
  if (!hasPhysics && !this.parent) return;
552
666
 
553
667
  // show object info for debugging
554
668
  const size = vec2(max(this.size.x, .2), max(this.size.y, .2));
555
- const color = rgb(this.collideTiles?1:0, this.collideSolidObjects?1:0, this.isSolid?1:0, .5);
556
- debugRect(this.pos, size, color, 0, this.angle, hasPhysics);
669
+ const color = rgb(this.collideLevel?1:0, this.collideSolidObjects?1:0, this.isSolid?1:0, .5);
670
+ debugRect(this.pos, size, color, 0, hasPhysics ? 0 : this.angle, hasPhysics, false); // collision ignores the angle
557
671
  if (this.parent)
558
- debugRect(this.pos, size.scale(.8), rgb(1,1,1,.5), 0, this.angle);
559
- this.parent && debugLine(this.pos, this.parent.pos, rgb(1,1,1,.5), .5);
672
+ debugRect(this.pos, size.scale(.8), rgb(1,1,1,.5), 0, this.angle, false, false);
673
+ this.parent && debugLine(this.pos, this.parent.pos, rgb(1,1,1,.5), .5, 0, false);
560
674
  }
561
675
  }