@combos-fun/plugin-matterjs 0.0.42 → 0.0.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/agent-skill.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  ## When to read
6
6
 
7
- Read for any 2D physics task: gravity, collisions, kinematic / static bodies, mouse constraints, rigid-body alignment with sprites.
7
+ Read for any 2D physics task: gravity, collisions, kinematic / static bodies, mouse constraints, rigid-body alignment with sprites, custom hulls (vertices / trapezoid / capsule / compound / edge / chain).
8
8
 
9
9
  ## Public API
10
10
 
@@ -13,24 +13,53 @@ import {
13
13
  PhysicsSystem,
14
14
  Physics,
15
15
  PhysicsType,
16
+ type PhysicsParams,
17
+ type PhysicsPartParams,
18
+ type PhysicsVertex,
16
19
  type PhysicsSystemParams,
17
20
  } from '@combos-fun/plugin-matterjs';
18
21
  ```
19
22
 
20
23
  ### `PhysicsType`
21
24
 
22
- `RECTANGLE`, `CIRCLE`, `POLYGON`.
25
+ | Value | Matter mapping | Notes |
26
+ |-------|----------------|-------|
27
+ | `RECTANGLE` | `Bodies.rectangle` | Size = `width`/`height`, else `transform.size × scale` |
28
+ | `CIRCLE` | `Bodies.circle` | `radius`; else `min(halfW, halfH)`, at least 1 |
29
+ | `POLYGON` | `Bodies.polygon` | Regular n-gon. `sides` (min 3, default 6) + `radius` |
30
+ | `TRAPEZOID` | `Bodies.trapezoid` | `width`/`height` (same fallback as rectangle) + `slope` (0–0.999, default 0.5) |
31
+ | `FROM_VERTICES` | `Bodies.fromVertices` | Arbitrary polygon. Concave shapes are decomposed via `poly-decomp` |
32
+ | `CAPSULE` | rectangle + 2 circles, then `Body.create({ parts })` | Stadium. `length` is full length including caps; `radius` is cap radius. `length <= 2r` becomes a circle |
33
+ | `COMPOUND` | `Body.create({ parts })` | One rigid body from `parts[]`. Nested `COMPOUND` is rejected |
34
+ | `EDGE` | thin `Bodies.rectangle` | Segment `from` → `to`. Orientation comes from the endpoints (not `bodyOptions.angle`) |
35
+ | `CHAIN` | compound of thin rectangles | Rigid polyline through `vertices`. **Not** a soft rope |
36
+
37
+ `CHAIN` / `EDGE` are static-terrain style colliders. A swinging rope needs multiple bodies + constraints; do not fake that with `CHAIN`.
38
+
39
+ ### Coordinates
40
+
41
+ `vertices`, `vertexSets`, `from`, `to`, and each part's `position` are **local** to the GameObject centre. The factory adds the body origin (`bodyParams.position` or the object's transform). Do not pass world-space vertices.
23
42
 
24
43
  ### `Physics` Component params
25
44
 
26
45
  | Field | Type | Notes |
27
46
  |-------|------|-------|
28
- | `type` | `PhysicsType?` | Body shape |
29
- | `bodyOptions` | `object?` | Matter Body options: `isStatic`, `restitution`, `frictionAir`, `density`, `friction`, ... |
30
- | `position` | `{x, y}?` | Initial offset |
31
- | `sides` / `radius` | `number?` | Polygon / circle |
47
+ | `type` | `PhysicsType?` | Body shape. Required when the object enters the scene |
48
+ | `bodyOptions` | `object?` | Matter Body options: `isStatic`, `restitution`, `frictionAir`, `density`, `friction`, `angle`, ... |
49
+ | `position` | `{x, y}?` | Body origin in world space. If omitted, taken from the GameObject transform |
50
+ | `width` / `height` | `number?` | Rectangle / trapezoid / capsule fallback size |
51
+ | `sides` / `radius` | `number?` | Regular polygon / circle / capsule cap |
52
+ | `slope` | `number?` | Trapezoid only. Clamped to `< 1` |
53
+ | `length` | `number?` | Capsule full length (including caps). Default = transform width |
54
+ | `vertices` | `{x, y}[]?` | `FROM_VERTICES` (one contour) or `CHAIN` (polyline) |
55
+ | `vertexSets` | `{x, y}[][]?` | `FROM_VERTICES` with multiple contours. Takes precedence over `vertices` |
56
+ | `from` / `to` | `{x, y}?` | `EDGE` endpoints, local to the body origin |
57
+ | `thickness` | `number?` | `EDGE` / `CHAIN` segment thickness. Default 4, minimum 1 |
58
+ | `parts` | `PhysicsPartParams[]?` | `COMPOUND` only. Same shape fields as above, plus a local `position` offset. No nested `COMPOUND` |
32
59
  | `stopRotation` | `boolean?` | Lock rotation sync to render transform |
33
60
 
61
+ `PhysicsPartParams` can be `RECTANGLE`, `CIRCLE`, `POLYGON`, `TRAPEZOID`, `FROM_VERTICES`, `CAPSULE`, `EDGE`, or `CHAIN`. Capsule / chain / concave vertex parts are flattened into the parent compound.
62
+
34
63
  Runtime fields:
35
64
 
36
65
  - `body: Matter.Body` — set by `PhysicsSystem` after the GameObject enters the scene.
@@ -41,6 +70,8 @@ Events on the `Physics` instance:
41
70
  - `'collisionActive'` — same signature
42
71
  - `'collisionEnd'` — same signature
43
72
 
73
+ Compound / capsule / chain collisions arrive on the **parent** `Physics` (the engine walks `body.parent` so part hits still emit).
74
+
44
75
  ### `PhysicsSystemParams`
45
76
 
46
77
  | Field | Type | Notes |
@@ -48,7 +79,7 @@ Events on the `Physics` instance:
48
79
  | `world` | `DeepPartial<IWorldDefinition>` | **Required**. Must include `gravity: { x, y, scale }` (e.g. `{ x: 0, y: 1, scale: 0.001 }`) |
49
80
  | `resolution` | `number?` | **Must match `RendererSystem.resolution`** to keep bodies aligned with sprites |
50
81
  | `fps` | `number?` | Step rate |
51
- | `isTest` | `boolean?` | Adds a Pixi debug overlay drawn from Matter renderer |
82
+ | `isTest` | `boolean?` | Adds a Pixi debug overlay drawn from Matter renderer (compound parts are outlined individually) |
52
83
  | `mouse` | `{ open, constraint? }?` | Optional Matter mouse constraint |
53
84
  | `element` / `canvas` | optional | Debug renderer mount points |
54
85
 
@@ -66,16 +97,21 @@ Events on the `Physics` instance:
66
97
  body's `position` / `rotation` back into the sibling `Transform`.
67
98
  - Pause / resume: `PhysicsSystem` automatically stops / starts the Matter
68
99
  runner when `Game.pause()` / `Game.resume()` is called.
100
+ - Changing `bodyParams` (deep observer) rebuilds the Matter body.
69
101
 
70
102
  ## Common pitfalls
71
103
 
72
104
  | Symptom | Fix |
73
105
  |---------|-----|
74
- | `gravity` complaint at runtime | Provide `world: { gravity: { x: 0, y: 1, scale: 0.001 } }` (`scale` is required) |
106
+ | `gravity` complaint at runtime | Provide `world: { gravity: { x, y, scale } }` (`scale` is required) |
75
107
  | Bodies misaligned with sprites | `PhysicsSystem.resolution` must match `RendererSystem.resolution` |
76
108
  | Rotation drift | Use `stopRotation: true` on `Physics` if the visual must stay axis-aligned |
77
109
  | Bodies pile up at origin | Set `position: { x, y }` on `Physics` constructor params |
78
110
  | Cannot tap through bodies | Disable mouse constraint or `pointer-events: none` on the canvas overlay |
111
+ | `FROM_VERTICES` / `CHAIN` in the wrong place | Vertices are local to the body origin, not world space |
112
+ | Nested `COMPOUND` throws | Flatten into one `parts` list, or use `CAPSULE` / `CHAIN` as a part |
113
+ | Soft rope expected from `CHAIN` | `CHAIN` is a rigid polyline. Soft ropes need constraints, not this type |
114
+ | Collision events missing on capsule / compound | Listen on the parent `Physics`; do not read `pair.bodyA.component` yourself |
79
115
 
80
116
  ## Minimal example
81
117
 
@@ -98,8 +134,47 @@ ball.addComponent(new Graphics(/* draw a circle */));
98
134
  ball.addComponent(new Physics({ type: PhysicsType.CIRCLE, radius: 25 }));
99
135
  ```
