littlejsengine 1.19.3 → 1.21.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 (54) hide show
  1. package/FAQ.md +841 -820
  2. package/README.md +29 -25
  3. package/dist/littlejs.d.ts +2857 -955
  4. package/dist/littlejs.esm.js +33428 -20995
  5. package/dist/littlejs.esm.min.js +4 -3
  6. package/dist/littlejs.js +33386 -20996
  7. package/dist/littlejs.min.js +2 -2
  8. package/dist/littlejs.release.js +27841 -20422
  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 +786 -0
  14. package/plugins/jsconfig.json +19 -0
  15. package/plugins/lightSystem.js +719 -332
  16. package/plugins/math3d.js +148 -27
  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 +25 -0
  21. package/plugins/postProcess.js +96 -24
  22. package/plugins/render3d.js +2067 -1632
  23. package/plugins/render3dDebug.js +110 -0
  24. package/plugins/render3dEditor.js +1812 -0
  25. package/plugins/render3dExtras.js +1948 -0
  26. package/plugins/render3dLevel.js +189 -0
  27. package/plugins/render3dVoxels.js +514 -0
  28. package/plugins/sceneSystem.js +58 -0
  29. package/plugins/textureSheet.js +496 -458
  30. package/plugins/threejs.js +183 -163
  31. package/plugins/tweakables.js +411 -0
  32. package/plugins/tweenSystem.js +265 -145
  33. package/plugins/uiSystem.js +2024 -1687
  34. package/plugins/zzfxm.js +159 -159
  35. package/src/engine.js +378 -97
  36. package/src/engineAudio.js +231 -127
  37. package/src/engineBuild.mjs +292 -256
  38. package/src/engineDebug.js +967 -743
  39. package/src/engineDraw.js +391 -198
  40. package/src/engineEditor.js +2452 -0
  41. package/src/engineExport.js +18 -0
  42. package/src/engineInput.js +1466 -1293
  43. package/src/engineLogo.js +19 -0
  44. package/src/engineMath.js +1274 -1252
  45. package/src/engineObject.js +197 -82
  46. package/src/engineParticles.js +583 -532
  47. package/src/engineRelease.js +79 -45
  48. package/src/engineSettings.js +115 -29
  49. package/src/engineTileLayer.js +995 -741
  50. package/src/engineUtilities.js +19 -13
  51. package/src/engineWebGL.js +239 -82
  52. package/src/jsconfig.json +17 -13
  53. package/plugins/box2d.wasm.js +0 -630
  54. 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. */
@@ -167,15 +188,21 @@ class EngineObject
167
188
  // bail if a collision callback destroyed us mid-frame
168
189
  if (this.destroyed) return;
169
190
 
170
- if (this.clampSpeed)
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))
171
194
  {
172
- // limit max speed to prevent missing collisions
173
195
  this.velocity.x = clamp(this.velocity.x, -objectMaxSpeed, objectMaxSpeed);
174
196
  this.velocity.y = clamp(this.velocity.y, -objectMaxSpeed, objectMaxSpeed);
175
197
  }
176
198
 
177
- // apply physics
178
- const oldPos = this.pos.copy();
199
+ // physics sanity checks
200
+ ASSERT(this.angleDamping >= 0 && this.angleDamping <= 1, 'angleDamping must be 0 to 1, the fraction kept each frame');
201
+ ASSERT(this.damping >= 0 && this.damping <= 1, 'damping must be 0 to 1, the fraction of velocity kept each frame');
202
+
203
+ // apply physics; only the solver needs where the object was, so only then is it copied
204
+ const solve = enablePhysicsSolver && this.mass;
205
+ const oldPos = solve ? this.pos.copy() : undefined;
179
206
  this.velocity.x *= this.damping;
180
207
  this.velocity.y *= this.damping;
181
208
  if (this.mass)
@@ -188,14 +215,12 @@ class EngineObject
188
215
  this.pos.y += this.velocity.y;
