emeraldengine 3.0.0 → 3.1.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 (103) hide show
  1. package/README.md +1498 -1659
  2. package/dist/types/index.d.ts +4 -1
  3. package/dist/types/src/Animator.d.ts +1 -1
  4. package/dist/types/src/CollisionLayers.d.ts +2 -2
  5. package/dist/types/src/Color.d.ts +1 -0
  6. package/dist/types/src/Drawable.d.ts +1 -1
  7. package/dist/types/src/EmeraldDB.d.ts +2 -2
  8. package/dist/types/src/InstancedTexture.d.ts +17 -2
  9. package/dist/types/src/Material.d.ts +2 -2
  10. package/dist/types/src/MathUtils.d.ts +2 -1
  11. package/dist/types/src/ParticleEmitter.d.ts +1 -1
  12. package/dist/types/src/Physics.d.ts +148 -18
  13. package/dist/types/src/Scene.d.ts +1 -1
  14. package/dist/types/src/Shaders.d.ts +2 -2
  15. package/dist/types/src/Tilemap.d.ts +1 -1
  16. package/dist/types/src/UI.d.ts +1 -1
  17. package/dist/types/src/components/Behaviour.d.ts +2 -2
  18. package/dist/types/src/components/Collider.d.ts +7 -1
  19. package/dist/types/src/components/GameObject.d.ts +2 -2
  20. package/dist/types/src/components/PolygonCollider.d.ts +33 -0
  21. package/dist/types/src/components/RigidBody.d.ts +281 -8
  22. package/dist/types/src/importers/Aseprite.d.ts +2 -2
  23. package/dist/types/src/importers/ForgeLevel.d.ts +97 -0
  24. package/dist/types/src/importers/TiledMap.d.ts +1 -1
  25. package/dist/types/src/managers/EventManager.d.ts +1 -1
  26. package/dist/types/src/managers/Gamepad.d.ts +102 -0
  27. package/dist/types/src/managers/InputManager.d.ts +93 -2
  28. package/dist/types/src/managers/NetworkManager.d.ts +2 -2
  29. package/dist/types/src/managers/RenderStats.d.ts +1 -1
  30. package/dist/types/src/managers/TextureManager.d.ts +1 -1
  31. package/dist/types/src/physics/AABB.d.ts +92 -0
  32. package/dist/types/src/physics/Body.d.ts +435 -0
  33. package/dist/types/src/physics/BodyType.d.ts +6 -0
  34. package/dist/types/src/physics/BroadPhase.d.ts +210 -0
  35. package/dist/types/src/physics/Collision.d.ts +102 -0
  36. package/dist/types/src/physics/Contact.d.ts +206 -0
  37. package/dist/types/src/physics/ContactSolver.d.ts +108 -0
  38. package/dist/types/src/physics/Distance.d.ts +54 -0
  39. package/dist/types/src/physics/DistanceJoint.d.ts +90 -0
  40. package/dist/types/src/physics/Fixture.d.ts +221 -0
  41. package/dist/types/src/physics/Island.d.ts +52 -0
  42. package/dist/types/src/physics/Joint.d.ts +59 -0
  43. package/dist/types/src/physics/Math2D.d.ts +371 -0
  44. package/dist/types/src/physics/RevoluteJoint.d.ts +119 -0
  45. package/dist/types/src/physics/Settings.d.ts +22 -0
  46. package/dist/types/src/physics/Shapes.d.ts +207 -0
  47. package/dist/types/src/physics/TimeOfImpact.d.ts +22 -0
  48. package/dist/types/src/physics/World.d.ts +274 -0
  49. package/dist/types/src/physics/index.d.ts +34 -0
  50. package/index.js +6 -0
  51. package/package.json +2 -3
  52. package/src/Animator.js +1 -1
  53. package/src/CollisionLayers.js +3 -3
  54. package/src/Color.js +8 -0
  55. package/src/Drawable.js +1 -1
  56. package/src/Emerald.js +1 -1
  57. package/src/EmeraldDB.js +2 -2
  58. package/src/InstancedTexture.js +57 -9
  59. package/src/Material.js +2 -2
  60. package/src/MathUtils.js +2 -1
  61. package/src/ParticleEmitter.js +1 -1
  62. package/src/Physics.js +270 -60
  63. package/src/Scene.js +1 -1
  64. package/src/Shaders.js +20 -20
  65. package/src/Tilemap.js +1 -1
  66. package/src/UI.js +1 -1
  67. package/src/components/Behaviour.js +2 -2
  68. package/src/components/BoxCollider.js +7 -9
  69. package/src/components/BoxColliderDebug.js +3 -4
  70. package/src/components/CircleCollider.js +7 -9
  71. package/src/components/CircleColliderDebug.js +3 -2
  72. package/src/components/Collider.js +13 -3
  73. package/src/components/GameObject.js +2 -2
  74. package/src/components/PolygonCollider.js +55 -0
  75. package/src/components/RigidBody.js +441 -14
  76. package/src/importers/Aseprite.js +2 -2
  77. package/src/importers/ForgeLevel.js +581 -0
  78. package/src/importers/TiledMap.js +1 -1
  79. package/src/managers/EventManager.js +1 -1
  80. package/src/managers/Gamepad.js +126 -0
  81. package/src/managers/InputManager.js +129 -3
  82. package/src/managers/NetworkManager.js +2 -2
  83. package/src/managers/RenderStats.js +1 -1
  84. package/src/managers/TextureManager.js +1 -1
  85. package/src/physics/AABB.js +207 -0
  86. package/src/physics/Body.js +862 -0
  87. package/src/physics/BodyType.js +16 -0
  88. package/src/physics/BroadPhase.js +641 -0
  89. package/src/physics/Collision.js +534 -0
  90. package/src/physics/Contact.js +500 -0
  91. package/src/physics/ContactSolver.js +526 -0
  92. package/src/physics/Distance.js +403 -0
  93. package/src/physics/DistanceJoint.js +227 -0
  94. package/src/physics/Fixture.js +346 -0
  95. package/src/physics/Island.js +203 -0
  96. package/src/physics/Joint.js +78 -0
  97. package/src/physics/Math2D.js +573 -0
  98. package/src/physics/RevoluteJoint.js +278 -0
  99. package/src/physics/Settings.js +78 -0
  100. package/src/physics/Shapes.js +549 -0
  101. package/src/physics/TimeOfImpact.js +87 -0
  102. package/src/physics/World.js +731 -0
  103. package/src/physics/index.js +79 -0