100
136
 
137
+ ### Extra shapes
138
+
139
+ ```ts
140
+ new Physics({
141
+ type: PhysicsType.FROM_VERTICES,
142
+ vertices: [{ x: -40, y: 20 }, { x: 40, y: 20 }, { x: 0, y: -40 }],
143
+ });
144
+
145
+ new Physics({ type: PhysicsType.TRAPEZOID, width: 80, height: 40, slope: 0.4 });
146
+
147
+ new Physics({ type: PhysicsType.CAPSULE, length: 80, radius: 16 });
148
+
149
+ new Physics({
150
+ type: PhysicsType.COMPOUND,
151
+ parts: [
152
+ { type: PhysicsType.RECTANGLE, width: 80, height: 20 },
153
+ { type: PhysicsType.CIRCLE, radius: 16, position: { x: -40, y: 0 } },
154
+ { type: PhysicsType.CIRCLE, radius: 16, position: { x: 40, y: 0 } },
155
+ ],
156
+ });
157
+
158
+ new Physics({
159
+ type: PhysicsType.EDGE,
160
+ from: { x: -100, y: 0 },
161
+ to: { x: 100, y: 20 },
162
+ thickness: 4,
163
+ bodyOptions: { isStatic: true },
164
+ });
165
+
166
+ new Physics({
167
+ type: PhysicsType.CHAIN,
168
+ vertices: [{ x: -200, y: 0 }, { x: 0, y: 40 }, { x: 200, y: 0 }],
169
+ thickness: 4,
170
+ bodyOptions: { isStatic: true },
171
+ });
172
+ ```
173
+
101
174
  ## Verification
