emeraldengine 2.2.1 → 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 (208) hide show
  1. package/README.md +2198 -968
  2. package/dist/types/index.d.ts +75 -32
  3. package/dist/types/src/Animator.d.ts +50 -0
  4. package/dist/types/{BitmapText.d.ts → src/BitmapText.d.ts} +19 -21
  5. package/dist/types/src/Camera.d.ts +122 -0
  6. package/dist/types/src/CameraController.d.ts +107 -0
  7. package/dist/types/src/CanvasText.d.ts +91 -0
  8. package/dist/types/src/CollisionLayers.d.ts +58 -0
  9. package/dist/types/{Color.d.ts → src/Color.d.ts} +6 -6
  10. package/dist/types/src/Coroutine.d.ts +111 -0
  11. package/dist/types/src/DebugOverlay.d.ts +86 -0
  12. package/dist/types/src/Drawable.d.ts +271 -0
  13. package/dist/types/src/Easing.d.ts +22 -0
  14. package/dist/types/src/Emerald.d.ts +420 -0
  15. package/dist/types/src/EmeraldDB.d.ts +159 -0
  16. package/dist/types/{FPSCounter.d.ts → src/FPSCounter.d.ts} +1 -3
  17. package/dist/types/src/GLUtils.d.ts +4 -0
  18. package/dist/types/{Instance.d.ts → src/Instance.d.ts} +37 -15
  19. package/dist/types/{InstancedTexture.d.ts → src/InstancedTexture.d.ts} +75 -23
  20. package/dist/types/src/Interpolator.d.ts +66 -0
  21. package/dist/types/src/Material.d.ts +87 -0
  22. package/dist/types/src/MathUtils.d.ts +81 -0
  23. package/dist/types/src/ParticleEmitter.d.ts +131 -0
  24. package/dist/types/src/Physics.d.ts +337 -0
  25. package/dist/types/src/Pool.d.ts +53 -0
  26. package/dist/types/src/PostEffects.d.ts +68 -0
  27. package/dist/types/src/PostProcessor.d.ts +124 -0
  28. package/dist/types/src/RenderTarget.d.ts +56 -0
  29. package/dist/types/src/Scene.d.ts +62 -0
  30. package/dist/types/src/ScreenEffects.d.ts +111 -0
  31. package/dist/types/src/Serializer.d.ts +86 -0
  32. package/dist/types/src/Shaders.d.ts +2 -0
  33. package/dist/types/{Shapes.d.ts → src/Shapes.d.ts} +4 -6
  34. package/dist/types/src/SpatialGrid.d.ts +50 -0
  35. package/dist/types/src/SpriteBatch.d.ts +89 -0
  36. package/dist/types/src/StateMachine.d.ts +59 -0
  37. package/dist/types/src/Storage.d.ts +89 -0
  38. package/dist/types/{Texture.d.ts → src/Texture.d.ts} +3 -4
  39. package/dist/types/src/TextureAtlas.d.ts +55 -0
  40. package/dist/types/src/Tilemap.d.ts +91 -0
  41. package/dist/types/src/Time.d.ts +53 -0
  42. package/dist/types/src/Timer.d.ts +54 -0
  43. package/dist/types/src/Transform.d.ts +94 -0
  44. package/dist/types/src/Tween.d.ts +81 -0
  45. package/dist/types/src/UI.d.ts +121 -0
  46. package/dist/types/src/components/Behaviour.d.ts +71 -0
  47. package/dist/types/{components → src/components}/BoxCollider.d.ts +14 -14
  48. package/dist/types/src/components/BoxColliderDebug.d.ts +15 -0
  49. package/dist/types/{components → src/components}/CircleCollider.d.ts +14 -12
  50. package/dist/types/src/components/CircleColliderDebug.d.ts +15 -0
  51. package/dist/types/src/components/Collider.d.ts +102 -0
  52. package/dist/types/src/components/GameObject.d.ts +135 -0
  53. package/dist/types/src/components/PolygonCollider.d.ts +33 -0
  54. package/dist/types/src/components/RigidBody.d.ts +456 -0
  55. package/dist/types/src/importers/Aseprite.d.ts +79 -0
  56. package/dist/types/src/importers/ForgeLevel.d.ts +97 -0
  57. package/dist/types/src/importers/TiledMap.d.ts +62 -0
  58. package/dist/types/{lights → src/lights}/DirectionalLight.d.ts +7 -9
  59. package/dist/types/{lights → src/lights}/PointLight.d.ts +6 -9
  60. package/dist/types/src/managers/AssetManager.d.ts +116 -0
  61. package/dist/types/src/managers/AudioManager.d.ts +259 -0
  62. package/dist/types/{managers → src/managers}/CameraManager.d.ts +8 -8
  63. package/dist/types/{managers → src/managers}/EventManager.d.ts +46 -32
  64. package/dist/types/src/managers/GLManager.d.ts +84 -0
  65. package/dist/types/src/managers/GLState.d.ts +37 -0
  66. package/dist/types/src/managers/Gamepad.d.ts +102 -0
  67. package/dist/types/src/managers/IDManager.d.ts +31 -0
  68. package/dist/types/src/managers/InputManager.d.ts +381 -0
  69. package/dist/types/src/managers/NetworkManager.d.ts +93 -0
  70. package/dist/types/src/managers/RenderStats.d.ts +34 -0
  71. package/dist/types/src/managers/SceneManager.d.ts +44 -0
  72. package/dist/types/src/managers/ShaderManager.d.ts +55 -0
  73. package/dist/types/src/managers/TextureManager.d.ts +102 -0
  74. package/dist/types/src/particlesystem/Particle.d.ts +64 -0
  75. package/dist/types/{particlesystem → src/particlesystem}/ParticleSettings.d.ts +32 -24
  76. package/dist/types/{particlesystem → src/particlesystem}/Particles.d.ts +20 -12
  77. package/dist/types/src/physics/AABB.d.ts +92 -0
  78. package/dist/types/src/physics/Body.d.ts +435 -0
  79. package/dist/types/src/physics/BodyType.d.ts +6 -0
  80. package/dist/types/src/physics/BroadPhase.d.ts +210 -0
  81. package/dist/types/src/physics/Collision.d.ts +102 -0
  82. package/dist/types/src/physics/Contact.d.ts +206 -0
  83. package/dist/types/src/physics/ContactSolver.d.ts +108 -0
  84. package/dist/types/src/physics/Distance.d.ts +54 -0
  85. package/dist/types/src/physics/DistanceJoint.d.ts +90 -0
  86. package/dist/types/src/physics/Fixture.d.ts +221 -0
  87. package/dist/types/src/physics/Island.d.ts +52 -0
  88. package/dist/types/src/physics/Joint.d.ts +59 -0
  89. package/dist/types/src/physics/Math2D.d.ts +371 -0
  90. package/dist/types/src/physics/RevoluteJoint.d.ts +119 -0
  91. package/dist/types/src/physics/Settings.d.ts +22 -0
  92. package/dist/types/src/physics/Shapes.d.ts +207 -0
  93. package/dist/types/src/physics/TimeOfImpact.d.ts +22 -0
  94. package/dist/types/src/physics/World.d.ts +274 -0
  95. package/dist/types/src/physics/index.d.ts +34 -0
  96. package/index.js +78 -0
  97. package/package.json +73 -60
  98. package/src/Animator.js +95 -0
  99. package/src/BitmapText.js +6 -5
  100. package/src/Camera.js +183 -0
  101. package/src/CameraController.js +192 -0
  102. package/src/CanvasText.js +281 -0
  103. package/src/CollisionLayers.js +86 -0
  104. package/src/Color.js +26 -18
  105. package/src/Coroutine.js +259 -0
  106. package/src/DebugOverlay.js +246 -0
  107. package/src/Drawable.js +842 -582
  108. package/src/Easing.js +57 -0
  109. package/src/Emerald.js +1150 -459
  110. package/src/EmeraldDB.js +328 -0
  111. package/src/FPSCounter.js +43 -43
  112. package/src/GLUtils.js +60 -67
  113. package/src/Instance.js +41 -5
  114. package/src/InstancedTexture.js +308 -127
  115. package/src/Interpolator.js +124 -0
  116. package/src/Material.js +202 -0
  117. package/src/MathUtils.js +134 -0
  118. package/src/ParticleEmitter.js +284 -0
  119. package/src/Physics.js +431 -40
  120. package/src/Pool.js +85 -0
  121. package/src/PostEffects.js +296 -0
  122. package/src/PostProcessor.js +304 -0
  123. package/src/RenderTarget.js +134 -0
  124. package/src/Scene.js +115 -83
  125. package/src/ScreenEffects.js +266 -0
  126. package/src/Serializer.js +131 -0
  127. package/src/Shaders.js +150 -165
  128. package/src/Shapes.js +118 -129
  129. package/src/SpatialGrid.js +111 -0
  130. package/src/SpriteBatch.js +299 -0
  131. package/src/StateMachine.js +82 -0
  132. package/src/Storage.js +175 -47
  133. package/src/Texture.js +58 -67
  134. package/src/TextureAtlas.js +96 -0
  135. package/src/Tilemap.js +274 -0
  136. package/src/Time.js +51 -6
  137. package/src/Timer.js +99 -0
  138. package/src/Transform.js +100 -7
  139. package/src/Tween.js +160 -0
  140. package/src/UI.js +394 -0
  141. package/src/components/Behaviour.js +90 -0
  142. package/src/components/BoxCollider.js +29 -12
  143. package/src/components/BoxColliderDebug.js +3 -4
  144. package/src/components/CircleCollider.js +26 -12
  145. package/src/components/CircleColliderDebug.js +25 -24
  146. package/src/components/Collider.js +150 -34
  147. package/src/components/GameObject.js +130 -21
  148. package/src/components/PolygonCollider.js +55 -0
  149. package/src/components/RigidBody.js +556 -8
  150. package/src/importers/Aseprite.js +142 -0
  151. package/src/importers/ForgeLevel.js +581 -0
  152. package/src/importers/TiledMap.js +158 -0
  153. package/src/lights/DirectionalLight.js +6 -15
  154. package/src/lights/PointLight.js +4 -4
  155. package/src/managers/AssetManager.js +239 -0
  156. package/src/managers/AudioManager.js +565 -146
  157. package/src/managers/EventManager.js +488 -477
  158. package/src/managers/GLManager.js +57 -0
  159. package/src/managers/GLState.js +70 -0
  160. package/src/managers/Gamepad.js +126 -0
  161. package/src/managers/IDManager.js +24 -2
  162. package/src/managers/InputManager.js +905 -0
  163. package/src/managers/NetworkManager.js +178 -0
  164. package/src/managers/RenderStats.js +34 -0
  165. package/src/managers/SceneManager.js +30 -0
  166. package/src/managers/ShaderManager.js +0 -2
  167. package/src/managers/TextureManager.js +218 -0
  168. package/src/particlesystem/Particle.js +82 -7
  169. package/src/particlesystem/ParticleSettings.js +21 -3
  170. package/src/particlesystem/Particles.js +80 -31
  171. package/src/physics/AABB.js +207 -0
  172. package/src/physics/Body.js +862 -0
  173. package/src/physics/BodyType.js +16 -0
  174. package/src/physics/BroadPhase.js +641 -0
  175. package/src/physics/Collision.js +534 -0
  176. package/src/physics/Contact.js +500 -0
  177. package/src/physics/ContactSolver.js +526 -0
  178. package/src/physics/Distance.js +403 -0
  179. package/src/physics/DistanceJoint.js +227 -0
  180. package/src/physics/Fixture.js +346 -0
  181. package/src/physics/Island.js +203 -0
  182. package/src/physics/Joint.js +78 -0
  183. package/src/physics/Math2D.js +573 -0
  184. package/src/physics/RevoluteJoint.js +278 -0
  185. package/src/physics/Settings.js +78 -0
  186. package/src/physics/Shapes.js +549 -0
  187. package/src/physics/TimeOfImpact.js +87 -0
  188. package/src/physics/World.js +731 -0
  189. package/src/physics/index.js +79 -0
  190. package/dist/types/Drawable.d.ts +0 -157
  191. package/dist/types/Emerald.d.ts +0 -73
  192. package/dist/types/GLUtils.d.ts +0 -6
  193. package/dist/types/Physics.d.ts +0 -139
  194. package/dist/types/Scene.d.ts +0 -39
  195. package/dist/types/Shaders.d.ts +0 -4
  196. package/dist/types/Storage.d.ts +0 -46
  197. package/dist/types/Time.d.ts +0 -22
  198. package/dist/types/Transform.d.ts +0 -41
  199. package/dist/types/components/BoxColliderDebug.d.ts +0 -19
  200. package/dist/types/components/CircleColliderDebug.d.ts +0 -19
  201. package/dist/types/components/Collider.d.ts +0 -53
  202. package/dist/types/components/GameObject.d.ts +0 -72
  203. package/dist/types/components/RigidBody.d.ts +0 -104
  204. package/dist/types/managers/AudioManager.d.ts +0 -60
  205. package/dist/types/managers/GLManager.d.ts +0 -47
  206. package/dist/types/managers/IDManager.d.ts +0 -21
  207. package/dist/types/managers/SceneManager.d.ts +0 -22
  208. package/dist/types/particlesystem/Particle.d.ts +0 -42