189
216
  this.angle += this.angleVelocity *= this.angleDamping;
190
217
 
191
- // physics sanity checks
192
- ASSERT(this.angleDamping >= 0 && this.angleDamping <= 1);
193
- ASSERT(this.damping >= 0 && this.damping <= 1);
194
-
195
218
  // don't do collision for static objects or if solver disabled
196
- if (!enablePhysicsSolver || !this.mass) return;
219
+ if (!solve) return;
197
220
 
198
- const wasFalling = this.velocity.y < 0 && gravity.y < 0 || this.velocity.y > 0 && gravity.y > 0;
221
+ // which way is down for this object, a negative gravityScale falls up and lands on ceilings
222
+ const gravityY = this.gravityScale < 0 ? -gravity.y : gravity.y;
223
+ const wasFalling = this.velocity.y < 0 && gravityY < 0 || this.velocity.y > 0 && gravityY > 0;
199
224
  if (this.groundObject)
200
225
  {
201
226
  // apply friction in local space of ground object
@@ -210,7 +235,7 @@ class EngineObject
210
235
  {
211
236
  // check collisions against solid objects
212
237
  const epsilon = .001; // necessary to push slightly outside of the collision
213
- for (const o of engineObjectsCollide)
238
+ for (const o of engineObjectsCollideStaticLast)
214
239
  {
215
240
  // skip destroyed, child objects, self collision, or objects with no box
216
241
  if (o.destroyed || o.parent || o === this || !o.size.x || !o.size.y) continue;
@@ -221,29 +246,76 @@ class EngineObject
221
246
  // check collision
222
247
  if (!this.isOverlappingObject(o)) continue;
223
248
 
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;
249
+ // each moving object checks its own contacts, so a pair the other one already asked about this frame
250
+ // is not asked twice: left overlapping, ignored or only nudged apart it is skipped, and one both said
251
+ // to resolve, pushed back together since, is resolved again
252
+ const answer = engineObjectsCollidePairAnswer(o, this);
253
+ if (answer === false) continue;
254
+ if (!answer)
255
+ {
256
+ // notify objects of collision and check if should be resolved
257
+ const collide1 = this.collideWithObject(o);
258
+ const collide2 = o.collideWithObject(this);
259
+ if (!collide1 || !collide2)
260
+ {
261
+ engineObjectsCollidePairAdd(this, o);
262
+ continue;
263
+ }
264
+ }
265
+
266
+ const wasOverlapping = isOverlapping(oldPos, this.size, o.pos, o.size);
267
+ if (wasOverlapping && (!o.mass || o.groundObject))
268
+ {
269
+ // a static solid that moved into it, like a door or an elevator, pushes it out the shortest way
270
+ // at once and carries it along, it would only drift out slowly and the solid would pass through;
271
+ // an object standing on something counts as fixed too, so a stack on an elevator rides together;
272
+ // it bounces off relative to the mover, a paddle moved by setting pos bounces a ball as a wall does
273
+ const push = collideBoxBox(this.pos, this.size, o.pos, o.size);
274
+ if (push)
275
+ {
276
+ this.pos.x += push.x + sign(push.x) * epsilon;
277
+ this.pos.y += push.y + sign(push.y) * epsilon;
278
+ const restitution = max(this.restitution, o.restitution);
279
+ if (push.x)
280
+ {
281
+ const v = this.velocity.x - o.velocity.x;
282
+ if (v * push.x < 0) // moving into it
283
+ this.velocity.x = o.velocity.x - v * restitution;
284
+ }
285
+ else
286
+ {
287
+ const v = this.velocity.y - o.velocity.y;
288
+ if (v * push.y < 0) // moving into it
289
+ this.velocity.y = o.velocity.y - v * restitution;
290
+ if (push.y * gravityY < 0) // pushed up against gravity, it stands on it
291
+ this.groundObject = o;
292
+ }
293
+ }
294
+ engineObjectsCollidePairAdd(this, o);
228
295
 
229
- if (isOverlapping(oldPos, this.size, o.pos, o.size))
296
+ debugPhysics && debugOverlap(this.pos, this.size, o.pos, o.size, '#f00', undefined, false);
297
+ continue;
298
+ }
299
+ if (wasOverlapping)
230
300
  {
231
301
  // if already was touching, try to push away
232
302
  const deltaPos = oldPos.subtract(o.pos);
233
303
  const length = deltaPos.length();
234
304
  const pushAwayAccel = .001;
235
- const velocity = length < .001 ? vec2(0,1) : deltaPos.scale(pushAwayAccel/length);
305
+ const velocity = length < .001 ? vec2(0, pushAwayAccel) : deltaPos.scale(pushAwayAccel/length);
236
306
  this.velocity = this.velocity.add(velocity);
237
307
  if (o.mass) // push away other object if not fixed
238
308
  o.velocity = o.velocity.subtract(velocity);
309
+ engineObjectsCollidePairAdd(this, o);
239
310
 
240
- debugPhysics && debugOverlap(this.pos, this.size, o.pos, o.size, '#f00');
311
+ debugPhysics && debugOverlap(this.pos, this.size, o.pos, o.size, '#f00', undefined, false);
241
312
  continue;
242
313
  }
243
314
 
244
315
  // check for collision
245
316
  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
317
+ // prefer to push up if small delta, up away from this object's own gravity
318
+ const smallStepUp = (gravityY > 0 ? o.pos.y - oldPos.y : oldPos.y - o.pos.y)*2 > sizeBoth.y - abs(gravityY);
247
319
  const isBlockedX = abs(oldPos.y - o.pos.y)*2 < sizeBoth.y;
248
320
  const isBlockedY = abs(oldPos.x - o.pos.x)*2 < sizeBoth.x;
249
321
  const restitution = max(this.restitution, o.restitution);
@@ -258,10 +330,11 @@ class EngineObject
258
330
  if (wasFalling)
259
331
  this.groundObject = o;
260
332
 
261
- // bounce if other object is fixed or grounded
262
- this.velocity.y *= -restitution;
333
+ // bounce if other object is fixed or grounded, relative to it so a rider keeps up with a
334
+ // platform moving down instead of landing on it again every few frames
335
+ this.velocity.y = o.velocity.y - (this.velocity.y - o.velocity.y) * restitution;
263
336
  }
264
- else if (o.mass)
337
+ else // o has mass here, the other branch already handled the massless case
265
338
  {
266
339
  // inelastic collision
267
340
  const inelastic = (this.mass * this.velocity.y + o.mass * o.velocity.y) / (this.mass + o.mass);
@@ -296,13 +369,14 @@ class EngineObject
296
369
  this.velocity.x = lerp(inelastic, elastic0, restitution);
297
370
  o.velocity.x = lerp(inelastic, elastic1, restitution);
298
371
  }
299
- else // bounce if other object is fixed
300
- this.velocity.x *= -restitution;
372
+ else // bounce if other object is fixed, relative to it as a landing is
373
+ this.velocity.x = o.velocity.x - (this.velocity.x - o.velocity.x) * restitution;
301
374
  }
302
- debugPhysics && debugOverlap(this.pos, this.size, o.pos, o.size, '#f0f');
375
+ engineObjectsCollidePairAdd(this, o, true);
376
+ debugPhysics && debugOverlap(this.pos, this.size, o.pos, o.size, '#f0f', undefined, false);
303
377
  }