102
175
 
103
176
  - `pnpm --filter @combos-fun/plugin-matterjs run build`
104
177
  - Run a 2D example with physics; bodies should fall under gravity, collide,
105
178
  and stay aligned with sprites.
179
+ - For new hulls: a static `CHAIN` / `EDGE` should support falling circles;
180
+ a `CAPSULE` / `COMPOUND` should emit `collisionStart` on the parent `Physics`.
@@ -4,7 +4,7 @@
4
4
  "category": "physics",
5
5
  "dimension": "2d",
6
6
  "isCore": false,
7
- "keywords": ["matter-js", "2d", "physics", "collision", "rigid-body", "gravity"],
7
+ "keywords": ["matter-js", "2d", "physics", "collision", "rigid-body", "gravity", "capsule", "compound", "polygon"],
8
8
  "agentSkill": "./agent-skill.md",
9
9
  "requires": ["@combos-fun/engine", "@combos-fun/plugin-renderer"],
10
10
  "exports": ["PhysicsSystem", "Physics", "PhysicsType"]
@@ -3,17 +3,25 @@
3
3
  var tslib = require('tslib');
4
4
  var engine = require('@combos-fun/engine');
5
5
  var Matter = require('matter-js');
6
+ var decomp = require('poly-decomp');
6
7
  var pixi_js = require('pixi.js');
7
8
 
8
9
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
9
10
 
10
11
  var Matter__default = /*#__PURE__*/_interopDefault(Matter);
12
+ var decomp__default = /*#__PURE__*/_interopDefault(decomp);
11
13
 
12
14
  exports.PhysicsType = void 0;