package/src/Physics.js CHANGED
@@ -1,20 +1,218 @@
1
- import * as planck from "planck";
1
+ import { World, Box, Vec2 as PhysicsVec2 } from "./physics/index.js";
2
+
3
+ /**
4
+ * @function dispatchCollision
5
+ * @description Calls onCollisionEnter/onCollisionExit on an object and its
6
+ * component list (Behaviours), passing the other object. Duck-typed so this
7
+ * module doesn't need to import the component classes.
8
+ * @private
9
+ */
10
+ function dispatchCollision(object, other, contact, type) {
11
+ if (!object) return;
12
+ if (typeof object[type] === "function") {
13
+ object[type](other, contact);
14
+ }
15
+ if (Array.isArray(object.components)) {
16
+ for (const comp of object.components) {
17
+ if (comp && typeof comp[type] === "function") {
18
+ comp[type](other, contact);
19
+ }
20
+ }
21
+ }
22
+ }
2
23
 
3
24
  /**
4
25
  * @class Physics
5
- * @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.
6
31
  * @param {number} gravity - The gravity of the physics engine
7
- * @param {number} scale - The scale of the physics engine
8
- * @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
9
35
  */
10
36
  class Physics {
11
37
  constructor(gravity, scale, velocityThreshold = 0.1) {
12
- this.world = new planck.World({
13
- gravity: new planck.Vec2(0, gravity),
38
+ this.world = new World({
39
+ gravity: new PhysicsVec2(0, gravity),
40
+ velocityThreshold,
14
41
  });
15
- planck.Settings.velocityThreshold = velocityThreshold;
16
42
  this.gravity = gravity;
17
43
  this.scale = scale;
44
+
45
+ this.fixedTimeStep = 1 / 60;
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;
53
+ /** @private */
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 = [];
59
+
60
+ this._dispatchContacts();
61
+ }
62
+
63
+ /**
64
+ * @method _dispatchContacts
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().
70
+ * @private
71
+ */
72
+ _dispatchContacts() {
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();
81
+ const objA = a && a.parentObject ? a.parentObject : null;
82
+ const objB = b && b.parentObject ? b.parentObject : null;
83
+ dispatchCollision(objA, objB, contact, type);
84
+ dispatchCollision(objB, objA, contact, type);
85
+
86
+ for (const callback of callbacks) callback(bodyA, bodyB, contact);
87
+ };
88
+
89
+ this.world.on("begin-contact", (contact) =>
90
+ fire(contact, "onCollisionEnter", this._enterCallbacks)
91
+ );
92
+ this.world.on("end-contact", (contact) =>
93
+ fire(contact, "onCollisionExit", this._exitCallbacks)
94
+ );
95
+ }
96
+
97
+ /**
98
+ * @method raycast
99
+ * @description Casts a ray through the world and returns the closest hit.
100
+ * @param {Object} origin - World-space { x, y } start point
101
+ * @param {Object} direction - Ray direction { x, y } (need not be normalized)
102
+ * @param {number} maxDistance - Max ray length in world units
103
+ * @returns {{object, rigidBody, point, normal, fraction}|null}
104
+ */
105
+ raycast(origin, direction, maxDistance) {
106
+ const scale = this.scale;
107
+ const len = Math.hypot(direction.x, direction.y) || 1;
108
+ const dx = direction.x / len;
109
+ const dy = direction.y / len;
110
+ const p1 = new PhysicsVec2(origin.x / scale, origin.y / scale);
111
+ const p2 = new PhysicsVec2(
112
+ (origin.x + dx * maxDistance) / scale,
113
+ (origin.y + dy * maxDistance) / scale
114
+ );
115
+
116
+ let result = null;
117
+ this.world.rayCast(p1, p2, (fixture, point, normal, fraction) => {
118
+ const rb = fixture.getBody().getUserData();
119
+ result = {
120
+ object: rb && rb.parentObject ? rb.parentObject : null,
121
+ rigidBody: rb || null,
122
+ point: { x: point.x * scale, y: point.y * scale },
123
+ normal: { x: normal.x, y: normal.y },
124
+ fraction,
125
+ };
126
+ return fraction;
127
+ });
128
+ return result;
129
+ }
130
+
131
+ /**
132
+ * @method queryPoint
133
+ * @description Returns the objects whose colliders contain a world-space point.
134
+ * @param {Object} point - World-space { x, y }
135
+ * @returns {Array} - Owning objects at the point
136
+ */
137
+ queryPoint(point) {
138
+ const p = new PhysicsVec2(point.x / this.scale, point.y / this.scale);
139
+ const results = [];
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
+ });
149
+ return results;
150
+ }
151
+
152
+ /**
153
+ * @method setFixedTimeStep
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
+ *
167
+ * @param {number} step - Fixed step in seconds (e.g. 1/60)
168
+ * @param {number} [maxSubSteps] - Max steps per process() call (spiral guard)
169
+ * @returns {Physics} - this
170
+ */
171
+ setFixedTimeStep(step, maxSubSteps = this.maxSubSteps) {
172
+ if (step > 0) this.fixedTimeStep = step;
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;
18
216
  }