package/src/Physics.js CHANGED
@@ -1,4 +1,4 @@
1
- import * as planck from "planck";
1
+ import { World, Box, Vec2 as PhysicsVec2 } from "./physics/index.js";
2
2
 
3
3
  /**
4
4
  * @function dispatchCollision
@@ -23,48 +23,75 @@ function dispatchCollision(object, other, contact, type) {
23
23
 
24
24
  /**
25
25
  * @class Physics
26
- * @description Represents the physics engine
26
+ * @description The game-facing front end of Emerald's own rigid-body engine
27
+ * (see `src/physics`). It owns the {@link World}, converts between world
28
+ * (pixel) units and physics units via `scale`, steps the simulation on a fixed
29
+ * timestep, and routes contacts to `onCollisionEnter`/`onCollisionExit` on your
30
+ * objects and {@link Behaviour} components.
27
31
  * @param {number} gravity - The gravity of the physics engine
28
- * @param {number} scale - The scale of the physics engine
29
- * @param {number} velocityThreshold - The velocity threshold of the physics engine
32
+ * @param {number} scale - Pixels per physics unit (meter)
33
+ * @param {number} velocityThreshold - Relative speed below which impacts stop
34
+ * bouncing, which is what lets resting bodies settle instead of jittering
30
35
  */