13
15
  (function (PhysicsType) {
14
16
  PhysicsType["RECTANGLE"] = "rectangle";
15
17
  PhysicsType["CIRCLE"] = "circle";
16
18
  PhysicsType["POLYGON"] = "polygon";
19
+ PhysicsType["TRAPEZOID"] = "trapezoid";
20
+ PhysicsType["FROM_VERTICES"] = "fromVertices";
21
+ PhysicsType["CAPSULE"] = "capsule";
22
+ PhysicsType["COMPOUND"] = "compound";
23
+ PhysicsType["EDGE"] = "edge";
24
+ PhysicsType["CHAIN"] = "chain";
17
25
  })(exports.PhysicsType || (exports.PhysicsType = {}));
18
26
  class Physics extends engine.Component {
19
27
  static { this.componentName = 'Physics'; }
@@ -36,41 +44,184 @@ class Physics extends engine.Component {
36
44
  }
37
45
  }
38
46
 
47
+ const DEFAULT_THICKNESS = 4;
48
+ const MIN_SEGMENT = 0.5;
49
+ let decompRegistered = false;
50
+ function ensureDecomp() {
51
+ if (decompRegistered) {
52
+ return;
53
+ }
54
+ const lib = decomp__default.default?.quickDecomp ? decomp__default.default : decomp__default.default?.default;
55
+ if (lib?.quickDecomp) {
56
+ Matter__default.default.Common.setDecomp(lib);
57
+ }
58
+ decompRegistered = true;
59
+ }
39
60
  class BodiesFactory {
40
61
  constructor() {
41
62
  this.Bodies = Matter__default.default.Bodies;
63
+ ensureDecomp();
42
64
  }
43
65
  create(component) {
44
- let body = null;
45
66
  const { gameObject, bodyParams } = component;
46
67
  const coordinate = this.getCoordinate(gameObject);
47
68
  const x = bodyParams.position ? bodyParams.position.x : coordinate.x;
48
69
  const y = bodyParams.position ? bodyParams.position.y : coordinate.y;
49
- const halfW = (gameObject.transform.size.width * gameObject.transform.scale.x) / 2;
50
- const halfH = (gameObject.transform.size.height * gameObject.transform.scale.y) / 2;
70
+ const transformWidth = gameObject.transform.size.width * gameObject.transform.scale.x;
71
+ const transformHeight = gameObject.transform.size.height * gameObject.transform.scale.y;
51
72
  /** Matter.circle approximates with a polygon; NaN/0 radius breaks vertices. */
52
- const defaultRadius = Math.max(1, Math.min(halfW, halfH));
53
- switch (bodyParams.type) {
73
+ const defaultRadius = Math.max(1, Math.min(transformWidth, transformHeight) / 2);
74
+ const body = this.createShape(bodyParams, {
75
+ x,
76
+ y,
77
+ transformWidth,
78
+ transformHeight,
79
+ defaultRadius,
80
+ }, false);
81
+ if (!body) {
82
+ throw new Error(`[plugin-matterjs] Failed to create body for type ${String(bodyParams.type)}`);
83
+ }
84
+ return body;
85
+ }
86
+ createShape(params, ctx, isPart) {
87
+ const options = params.bodyOptions;
88
+ switch (params.type) {
54
89
  case exports.PhysicsType.RECTANGLE: {
55
- const width = gameObject.transform.size.width * gameObject.transform.scale.x;
56
- const height = gameObject.transform.size.height * gameObject.transform.scale.y;
57
- body = this.Bodies.rectangle(x, y, width, height, bodyParams.bodyOptions);
58
- break;
90
+ const width = params.width ?? ctx.transformWidth;
91
+ const height = params.height ?? ctx.transformHeight;
92
+ return this.Bodies.rectangle(ctx.x, ctx.y, width, height, options);
59
93
  }
60
94
  case exports.PhysicsType.CIRCLE: {
61
- const radius = bodyParams.radius ?? defaultRadius;
62
- body = this.Bodies.circle(x, y, radius, bodyParams.bodyOptions);
63
- break;
95
+ const radius = params.radius ?? ctx.defaultRadius;
96
+ return this.Bodies.circle(ctx.x, ctx.y, radius, options);
64
97
  }
65
98
  case exports.PhysicsType.POLYGON: {
66
- const sides = Math.max(3, bodyParams.sides ?? 6);
67
- const radius = bodyParams.radius ?? defaultRadius;
68
- body = this.Bodies.polygon(x, y, sides, radius, bodyParams.bodyOptions);
69
- break;
99
+ const sides = Math.max(3, params.sides ?? 6);
100
+ const radius = params.radius ?? ctx.defaultRadius;
101
+ return this.Bodies.polygon(ctx.x, ctx.y, sides, radius, options);
102
+ }
103
+ case exports.PhysicsType.TRAPEZOID: {
104
+ const width = params.width ?? ctx.transformWidth;
105
+ const height = params.height ?? ctx.transformHeight;
106
+ const slope = Math.min(Math.max(params.slope ?? 0.5, 0), 0.999);
107
+ return this.Bodies.trapezoid(ctx.x, ctx.y, width, height, slope, options);
70
108
  }
109
+ case exports.PhysicsType.FROM_VERTICES:
110
+ return this.createFromVertices(params, ctx, options);
111
+ case exports.PhysicsType.CAPSULE:
112
+ return this.createCapsule(params, ctx, options);
113
+ case exports.PhysicsType.EDGE:
114
+ return this.createEdge(params, ctx, options);
115
+ case exports.PhysicsType.CHAIN:
116
+ return this.createChain(params, ctx, options);
117
+ case exports.PhysicsType.COMPOUND:
118
+ if (isPart) {
119
+ throw new Error('[plugin-matterjs] Nested COMPOUND parts are not supported');
120
+ }
121
+ return this.createCompound(params, ctx, options);
122
+ default:
123
+ throw new Error(`[plugin-matterjs] Unsupported PhysicsType: ${String(params.type)}`);
124
+ }
125
+ }
126
+ createFromVertices(params, ctx, options) {
127
+ const sets = params.vertexSets ?? (params.vertices ? [params.vertices] : []);
128
+ if (!sets.length || sets.some(set => !set || set.length < 3)) {
129
+ throw new Error('[plugin-matterjs] FROM_VERTICES requires vertices (or vertexSets) with at least 3 points each');
130
+ }
131
+ ensureDecomp();
132
+ const body = this.Bodies.fromVertices(ctx.x, ctx.y, sets, options, true);
133
+ if (!body) {
134
+ throw new Error('[plugin-matterjs] FROM_VERTICES produced an empty body');
71
135
  }
72
136
  return body;
73
137
  }
138
+ createCapsule(params, ctx, options) {
139
+ const radius = Math.max(1, params.radius ?? ctx.defaultRadius);
140
+ const length = Math.max(radius * 2, params.length ?? ctx.transformWidth);
141
+ const shaft = length - radius * 2;
142
+ if (shaft < MIN_SEGMENT) {
143
+ return this.Bodies.circle(ctx.x, ctx.y, radius, options);
144
+ }
145
+ const parts = [
146
+ this.Bodies.rectangle(ctx.x, ctx.y, shaft, radius * 2),
147
+ this.Bodies.circle(ctx.x - shaft / 2, ctx.y, radius),
148
+ this.Bodies.circle(ctx.x + shaft / 2, ctx.y, radius),
149
+ ];
150
+ return Matter__default.default.Body.create({ ...options, parts });
151
+ }
152
+ createEdge(params, ctx, options) {
153
+ if (!params.from || !params.to) {
154
+ throw new Error('[plugin-matterjs] EDGE requires from and to');
155
+ }
156
+ const x1 = ctx.x + params.from.x;
157
+ const y1 = ctx.y + params.from.y;
158
+ const x2 = ctx.x + params.to.x;
159
+ const y2 = ctx.y + params.to.y;
160
+ const dx = x2 - x1;
161
+ const dy = y2 - y1;
162
+ const len = Math.hypot(dx, dy);
163
+ if (len < MIN_SEGMENT) {
164
+ throw new Error('[plugin-matterjs] EDGE from/to are too close');
165
+ }
166
+ const thickness = Math.max(1, params.thickness ?? DEFAULT_THICKNESS);
167
+ const { angle: _ignored, ...rest } = options ?? {};
168
+ return this.Bodies.rectangle((x1 + x2) / 2, (y1 + y2) / 2, len, thickness, {
169
+ ...rest,
170
+ angle: Math.atan2(dy, dx),
171
+ });
172
+ }
173
+ createChain(params, ctx, options) {
174
+ const verts = params.vertices;
175
+ if (!verts || verts.length < 2) {
176
+ throw new Error('[plugin-matterjs] CHAIN requires vertices with at least 2 points');
177
+ }
178
+ const thickness = Math.max(1, params.thickness ?? DEFAULT_THICKNESS);
179
+ const parts = [];
180
+ for (let i = 0; i < verts.length - 1; i++) {
181
+ const a = verts[i];
182
+ const b = verts[i + 1];
183
+ const x1 = ctx.x + a.x;
184
+ const y1 = ctx.y + a.y;
185
+ const x2 = ctx.x + b.x;
186
+ const y2 = ctx.y + b.y;
187
+ const dx = x2 - x1;
188
+ const dy = y2 - y1;
189
+ const len = Math.hypot(dx, dy);
190
+ if (len < MIN_SEGMENT) {
191
+ continue;
192
+ }
193
+ parts.push(this.Bodies.rectangle((x1 + x2) / 2, (y1 + y2) / 2, len, thickness, { angle: Math.atan2(dy, dx) }));
194
+ }
195
+ if (!parts.length) {
196
+ throw new Error('[plugin-matterjs] CHAIN has no usable segments');
197
+ }
198
+ return Matter__default.default.Body.create({ ...options, parts });
199
+ }
200
+ createCompound(params, ctx, options) {
201
+ const partParams = params.parts;
202
+ if (!partParams?.length) {
203
+ throw new Error('[plugin-matterjs] COMPOUND requires a non-empty parts array');
204
+ }
205
+ const parts = [];
206
+ for (let i = 0; i < partParams.length; i++) {
207
+ const part = partParams[i];
208
+ const ox = ctx.x + (part.position?.x ?? 0);
209
+ const oy = ctx.y + (part.position?.y ?? 0);
210
+ const partBody = this.createShape(part, { ...ctx, x: ox, y: oy }, true);
211
+ parts.push(...this.asParts(partBody));
212
+ }
213
+ if (!parts.length) {
214
+ throw new Error('[plugin-matterjs] COMPOUND produced no parts');
215
+ }
216
+ return Matter__default.default.Body.create({ ...options, parts });
217
+ }
218
+ /** Unwrap a compound wrapper so it can be merged into another parent. */
219
+ asParts(body) {
220
+ if (body.parts && body.parts.length > 1) {
221
+ return body.parts.slice(1);
222
+ }
223
+ return [body];
224
+ }
74
225
  getCoordinate(gameObject) {
75
226
  const x = gameObject.transform.position.x + gameObject.transform.anchor.x * gameObject.parent.transform.size.width;
76
227
  const y = gameObject.transform.position.y + gameObject.transform.anchor.y * gameObject.parent.transform.size.height;
@@ -120,17 +271,20 @@ class MatterPixiDebugRenderer {
120
271
  if (body.render?.visible === false) {
121
272
  continue;
122
273
  }
123
- const verts = body.vertices;
124
- if (!verts?.length) {
125
- continue;
126
- }
127
- const v0 = verts[0];
128
- g.moveTo(v0.x, v0.y);
129
- for (let i = 1; i < verts.length; i++) {
130
- g.lineTo(verts[i].x, verts[i].y);
274
+ const pieces = body.parts && body.parts.length > 1 ? body.parts.slice(1) : [body];
275
+ for (let p = 0; p < pieces.length; p++) {
276
+ const verts = pieces[p].vertices;
277
+ if (!verts?.length) {
278
+ continue;
279
+ }
280
+ const v0 = verts[0];
281
+ g.moveTo(v0.x, v0.y);
282
+ for (let i = 1; i < verts.length; i++) {
283
+ g.lineTo(verts[i].x, verts[i].y);
284
+ }
285
+ g.closePath();
286
+ g.stroke({ width: 1, color: 0x00e676, alpha: 0.9 });
131
287
  }
132
- g.closePath();
133
- g.stroke({ width: 1, color: 0x00e676, alpha: 0.9 });
134
288
  }
135
289
  }
136
290
  destroy() {
@@ -143,6 +297,21 @@ class MatterPixiDebugRenderer {
143
297
  }
144
298
  }
145
299
 
300
+ /** Collision pairs often report a compound part, not the parent body. */
301
+ function resolveLinkedComponent(body) {
302
+ let current = body;
303
+ while (current) {
304
+ const linked = current;
305
+ if (linked.component) {
306
+ return linked.component;
307
+ }
308
+ if (!current.parent || current.parent === current) {
309
+ break;
310
+ }
311
+ current = current.parent;
312
+ }
313
+ return undefined;
314
+ }
146
315
  class PhysicsEngine {
147
316
  constructor(game, options) {
148
317
  this.debugRenderer = null;
@@ -216,19 +385,13 @@ class PhysicsEngine {
216
385
  add(component) {
217
386
  const body = this.createBodies(component);
218
387
  this.World.add(this.engine.world, [body]);
219
- component.body = body;
220
- component.Body = Matter__default.default.Body;
221
- component.PhysicsEngine = this.engine;
222
- component.Constraint = this.Constraint;
223
- component.mouseConstraint = this.mouseConstraint;
224
- component.World = this.World;
225
- body.component = component;
388
+ this.linkComponent(component, body);
226
389
  }
227
390
  change(component) {
228
391
  const newBody = this.createBodies(component);
229
392
  this.World.remove(this.engine.world, component.body, true);
230
393
  this.World.add(this.engine.world, [newBody]);
231
- component.body = newBody;
394
+ this.linkComponent(component, newBody);
232
395
  }
233
396
  remove(component) {
234
397
  this.World.remove(this.engine.world, component.body, true);
@@ -236,8 +399,20 @@ class PhysicsEngine {
236
399
  }
237
400
  createBodies(params) {
238
401
  const body = this.bodiesFactory.create(params);
402
+ if (!body) {
403
+ throw new Error('[plugin-matterjs] Failed to create Matter body');
404
+ }
239
405
  return body;
240
406
  }
407
+ linkComponent(component, body) {
408
+ component.body = body;
409
+ component.Body = Matter__default.default.Body;
410
+ component.PhysicsEngine = this.engine;
411
+ component.Constraint = this.Constraint;
412
+ component.mouseConstraint = this.mouseConstraint;
413
+ component.World = this.World;
414
+ body.component = component;
415
+ }
241
416
  initCollisionEvents() {
242
417
  this.collisionEvents.forEach(eventName => {
243
418
  Matter__default.default.Events.on(this.engine, eventName, (event) => {
@@ -245,8 +420,11 @@ class PhysicsEngine {
245
420
  for (let i = 0; i < pairs.length; i++) {
246
421
  const pair = pairs[i];
247
422
  const { bodyA, bodyB } = pair;
248
- const componentA = bodyA.component;
249
- const componentB = bodyB.component;
423
+ const componentA = resolveLinkedComponent(bodyA);
424
+ const componentB = resolveLinkedComponent(bodyB);
425
+ if (!componentA || !componentB) {
426
+ continue;
427
+ }
250
428
  componentA.emit(eventName, componentB.gameObject, componentA.gameObject);
251
429
  componentB.emit(eventName, componentA.gameObject, componentB.gameObject);
252
430
  }
@@ -383,7 +561,7 @@ var PhysicsSystem = PhysicsSystem$1;
383
561
  /** Auto-generated by scripts/build-package.mjs — do not edit. */
384
562
  Object.assign(PhysicsSystem, {
385
563
  packageName: "@combos-fun/plugin-matterjs",
386
- packageVersion: "0.0.42",
564
+ packageVersion: "0.0.44",
387
565
  });
388
566
 
389
567
  exports.Physics = Physics;