19
217
 
20
218
  /**
@@ -42,7 +240,7 @@ class Physics {
42
240
  ) {
43
241
  const bodyRef = this.world.createBody({
44
242
  type: type,
45
- position: new planck.Vec2(
243
+ position: new PhysicsVec2(
46
244
  position.x / this.scale,
47
245
  position.y / this.scale
48
246
  ),
@@ -50,7 +248,7 @@ class Physics {
50
248
  });
51
249
 
52
250
  if (attachFixture) {
53
- bodyRef.createFixture(new planck.Box(fixtureSize.x, fixtureSize.y), {
251
+ bodyRef.createFixture(Box(fixtureSize.x, fixtureSize.y), {
54
252
  density: density,
55
253
  friction: friction,
56
254
  restitution: restitution,
@@ -61,56 +259,200 @@ class Physics {
61
259
  }
62
260
 
63
261
  /**
64
- * @method onCollisionEnter
65
- * @description Handles the collision enter event
66
- * @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}
67
273
  */
68
- onCollisionEnter(callback) {
69
- this.world.on("begin-contact", (contact) => {
70
- const fixtureA = contact.getFixtureA();
71
- 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);
72
288
 
73
- const bodyA = fixtureA.getBody();
74
- const bodyB = fixtureB.getBody();
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
+ }
301
+
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
+ );
75
321
 