304
378
  }
305
- if (this.collideTiles)
379
+ if (this.collideLevel)
306
380
  {
307
381
  // check collision against tiles
308
382
  const hitLayer = tileCollisionTest(this.pos, this.size, this);
@@ -318,21 +392,17 @@ class EngineObject
318
392
  const restitution = max(this.restitution, hitLayer.restitution);
319
393
  if (isBlockedX)
320
394
  {
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)
395
+ // a ledge caught less than maxMove below its top lifts the object onto it instead of stopping it,
396
+ // down off a ceiling ledge when gravity points up; zero gravity counts as down
324
397
  const epsilon = 1e-3;
325
398
  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))
399
+ const y = gravityY > 0 ?
400
+ ceil( oldPos.y+this.size.y/2-1) - this.size.y/2 - epsilon :
401
+ floor(oldPos.y-this.size.y/2+1) + this.size.y/2 + epsilon;
402
+ if (abs(y - this.pos.y) < maxMove && !tileCollisionTest(vec2(this.pos.x, y), this.size, this))
333
403
  {
334
404
  this.pos.y = y;
335
- debugPhysics && debugRect(this.pos, this.size, '#ff0');
405
+ debugPhysics && debugRect(this.pos, this.size, '#ff0', 0, 0, false, false);
336
406
  return;
337
407
  }