31
36
  class Physics {
32
37
  constructor(gravity, scale, velocityThreshold = 0.1) {
33
- this.world = new planck.World({
34
- gravity: new planck.Vec2(0, gravity),
38
+ this.world = new World({
39
+ gravity: new PhysicsVec2(0, gravity),
40
+ velocityThreshold,
35
41
  });
36
- planck.Settings.velocityThreshold = velocityThreshold;
37
42
  this.gravity = gravity;
38
43
  this.scale = scale;
39
44
 
40
45
  this.fixedTimeStep = 1 / 60;
41
46
  this.maxSubSteps = 5;
47
+ /** Largest slice the variable mode will simulate in one step. */
48
+ this.maxTimeStep = 1 / 30;
49
+ /** "fixed" or "variable", see {@link Physics#setVariableTimeStep}. */
50
+ this.timeStepMode = "fixed";
51
+ /** Steps taken by the last process() call. */
52
+ this.lastStepCount = 0;
42
53
  /** @private */
43
54
  this._accumulator = 0;
55
+ /** World-level listeners added via onCollisionEnter. @private */
56
+ this._enterCallbacks = [];
57
+ /** World-level listeners added via onCollisionExit. @private */
58
+ this._exitCallbacks = [];
44
59
 
45
60
  this._dispatchContacts();
46
61
  }
47
62
 
48
63
  /**
49
64
  * @method _dispatchContacts
50
- * @description Routes planck contacts to the owning objects so Behaviour
51
- * components receive onCollisionEnter/onCollisionExit. Bodies created through
52
- * RigidBody carry the owner via userData.
65
+ * @description Binds contact routing to the current world: owning objects (so
66
+ * Behaviour components receive onCollisionEnter/onCollisionExit) first, then
67
+ * any world-level listeners. Bodies created through RigidBody carry the owner
68
+ * via userData. Re-run whenever the world is replaced, so subscriptions
69
+ * survive a clear().
53
70
  * @private
54
71
  */
55
72
  _dispatchContacts() {
56
- const fire = (contact, type) => {
57
- const a = contact.getFixtureA().getBody().getUserData();
58
- const b = contact.getFixtureB().getBody().getUserData();
73
+ const fire = (contact, type, callbacks) => {
74
+ const fixtureA = contact.getFixtureA();
75
+ const fixtureB = contact.getFixtureB();
76
+ const bodyA = fixtureA.getBody();
77
+ const bodyB = fixtureB.getBody();
78
+
79
+ const a = bodyA.getUserData();
80
+ const b = bodyB.getUserData();
59
81
  const objA = a && a.parentObject ? a.parentObject : null;
60
82
  const objB = b && b.parentObject ? b.parentObject : null;
61
83
  dispatchCollision(objA, objB, contact, type);
62
84
  dispatchCollision(objB, objA, contact, type);
85
+
86
+ for (const callback of callbacks) callback(bodyA, bodyB, contact);
63
87
  };
88
+
64
89
  this.world.on("begin-contact", (contact) =>
65
- fire(contact, "onCollisionEnter")
90
+ fire(contact, "onCollisionEnter", this._enterCallbacks)
91
+ );
92
+ this.world.on("end-contact", (contact) =>
93
+ fire(contact, "onCollisionExit", this._exitCallbacks)
66
94
  );
67
- this.world.on("end-contact", (contact) => fire(contact, "onCollisionExit"));
68
95
  }
69
96
 
70
97
  /**
@@ -80,8 +107,8 @@ class Physics {
80
107
  const len = Math.hypot(direction.x, direction.y) || 1;
81
108
  const dx = direction.x / len;
82
109
  const dy = direction.y / len;
83
- const p1 = new planck.Vec2(origin.x / scale, origin.y / scale);
84
- const p2 = new planck.Vec2(
110
+ const p1 = new PhysicsVec2(origin.x / scale, origin.y / scale);
111
+ const p2 = new PhysicsVec2(
85
112
  (origin.x + dx * maxDistance) / scale,
86
113
  (origin.y + dy * maxDistance) / scale
87
114
  );
@@ -108,29 +135,84 @@ class Physics {
108
135
  * @returns {Array} - Owning objects at the point
109
136
  */
110
137
  queryPoint(point) {
111
- const p = new planck.Vec2(point.x / this.scale, point.y / this.scale);
138
+ const p = new PhysicsVec2(point.x / this.scale, point.y / this.scale);
112
139
  const results = [];
113
- for (let body = this.world.getBodyList(); body; body = body.getNext()) {
114
- for (let f = body.getFixtureList(); f; f = f.getNext()) {
115
- if (f.testPoint(p)) {
116
- const rb = body.getUserData();
117
- if (rb && rb.parentObject) results.push(rb.parentObject);
118
- break;
119
- }
120
- }
121
- }
140
+ const seen = new Set();
141
+ this.world.queryPoint(p, (fixture) => {
142
+ const body = fixture.getBody();
143
+ if (seen.has(body)) return true;
144
+ seen.add(body);
145
+ const rb = body.getUserData();
146
+ if (rb && rb.parentObject) results.push(rb.parentObject);
147
+ return true;
148
+ });
122
149
  return results;
123
150
  }
124
151
 
125
152
  /**
126
153
  * @method setFixedTimeStep
127
- * @description Sets the fixed physics step (seconds) and optional substep cap.
154
+ * @description Runs the simulation in constant-size slices, independent of
155
+ * the frame rate: `process()` banks the elapsed time and steps as many whole
156
+ * slices as fit. This is the default and the safe choice: the same inputs
157
+ * produce the same result on every machine, and a slow frame can't destabilise
158
+ * the solver.
159
+ *
160
+ * The cost is that motion updates at the step rate, not the display rate. On
161
+ * a 120Hz screen with a 1/60 step, every simulated position is shown for two
162
+ * frames, so anything that moves in the render frame (a smoothly lerped
163
+ * camera, for instance) will slide against sprites that only move every other
164
+ * frame. Either drive those from the same fixed step, or use
165
+ * {@link Physics#setVariableTimeStep}.
166
+ *
128
167
  * @param {number} step - Fixed step in seconds (e.g. 1/60)
129
168
  * @param {number} [maxSubSteps] - Max steps per process() call (spiral guard)
169
+ * @returns {Physics} - this
130
170
  */
131
171
  setFixedTimeStep(step, maxSubSteps = this.maxSubSteps) {
132
172
  if (step > 0) this.fixedTimeStep = step;
133
173
  this.maxSubSteps = maxSubSteps;
174
+ this.timeStepMode = "fixed";
175
+ return this;
176
+ }
177
+
178
+ /**
179
+ * @method setVariableTimeStep
180
+ * @description Advances the simulation once per `process()` call using the
181
+ * frame's own delta, so physics runs at exactly the rendering rate. Every
182
+ * rendered frame then shows a freshly simulated position, which is what
183
+ * removes the stepping you otherwise see when the display refreshes faster
184
+ * than the simulation.
185
+ *
186
+ * The trade-offs are real and worth knowing:
187
+ * - **Not deterministic.** Results depend on the frame timings the machine
188
+ * happened to produce, so replays and lockstep networking need fixed steps.
189
+ * - **Solver accuracy tracks the frame rate.** Contacts are resolved
190
+ * iteratively, so a long frame is a coarser solve; deep stacks and fast
191
+ * bodies are more forgiving under a fixed step.
192
+ *
193
+ * A frame longer than `maxStep` is split into several equal steps rather than
194
+ * simulated in one lump, up to `maxSubSteps`; beyond that the excess time is
195
+ * dropped instead of letting the world explode or spiral.
196
+ *
197
+ * @param {number} [maxStep=1/30] - Longest slice to simulate in one step
198
+ * @param {number} [maxSubSteps] - Max steps per process() call
199
+ * @returns {Physics} - this
200
+ */
201
+ setVariableTimeStep(maxStep = 1 / 30, maxSubSteps = this.maxSubSteps) {
202
+ if (maxStep > 0) this.maxTimeStep = maxStep;
203
+ this.maxSubSteps = maxSubSteps;
204
+ this.timeStepMode = "variable";
205
+ this._accumulator = 0;
206
+ return this;
207
+ }
208
+
209
+ /**
210
+ * @method getTimeStepMode
211
+ * @description Returns "fixed" or "variable".
212
+ * @returns {string}
213
+ */
214
+ getTimeStepMode() {
215
+ return this.timeStepMode;
134
216
  }
135
217
 
136
218
  /**
@@ -158,7 +240,7 @@ class Physics {
158
240
  ) {
159
241
  const bodyRef = this.world.createBody({
160
242
  type: type,
161
- position: new planck.Vec2(
243
+ position: new PhysicsVec2(
162
244
  position.x / this.scale,
163
245
  position.y / this.scale
164
246
  ),
@@ -166,7 +248,7 @@ class Physics {
166
248
  });
167
249
 
168
250
  if (attachFixture) {
169
- bodyRef.createFixture(new planck.Box(fixtureSize.x, fixtureSize.y), {
251
+ bodyRef.createFixture(Box(fixtureSize.x, fixtureSize.y), {
170
252
  density: density,
171
253
  friction: friction,
172
254
  restitution: restitution,
@@ -177,47 +259,158 @@ class Physics {
177
259
  }
178
260
 
179
261
  /**
180
- * @method onCollisionEnter
181
- * @description Handles the collision enter event
182
- * @param {Function} callback - The callback function to handle the collision enter event
262
+ * @method createDistanceJoint
263
+ * @description Connects two rigid bodies with a fixed-length rod between two
264
+ * world-space anchor points, or a damped spring toward that length when
265
+ * `frequencyHz` is set.
266
+ * @param {RigidBody} rigidBodyA
267
+ * @param {RigidBody} rigidBodyB
268
+ * @param {Object} [options] - `{ anchorA, anchorB, length, frequencyHz,
269
+ * dampingRatio, collideConnected }`. `anchorA`/`anchorB` are world-space
270
+ * pixel points; each defaults to its body's own center. `length` is in
271
+ * pixels; it defaults to the current distance between the anchors.
272
+ * @returns {DistanceJoint}
183
273
  */
184
- onCollisionEnter(callback) {
185
- this.world.on("begin-contact", (contact) => {
186
- const fixtureA = contact.getFixtureA();
187
- const fixtureB = contact.getFixtureB();
274
+ createDistanceJoint(rigidBodyA, rigidBodyB, options = {}) {
275
+ const bodyA = rigidBodyA.body;
276
+ const bodyB = rigidBodyB.body;
277
+ const scale = this.scale;
278
+ const anchorA = options.anchorA
279
+ ? new PhysicsVec2(options.anchorA.x / scale, options.anchorA.y / scale)
280
+ : bodyA.getWorldCenter();
281
+ const anchorB = options.anchorB
282
+ ? new PhysicsVec2(options.anchorB.x / scale, options.anchorB.y / scale)
283
+ : bodyB.getWorldCenter();
284
+ const length =
285
+ options.length != null
286
+ ? options.length / scale
287
+ : Math.hypot(anchorB.x - anchorA.x, anchorB.y - anchorA.y);
288
+
289
+ return this.world.createJoint({
290
+ type: "distance",
291
+ bodyA,
292
+ bodyB,
293
+ localAnchorA: bodyA.getLocalPoint(anchorA),
294
+ localAnchorB: bodyB.getLocalPoint(anchorB),
295
+ length,
296
+ frequencyHz: options.frequencyHz,
297
+ dampingRatio: options.dampingRatio,
298
+ collideConnected: options.collideConnected,
299
+ });
300
+ }
188
301
 
189
- const bodyA = fixtureA.getBody();
190
- const bodyB = fixtureB.getBody();
302
+ /**
303
+ * @method createRevoluteJoint
304
+ * @description Pins two rigid bodies together at a shared world-space point,
305
+ * like a hinge: a door, a pendulum arm, a see-saw. Set `enableMotor` to
306
+ * drive it toward a target angular speed instead of swinging freely.
307
+ * @param {RigidBody} rigidBodyA
308
+ * @param {RigidBody} rigidBodyB
309
+ * @param {Object} anchor - World-space pixel point both bodies pin to
310
+ * @param {Object} [options] - `{ enableMotor, motorSpeed, maxMotorTorque,
311
+ * collideConnected }`
312
+ * @returns {RevoluteJoint}
313
+ */
314
+ createRevoluteJoint(rigidBodyA, rigidBodyB, anchor, options = {}) {
315
+ const bodyA = rigidBodyA.body;
316
+ const bodyB = rigidBodyB.body;
317
+ const worldAnchor = new PhysicsVec2(
318
+ anchor.x / this.scale,
319
+ anchor.y / this.scale
320
+ );
191
321
 
192
- callback(bodyA, bodyB, contact);
322
+ return this.world.createJoint({
323
+ type: "revolute",
324
+ bodyA,
325
+ bodyB,
326
+ localAnchorA: bodyA.getLocalPoint(worldAnchor),
327
+ localAnchorB: bodyB.getLocalPoint(worldAnchor),
328
+ enableMotor: options.enableMotor,
329
+ motorSpeed: options.motorSpeed,
330
+ maxMotorTorque: options.maxMotorTorque,
331
+ collideConnected: options.collideConnected,
193
332
  });
194
333
  }
195
334
 
196
335
  /**
197
- * @method onCollisionExit
198
- * @description Handles the collision exit event
199
- * @param {Function} callback - The callback function to handle the collision exit event
336
+ * @method destroyJoint
337
+ * @description Removes a joint created by {@link Physics#createDistanceJoint}
338
+ * or {@link Physics#createRevoluteJoint}.
339
+ * @param {Joint} joint
200
340
  */
201
- onCollisionExit(callback) {
202
- this.world.on("end-contact", (contact) => {
203
- const fixtureA = contact.getFixtureA();
204
- const fixtureB = contact.getFixtureB();
341
+ destroyJoint(joint) {
342
+ this.world.destroyJoint(joint);
343
+ }
205
344
 
206
- const bodyA = fixtureA.getBody();
207
- const bodyB = fixtureB.getBody();
345
+ /**
346
+ * @method onCollisionEnter
347
+ * @description Registers a world-level listener called whenever any two
348
+ * fixtures start touching. Survives {@link Physics#clear}.
349
+ * @param {Function} callback - Called with (bodyA, bodyB, contact)
350
+ */
351
+ onCollisionEnter(callback) {
352
+ if (typeof callback === "function") this._enterCallbacks.push(callback);
353
+ }
208
354
 
209
- callback(bodyA, bodyB, contact);
210
- });
355
+ /**
356
+ * @method onCollisionExit
357
+ * @description Registers a world-level listener called whenever any two
358
+ * fixtures stop touching. Survives {@link Physics#clear}.
359
+ * @param {Function} callback - Called with (bodyA, bodyB, contact)
360
+ */
361
+ onCollisionExit(callback) {
362
+ if (typeof callback === "function") this._exitCallbacks.push(callback);
211
363
  }
212
364
 
213
365
  /**
214
366
  * @method process
215
- * @description Processes the physics engine
216
- * @param {number} dt - The delta time
367
+ * @description Advances the simulation by `dt` seconds, in whichever way the
368
+ * current time-step mode calls for. Call it once per frame.
369
+ * @param {number} dt - Seconds elapsed since the previous call
370
+ * @returns {number} - How many steps were simulated
217
371
  */
218
372
  process(dt) {
219
- if (!isFinite(dt) || dt <= 0) return;
373
+ this.lastStepCount = 0;
374
+ if (!Number.isFinite(dt) || dt <= 0) return 0;
375
+
376
+ this.lastStepCount =
377
+ this.timeStepMode === "variable"
378
+ ? this._stepVariable(dt)
379
+ : this._stepFixed(dt);
380
+ return this.lastStepCount;
381
+ }
220
382
 
383
+ /**
384
+ * @method _stepVariable
385
+ * @description Simulates the frame's own delta. Normally that is a single
386
+ * step of exactly `dt`, so physics advances in lock-step with rendering. Only
387
+ * an unusually long frame is divided, and only far enough to keep each slice
388
+ * within `maxTimeStep`.
389
+ * @param {number} dt
390
+ * @returns {number} - Steps taken
391
+ * @private
392
+ */
393
+ _stepVariable(dt) {
394
+ const budget = this.maxTimeStep * this.maxSubSteps;
395
+ const total = Math.min(dt, budget);
396
+ const steps = Math.max(1, Math.ceil(total / this.maxTimeStep));
397
+ const step = total / steps;
398
+
399
+ for (let i = 0; i < steps; i++) {
400
+ this.world.step(step);
401
+ }
402
+ return steps;
403
+ }
404
+
405
+ /**
406
+ * @method _stepFixed
407
+ * @description Banks elapsed time and simulates as many constant-size slices
408
+ * as have accumulated, leaving the remainder for next frame.
409
+ * @param {number} dt
410
+ * @returns {number} - Steps taken
411
+ * @private
412
+ */
413
+ _stepFixed(dt) {
221
414
  this._accumulator += dt;
222
415
  let steps = 0;
223
416
  while (
@@ -229,20 +422,37 @@ class Physics {
229
422
  steps++;
230
423
  }
231
424
 
232
- if (steps === this.maxSubSteps) {
233
- this._accumulator = 0;
234
- }
425
+ if (steps === this.maxSubSteps) this._accumulator = 0;
426
+ return steps;
427
+ }
428
+
429
+ /**
430
+ * @method getInterpolationAlpha
431
+ * @description How far the fixed-step simulation currently sits between its
432
+ * last completed step and the next one (0..1). Useful if you interpolate
433
+ * renderables between physics states. Always 0 in variable mode, where every
434
+ * frame already renders a freshly simulated position.
435
+ * @returns {number}
436
+ */
437
+ getInterpolationAlpha() {
438
+ if (this.timeStepMode === "variable" || this.fixedTimeStep <= 0) return 0;
439
+ return Math.min(1, this._accumulator / this.fixedTimeStep);
235
440
  }
236
441
 
237
442
  /**
238
443
  * @method clear
239
- * @description Clears the physics engine objects and resets the gravity
444
+ * @description Drops every body and starts a fresh world with the original
445
+ * gravity; use it when tearing a level down. Collision routing and any
446
+ * listeners added through onCollisionEnter/onCollisionExit are re-bound to
447
+ * the new world, so they keep working afterwards.
240
448
  */
241
449
  clear() {
242
- this.world = new planck.World({
243
- gravity: new planck.Vec2(0, this.gravity),
450
+ this.world = new World({
451
+ gravity: new PhysicsVec2(0, this.gravity),
452
+ velocityThreshold: this.world.velocityThreshold,
244
453
  });
245
454
  this._accumulator = 0;
455
+ this._dispatchContacts();
246
456
  }
247
457
 
248
458
  /**
package/src/Scene.js CHANGED
@@ -51,7 +51,7 @@ class Scene {
51
51
  * @method dispose
52
52
  * @description Destroys every object in the scene (freeing their GPU
53
53
  * resources and physics bodies) and empties it. Call when a level/screen is
54
- * torn down for good removing objects without disposing leaks GL buffers
54
+ * torn down for good; removing objects without disposing leaks GL buffers
55
55
  * over repeated scene swaps.
56
56
  */
57
57
  dispose() {
package/src/Shaders.js CHANGED
@@ -1,12 +1,12 @@
1
1
  const STANDARD_VERTEX_SHADER = `
2
2
  attribute vec4 aVertexPosition;
3
3
  attribute vec2 aTextureCoord;
4
-
4
+
5
5
  attribute vec4 aInstanceMatrix0;
6
6
  attribute vec4 aInstanceMatrix1;
7
7
  attribute vec4 aInstanceMatrix2;
8
8
  attribute vec4 aInstanceMatrix3;
9
-
9
+
10
10
  attribute vec2 aInstanceTexCoord0;
11
11
  attribute vec2 aInstanceTexCoord1;
12
12
  attribute vec2 aInstanceTexCoord2;
@@ -30,14 +30,14 @@ const STANDARD_VERTEX_SHADER = `
30
30
  aInstanceMatrix2,
31
31
  aInstanceMatrix3
32
32
  );
33
-
33
+
34
34
  if(useInstances) {
35
35
  gl_Position = uProjectionMatrix * uInstancedModelViewMatrix * instanceMatrix * aVertexPosition;
36
36
  vFragPos = (uInstancedModelViewMatrix * instanceMatrix * aVertexPosition).xy;
37
37
  vInstanceColor = aInstanceColor;
38
38
 
39
39
  int vertexIndex = int(aVertexPosition.x > 0.0 ? (aVertexPosition.y > 0.0 ? 0 : 2) : (aVertexPosition.y > 0.0 ? 1 : 3));
40
-
40
+
41
41
  if(vertexIndex == 0) {
42
42
  vTexCoord = aInstanceTexCoord0;
43
43
  } else if(vertexIndex == 1) {
@@ -70,14 +70,14 @@ const STANDARD_FRAGMENT_SHADER = `
70
70
  uniform float uLightIntensity[4];
71
71
  uniform float uLightRadius[4];
72
72
  uniform int uActiveLights;
73
-
73
+
74
74
  uniform vec2 uDirLightPosition[4];
75
75
  uniform vec2 uDirLightDirection[4];
76
76
  uniform vec3 uDirLightColor[4];
77
77
  uniform float uDirLightIntensity[4];
78
78
  uniform float uDirLightWidth[4];
79
79
  uniform int uActiveDirLights;
80
-
80
+
81
81
  uniform vec3 uAmbientLightValues;
82
82
  uniform bool uUseLighting;
83
83
 
@@ -98,44 +98,44 @@ const STANDARD_FRAGMENT_SHADER = `
98
98
  }
99
99
 
100
100
  texColor *= vInstanceColor;
101
-
101
+
102
102
  vec3 lighting = ambientLight.rgb;
103
-
103
+
104
104
  for(int i = 0; i < 4; i++) {
105
105
  if(i >= uActiveLights) break;
106
-
106
+
107
107
  float distance = length(uLightPosition[i] - vFragPos);
108
-
108
+
109
109
  if(distance < uLightRadius[i]) {
110
110
  float attenuation = 1.0 - distance / uLightRadius[i];
111
-
111
+
112
112
  lighting += uLightColor[i] * attenuation * uLightIntensity[i];
113
113
  }
114
114
  }
115
-
115
+
116
116
  for(int i = 0; i < 4; i++) {
117
117
  if(i >= uActiveDirLights) break;
118
-
118
+
119
119
  vec2 lightPos = uDirLightPosition[i];
120
120
  vec2 lightDir = normalize(uDirLightDirection[i]);
121
121
  vec2 toFrag = vFragPos - lightPos;
122
-
122
+
123
123
  float projection = dot(toFrag, lightDir);
124
124
  if(projection < 0.0) continue;
125
-
125
+
126
126
  float perpDistance = abs(dot(toFrag, vec2(-lightDir.y, lightDir.x)));
127
-
127
+
128
128
  if(perpDistance > uDirLightWidth[i] * 0.5) continue;
129
-
129
+
130
130
  float widthFactor = 1.0 - (perpDistance / (uDirLightWidth[i] * 0.5));
131
-
131
+
132
132
  float distance = length(toFrag);
133
133
  float maxDistance = 1000.0;
134
134
  float distanceFactor = max(0.0, 1.0 - (distance / maxDistance));
135
-
135
+
136
136
  lighting += uDirLightColor[i] * uDirLightIntensity[i] * widthFactor * distanceFactor;
137
137
  }
138
-
138
+
139
139
  vec4 outColor;
140
140
  if (uUseLighting) {
141
141
  outColor = vec4(texColor.rgb * lighting, texColor.a);
package/src/Tilemap.js CHANGED
@@ -132,7 +132,7 @@ class Tilemap {
132
132
  * @method buildColliders
133
133
  * @description Generates static physics colliders from the current map. Solid
134
134
  * cells are merged greedily into horizontal runs, so a row of N tiles becomes
135
- * one box collider instead of N far fewer bodies for the physics engine.
135
+ * one box collider instead of N, far fewer bodies for the physics engine.
136
136
  * The bodies are tagged so collision callbacks resolve back to `ownerObject`
137
137
  * (defaults to the tilemap's GameObject). Call again after setMap to rebuild.
138
138
  *
package/src/UI.js CHANGED
@@ -297,7 +297,7 @@ class UI {
297
297
 
298
298
  /**
299
299
  * @method isOver
300
- * @description Whether an interactive element is under the given page point
300
+ * @description Whether an interactive element is under the given page point,
301
301
  * useful to suppress game clicks behind the UI.
302
302
  */
303
303
  isOver(clientX, clientY) {
@@ -74,7 +74,7 @@ class Behaviour {
74
74
  * @description Called when the owner's body starts touching another. Requires
75
75
  * the Physics world to be ticked. Override in subclasses.
76
76
  * @param {Object} other - The other GameObject/Instance (or null)
77
- * @param {Object} contact - The planck contact
77
+ * @param {Object} contact - The physics contact
78
78
  */
79
79
  onCollisionEnter(other, contact) {}
80
80
 
@@ -82,7 +82,7 @@ class Behaviour {
82
82
  * @method onCollisionExit
83
83
  * @description Called when the owner's body stops touching another.
84
84
  * @param {Object} other - The other GameObject/Instance (or null)
85
- * @param {Object} contact - The planck contact
85
+ * @param {Object} contact - The physics contact
86
86
  */
87
87
  onCollisionExit(other, contact) {}
88
88
  }
@@ -1,4 +1,4 @@
1
- import * as planck from "planck";
1
+ import { Box } from "../physics/index.js";
2
2
  import BoxColliderDebug from "./BoxColliderDebug.js";
3
3
  import SceneManager from "../managers/SceneManager.js";
4
4
  import Collider from "./Collider.js";
@@ -26,14 +26,12 @@ class BoxCollider extends Collider {
26
26
  filter = null
27
27
  ) {
28
28
  super(rigidbody, isSensor, parentObject);
29
- this.collider = rigidbody
30
- .getBody()
31
- .createFixture(new planck.Box(fixtureSize.x, fixtureSize.y), {
32
- density: density,
33
- friction: friction,
34
- restitution: restitution,
35
- isSensor: isSensor,
36
- });
29
+ this.collider = rigidbody.createFixture(Box(fixtureSize.x, fixtureSize.y), {
30
+ density: density,
31
+ friction: friction,
32
+ restitution: restitution,
33
+ isSensor: isSensor,
34
+ });
37
35
  this.fixtureSize = fixtureSize;
38
36
  this.rigidbody.setCollider(this);
39
37
  this.name = "BoxCollider" + this.id;
@@ -14,13 +14,12 @@ class BoxColliderDebug {
14
14
  this.physics = this.rigidbody.physics;
15
15
  this.scale = this.physics.getScale();
16
16
  this.color = color || new Color(255, 0, 0, 255);
17
+ const body = this.rigidbody.getBody();
17
18
  this.gameObject = new GameObject(
18
19
  "BoxColliderDebug",
19
20
  new Vector3(
20
- this.rigidbody.getBody().getPosition().x *
21
- this.rigidbody.getPhysics().getScale(),
22
- this.rigidbody.getBody().getPosition().y *
23
- this.rigidbody.getPhysics().getScale(),
21
+ body.getPosition().x * this.scale,
22
+ body.getPosition().y * this.scale,
24
23
  100
25
24
  ),
26
25
  this.rigidbody.getAngle(),
@@ -1,4 +1,4 @@
1
- import * as planck from "planck";
1
+ import { Circle } from "../physics/index.js";
2
2
  import SceneManager from "../managers/SceneManager.js";
3
3
  import CircleColliderDebug from "./CircleColliderDebug.js";
4
4
  import Collider from "./Collider.js";
@@ -26,14 +26,12 @@ class CircleCollider extends Collider {
26
26
  filter = null
27
27
  ) {
28
28
  super(rigidbody, isSensor, parentObject);
29
- this.collider = rigidbody
30
- .getBody()
31
- .createFixture(new planck.Circle(radius), {
32
- density: density,
33
- friction: friction,
34
- restitution: restitution,
35
- isSensor: isSensor,
36
- });
29
+ this.collider = rigidbody.createFixture(Circle(radius), {
30
+ density: density,
31
+ friction: friction,
32
+ restitution: restitution,
33
+ isSensor: isSensor,
34
+ });
37
35
  this.radius = radius;
38
36
  this.rigidbody.setCollider(this);
39
37
  this.name = "CircleCollider" + this.id;