76
- 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,
77
332
  });
78
333
  }
79
334
 
80
335
  /**
81
- * @method onCollisionExit
82
- * @description Handles the collision exit event
83
- * @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
84
340
  */
85
- onCollisionExit(callback) {
86
- this.world.on("end-contact", (contact) => {
87
- const fixtureA = contact.getFixtureA();
88
- const fixtureB = contact.getFixtureB();
341
+ destroyJoint(joint) {
342
+ this.world.destroyJoint(joint);
343
+ }
89
344
 
90
- const bodyA = fixtureA.getBody();
91
- 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
+ }
92
354
 
93
- callback(bodyA, bodyB, contact);
94
- });
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);
95
363
  }
96
364
 
97
365
  /**
98
366
  * @method process
99
- * @description Processes the physics engine
100
- * @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
101
371
  */
102
372
  process(dt) {
103
- this.world.step(dt);
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
+ }
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) {
414
+ this._accumulator += dt;
415
+ let steps = 0;
416
+ while (
417
+ this._accumulator >= this.fixedTimeStep &&
418
+ steps < this.maxSubSteps
419
+ ) {
420
+ this.world.step(this.fixedTimeStep);
421
+ this._accumulator -= this.fixedTimeStep;
422
+ steps++;
423
+ }
424
+
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);
104
440
  }