338
408
 
@@ -348,9 +418,12 @@ class EngineObject
348
418
  // this prevents gap between object and ground
349
419
  const epsilon = .0001;
350
420
  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;
421
+ // rounded in the layer's space as its collision test is, or a bottom a hair below a
422
+ // grid line would round to the row under it, inside the floor, and fall through
423
+ const layerY = hitLayer.pos.y;
424
+ this.pos.y = layerY + (gravityY < 0 ?
425
+ floor(oldPos.y - layerY - this.size.y/2) + offset :
426
+ ceil( oldPos.y - layerY + this.size.y/2) - offset);
354
427
 
355
428
  // set ground object for tile collision
356
429
  this.groundObject = hitLayer;
@@ -364,7 +437,7 @@ class EngineObject
364
437
  // bounce velocity
365
438
  this.velocity.y *= -restitution;
366
439
  }
367
- debugPhysics && debugRect(this.pos, this.size, '#f00');
440
+ debugPhysics && debugRect(this.pos, this.size, '#f00', 0, 0, false, false);
368
441
  }
369
442
  }
370
443
  }
@@ -377,14 +450,24 @@ class EngineObject
377
450
  render()
378
451
  {
379
452
  // default object render
380
- drawTile(this.pos, this.drawSize || this.size, this.tileInfo, this.color, this.angle, this.mirror, this.additiveColor);
453
+ drawTile(this.pos, this.drawSize || this.size, this.tileInfo, this.color, this.angle, this.mirror, this.additiveColor, glEnable, false);
381
454
  }
382
455
 
383
456
  /** Optional hook called during the light system plugin's lightmap pass to draw this object's lightmap contribution. Does nothing by default. */
384
457
  renderLight() {}
385
458
 
459
+ /** Draw this object into the light system's shadow map, called during its shadow pass when castShadow is set.
460
+ * 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
+ * screen space WebGL draws in render() are skipped during the pass */
462
+ renderShadow() { this.render(); }
463
+
464
+ /** Draw this object's shape into the light system's lightmap, called during its light pass when emissive is above
465
+ * 0; what it draws shows in its own colors that much brighter. Calls render() by default so the whole object
466
+ * glows; override to glow a part, like a robot's eyes */
467
+ renderEmissive() { this.render(); }
468
+
386
469
  /** 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? */
470
+ * @param {boolean} [immediate] - true removes attached effects like particle emitters at once, false lets them finish first */
388
471
  destroy(immediate=false)
389
472
  {
390
473
  if (this.destroyed) return;
@@ -416,14 +499,21 @@ class EngineObject
416
499
  worldToLocalVector(vec) { return vec.rotate(-this.angle); }
417
500
 
418
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.
502
+ * - Called for each solid tile the physics tests, which can be several times a frame for the same tile, and for
503
+ * positions it only tries, so keep it free of side effects or guard them to once a frame
504
+ * - this.pos has already moved, so a check on where it came from, like a one way platform, needs the position
505
+ * saved in update, as the platformer example does
419
506
  * @param {number} tileData - the value of the tile at the position
420
- * @param {Vector2} pos - tile where the collision occurred
507
+ * @param {Vector2} pos - the tile's bottom left corner in world space
421
508
  * @return {boolean} - true if the collision should be resolved by modifying it's position and velocity */
422
509
  collideWithTile(tileData, pos) { return tileData > 0; }
423
510
 
424
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.
512
+ * - Both objects of a touching pair are asked once a frame, whichever order they update in; an object that
513
+ * destroys itself here is gone at the end of the frame and is still asked about the pairs left this frame, so a
514
+ * bullet that should hit one thing checks its own destroyed flag first
425
515
  * @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
516
+ * @param {Vector3} [push] - what it would take to move this object clear, a Vector3 from the 3D plugin, undefined in 2D
427
517
  * @return {boolean} - true if the collision should be resolved by modifying it's position and velocity
428
518
  */
429
519
  collideWithObject(object, push) { return true; }
@@ -447,16 +537,19 @@ class EngineObject
447
537
  getSpeed() { return this.velocity.length(); }
448
538
 
449
539
  /** Apply acceleration to this object (adjust velocity, not affected by mass)
540
+ * - Does nothing on a static object (mass 0), set its velocity instead
450
541
  * @param {Vector2} acceleration */
451
542
  applyAcceleration(acceleration)
452
543
  { if (this.mass) this.velocity = this.velocity.add(acceleration); }
453
544
 
454
545
  /** Apply angular acceleration to this object
546
+ * - Does nothing on a static object (mass 0), set its angleVelocity instead
455
547
  * @param {number} acceleration */
456
548
  applyAngularAcceleration(acceleration)
457
549
  { if (this.mass) this.angleVelocity += acceleration; }
458
550
 
459
551
  /** Apply force to this object (adjust velocity, affected by mass)
552
+ * - Does nothing on a static object (mass 0), set its velocity instead
460
553
  * @param {Vector2} force */
461
554
  applyForce(force)
462
555
  { if (this.mass) this.applyAcceleration(force.scale(1/this.mass)); }
@@ -474,9 +567,11 @@ class EngineObject
474
567
  {
475
568
  ASSERT(!this.destroyed, 'cannot add child to destroyed object');
476
569
  if (this.destroyed) return child;
477
- ASSERT(!child.parent && !this.children.includes(child));
570
+ ASSERT(!child.parent && !this.children.includes(child), 'child already has a parent, removeChild it first or use attach');
478
571
  ASSERT(child instanceof EngineObject, 'child must be an EngineObject');
479
- ASSERT(child !== this, 'cannot add self as child');
572
+ ASSERT(!child.destroyed, 'cannot add a destroyed child');
573
+ for (let p = /** @type {EngineObject} */ (this); p; p = p.parent)
574
+ ASSERT(p !== child, 'cannot add an object as a child of itself or of its own child');
480
575
  this.children.push(child);
481
576
  child.parent = this;
482
577
  child.localPos = localPos.copy();
@@ -485,17 +580,33 @@ class EngineObject
485
580
  return child;
486
581
  }
487
582
 
488
- /** Removes a child from this one
583
+ /** Attaches a child to this without moving it: the local transform is worked out from where the child is now,
584
+ * where addChild takes one; a child of something else is moved over, returns child for chaining
585
+ * @param {EngineObject} child
586
+ * @return {EngineObject} The child object attached */
587
+ attach(child)
588
+ {
589
+ ASSERT(child instanceof EngineObject, 'child must be an EngineObject');
590
+ ASSERT(child !== this, 'cannot attach self');
591
+ child.parent?.removeChild(child);
592
+ // the local values that updateTransforms turns back into the child's current pos and angle
593
+ const mirror = this.getMirrorSign(), local = this.worldToLocal(child.pos);
594
+ local.x *= mirror;
595
+ return this.addChild(child, local, mirror * (child.angle - this.angle));
596
+ }
597
+
598
+ /** Removes a child from this one, it stays where it is in the world
489
599
  * @param {EngineObject} child */
490
600
  removeChild(child)
491
601
  {
492
602
  ASSERT(child.parent === this && this.children.includes(child));
493
- this.children.splice(this.children.indexOf(child), 1);
494
- child.parent = undefined;
603
+ const i = this.children.indexOf(child);
604
+ if (i < 0) return; // not a child of this one, release has no assert
605
+ this.children.splice(i, 1);
606
+ child.parent = child.localPos = undefined;
495
607
  }
496
608
 
497
- /** Check if overlapping another engine object
498
- * Collisions are resolved to prevent overlaps
609
+ /** Check if this object's box overlaps another object's box
499
610
  * @param {EngineObject} object
500
611
  * @return {boolean} */
501
612
  isOverlappingObject(object)
@@ -511,18 +622,23 @@ class EngineObject
511
622
  /** Set how this object collides
512
623
  * @param {boolean} [collideSolidObjects] - Does it collide with solid objects?
513
624
  * @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?
625
+ * @param {boolean} [collideLevel] - Does it collide with the level, its tile collision layers?
515
626
  * @param {boolean} [collideRaycast] - Does it collide with raycasts? */
516
- setCollision(collideSolidObjects=true, isSolid=true, collideTiles=true, collideRaycast=true)
627
+ setCollision(collideSolidObjects=true, isSolid=true, collideLevel=true, collideRaycast=true)
517
628
  {
518
629
  ASSERT(collideSolidObjects || !isSolid, 'solid objects must be set to collide');
519
630
 
520
631
  this.collideSolidObjects = collideSolidObjects;
521
632
  this.isSolid = isSolid;
522
- this.collideTiles = collideTiles;
633
+ this.collideLevel = collideLevel;
523
634
  this.collideRaycast = collideRaycast;
524
635
  }
525
636
 
637
+ /** @deprecated since 1.20, use collideLevel
638
+ * @type {boolean} */
639
+ get collideTiles() { return this.collideLevel; }
640
+ set collideTiles(collide) { this.collideLevel = collide; }
641
+
526
642
  /** Returns string containing info about this object for debugging
527
643
  * @return {string} */
528
644
  toString()
@@ -536,8 +652,7 @@ class EngineObject
536
652
  text += '\nsize = ' + this.size;
537
653
  if (this.angle)
538
654
  text += '\nangle = ' + this.angle.toFixed(3);
539
- if (this.color)
540
- text += '\ncolor = ' + this.color;
655
+ text += '\ncolor = ' + this.color;
541
656
  return text;
542
657
  }
543
658
 
@@ -547,15 +662,15 @@ class EngineObject
547
662
  if (!debug) return;
548
663
 
549
664
  // check if there is anything to show
550
- const hasPhysics = this.collideTiles || this.collideSolidObjects || this.isSolid;
665
+ const hasPhysics = this.collideLevel || this.collideSolidObjects || this.isSolid;
551
666
  if (!hasPhysics && !this.parent) return;
552
667
 
553
668
  // show object info for debugging
554
669
  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);
670
+ const color = rgb(this.collideLevel?1:0, this.collideSolidObjects?1:0, this.isSolid?1:0, .5);
671
+ debugRect(this.pos, size, color, 0, hasPhysics ? 0 : this.angle, hasPhysics, false); // collision ignores the angle
557
672
  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);
673
+ debugRect(this.pos, size.scale(.8), rgb(1,1,1,.5), 0, this.angle, false, false);
674
+ this.parent && debugLine(this.pos, this.parent.pos, rgb(1,1,1,.5), .5, 0, false);
560
675
  }
561
676
  }