105
441
 
106
442
  /**
107
443
  * @method clear
108
- * @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.
109
448
  */
110
449
  clear() {
111
- this.world = new planck.World({
112
- gravity: new planck.Vec2(0, this.gravity),
450
+ this.world = new World({
451
+ gravity: new PhysicsVec2(0, this.gravity),
452
+ velocityThreshold: this.world.velocityThreshold,
113
453
  });
454
+ this._accumulator = 0;
455
+ this._dispatchContacts();
114
456
  }
115
457
 
116
458
  /**
@@ -152,8 +494,31 @@ class Physics {
152
494
  * @param {number} y - The y coordinate of the vector
153
495
  */
154
496
  class Vector2 {
155
- constructor(x, y) {
156
- return new planck.Vec2(x, y);
497
+ constructor(x = 0, y = 0) {
498
+ this.x = x;
499
+ this.y = y;
500
+ }
501
+
502
+ /**
503
+ * @method set
504
+ * @description Sets the x and y coordinates in place
505
+ * @param {number} x - The x coordinate
506
+ * @param {number} y - The y coordinate
507
+ * @returns {Vector2} - This vector
508
+ */
509
+ set(x, y) {
510
+ this.x = x;
511
+ this.y = y;
512
+ return this;
513
+ }
514
+
515
+ /**
516
+ * @method clone
517
+ * @description Returns a copy of the vector
518
+ * @returns {Vector2} - The cloned vector
519
+ */
520
+ clone() {
521
+ return new Vector2(this.x, this.y);
157
522
  }
158
523
 
159
524
  /**
@@ -195,8 +560,34 @@ class Vector2 {
195
560
  * @param {number} z - The z coordinate of the vector
196
561
  */
197
562
  class Vector3 {
198
- constructor(x, y, z) {
199
- return new planck.Vec3(x, y, z);
563
+ constructor(x = 0, y = 0, z = 0) {
564
+ this.x = x;
565
+ this.y = y;
566
+ this.z = z;
567
+ }
568
+
569
+ /**
570
+ * @method set
571
+ * @description Sets the x, y and z coordinates in place
572
+ * @param {number} x - The x coordinate
573
+ * @param {number} y - The y coordinate
574
+ * @param {number} z - The z coordinate
575
+ * @returns {Vector3} - This vector
576
+ */
577
+ set(x, y, z) {
578
+ this.x = x;
579
+ this.y = y;
580
+ this.z = z;
581
+ return this;
582
+ }
583
+
584
+ /**
585
+ * @method clone
586
+ * @description Returns a copy of the vector
587
+ * @returns {Vector3} - The cloned vector
588
+ */
589
+ clone() {
590
+ return new Vector3(this.x, this.y, this.z);
200
591
  }
201
592
 
202
593
  /**
package/src/Pool.js ADDED
@@ -0,0 +1,85 @@
1
+ /**
2
+ * @class Pool
3
+ * @description Generic object pool to avoid per-frame allocations for churny
4
+ * objects (bullets, particles, enemies). Provide a factory and an optional
5
+ * reset function; acquire reused objects and release them when done.
6
+ *
7
+ * @example
8
+ * const bullets = new Pool(() => new Bullet(), (b, x, y) => b.spawn(x, y), 50);
9
+ * const b = bullets.acquire(px, py);
10
+ * // later...
11
+ * bullets.release(b);
12
+ */
13
+ class Pool {
14
+ /**
15
+ * @param {Function} factory - Creates a new pooled object
16
+ * @param {Function} [reset] - Called on acquire as reset(obj, ...args)
17
+ * @param {number} [initialSize] - Number of objects to pre-create
18
+ */
19
+ constructor(factory, reset = null, initialSize = 0) {
20
+ if (typeof factory !== "function") {
21
+ throw new Error("[Pool] > factory must be a function");
22
+ }
23
+ this.factory = factory;
24
+ this.reset = reset;
25
+ this.available = [];
26
+ this.active = new Set();
27
+
28
+ for (let i = 0; i < initialSize; i++) {
29
+ this.available.push(factory());
30
+ }
31
+ }
32
+
33
+ /**
34
+ * @method acquire
35
+ * @description Returns a pooled object (reused or freshly created). Extra
36
+ * arguments are forwarded to the reset function.
37
+ * @returns {*} - The acquired object
38
+ */
39
+ acquire(...args) {
40
+ const obj = this.available.length ? this.available.pop() : this.factory();
41
+ this.active.add(obj);
42
+ if (this.reset) this.reset(obj, ...args);
43
+ return obj;
44
+ }
45
+
46
+ /**
47
+ * @method release
48
+ * @description Returns an object to the pool.
49
+ * @param {*} obj - The object to release
50
+ */
51
+ release(obj) {
52
+ if (this.active.delete(obj)) {
53
+ this.available.push(obj);
54
+ }
55
+ }
56
+
57
+ /**
58
+ * @method releaseAll
59
+ * @description Returns every active object to the pool.
60
+ */
61
+ releaseAll() {
62
+ for (const obj of this.active) {
63
+ this.available.push(obj);
64
+ }
65
+ this.active.clear();
66
+ }
67
+
68
+ /**
69
+ * @member activeCount
70
+ * @description Number of currently-acquired objects.
71
+ */
72
+ get activeCount() {
73
+ return this.active.size;
74
+ }
75
+
76
+ /**
77
+ * @member size
78
+ * @description Total objects managed by the pool (active + available).
79
+ */
80
+ get size() {
81
+ return this.available.length + this.active.size;
82
+ }
83
+ }
84
+
85
+ export default Pool;