# `@combos-fun/plugin-matterjs` — Agent notes

**2D** physics for Combos Fun, backed by `matter-js` ^0.20. Use
`@combos-fun/plugin-cannon` for **3D** physics.

## When to read

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).

## Public API

```ts
import {
  PhysicsSystem,
  Physics,
  PhysicsType,
  type PhysicsParams,
  type PhysicsPartParams,
  type PhysicsVertex,
  type PhysicsBodyOptions,
  type PhysicsSystemParams,
} from '@combos-fun/plugin-matterjs';
```

### `PhysicsType`

| Value | Matter mapping | Notes |
|-------|----------------|-------|
| `RECTANGLE` | `Bodies.rectangle` | Size = `width`/`height`, else `Transform2D.size × scale` |
| `CIRCLE` | `Bodies.circle` | `radius`; else `min(halfW, halfH)`, at least 1 |
| `POLYGON` | `Bodies.polygon` | Regular n-gon. `sides` (min 3, default 6) + `radius` |
| `TRAPEZOID` | `Bodies.trapezoid` | `width`/`height` (same fallback as rectangle) + `slope` (0–0.999, default 0.5) |
| `FROM_VERTICES` | `Bodies.fromVertices` | Arbitrary polygon. Concave shapes are decomposed via `poly-decomp` |
| `CAPSULE` | rectangle + 2 circles, then `Body.create({ parts })` | Stadium. `length` is full length including caps; `radius` is cap radius. `length <= 2r` becomes a circle |
| `COMPOUND` | `Body.create({ parts })` | One rigid body from `parts[]`. Nested `COMPOUND` is rejected |
| `EDGE` | thin `Bodies.rectangle` | Segment `from` → `to`. Orientation comes from the endpoints (not `bodyOptions.angle`) |
| `CHAIN` | compound of thin rectangles | Rigid polyline through `vertices`. **Not** a soft rope |

`CHAIN` / `EDGE` are static-terrain style colliders. A swinging rope needs multiple bodies + constraints; do not fake that with `CHAIN`.

### Coordinates

`vertices`, `vertexSets`, `from`, `to`, and each part's `position` are **local** to the **body origin** (`bodyParams.position`, or `Transform2D.position + Transform2D.anchor × parent Transform2D.size`). Do not pass world-space vertices.

### `Physics` Component params

| Field | Type | Notes |
|-------|------|-------|
| `type` | `PhysicsType?` | Body shape. Required when the object enters the scene |
| `bodyOptions` | `PhysicsBodyOptions?` | Matter Body options: `isStatic`, `restitution`, `frictionAir`, `density`, `friction`, `angle`, ... |
| `position` | `{x, y}?` | Body origin in world space. If omitted, taken from the GameObject transform |
| `width` / `height` | `number?` | Rectangle / trapezoid / capsule fallback size |
| `sides` / `radius` | `number?` | Regular polygon / circle / capsule cap |
| `slope` | `number?` | Trapezoid only. Clamped to `< 1` |
| `length` | `number?` | Capsule full length (including caps). Default = transform width |
| `vertices` | `{x, y}[]?` | `FROM_VERTICES` (one contour) or `CHAIN` (polyline) |
| `vertexSets` | `{x, y}[][]?` | `FROM_VERTICES` with multiple contours. Takes precedence over `vertices` |
| `from` / `to` | `{x, y}?` | `EDGE` endpoints, local to the body origin |
| `thickness` | `number?` | `EDGE` / `CHAIN` segment thickness. Default 4, minimum 1 |
| `parts` | `PhysicsPartParams[]?` | `COMPOUND` only. Same shape fields as above, plus a local `position` offset. No nested `COMPOUND` |
| `stopRotation` | `boolean?` | Lock rotation sync to render transform |

`PhysicsPartParams` can be `RECTANGLE`, `CIRCLE`, `POLYGON`, `TRAPEZOID`, `FROM_VERTICES`, `CAPSULE`, `EDGE`, or `CHAIN`. Capsule / chain / concave vertex parts are flattened into the parent compound.

Runtime fields:

- `body: Matter.Body` — set by `PhysicsSystem` after the GameObject enters the scene.

Events on the `Physics` instance:

- `'collisionStart'` — `(otherGameObject, selfGameObject) => void`
- `'collisionActive'` — same signature
- `'collisionEnd'` — same signature

Compound / capsule / chain collisions arrive on the **parent** `Physics` (the engine walks `body.parent` so part hits still emit).

### `PhysicsSystemParams`

| Field | Type | Notes |
|-------|------|-------|
| `world` | `DeepPartial<IWorldDefinition>` | **Required**. Must include `gravity: { x, y, scale }` (e.g. `{ x: 0, y: 1, scale: 0.001 }`) |
| `resolution` | `number?` | Default `1`; **must match `RendererSystem.resolution`** |
| `fps` | `number?` | Default `60`; Matter runner step rate |
| `isTest` | `boolean?` | Adds a Pixi debug overlay drawn from Matter renderer (compound parts are outlined individually) |
| `mouse` | `{ open, constraint? }?` | Optional Matter mouse constraint |
| `element` / `canvas` | optional | Debug renderer mount points |
| `deltaSampleSize` | `number?` | When `> 1`, enables Matter `frameDeltaSmoothing` |

## Required setup

- Add `RendererSystem` (from `plugin-renderer`) **before** `PhysicsSystem`.
- Match `resolution` between renderer and physics.
- Body origin uses **`Transform2D.anchor`**, not `origin`. `Physics.update()` also
  forces `anchor` to `{ x: 0, y: 0 }` each frame after the body exists. Center a
  sprite with `Transform2D.origin`; do not expect `origin` to move the Matter body.
- Bodies are created lazily when the `GameObject` is added to a scene.

## Runtime behaviour

- The engine runs all Component `update`/`lateUpdate` hooks before System updates.
  `Physics.update()` first copies the body's current pose into `Transform2D`, so
  the visual reads the result of the previous `PhysicsSystem` step.
- Later in that frame, `PhysicsSystem.update()` processes body changes and
  advances Matter via `Runner.tick`. Matter collision events fire during that
  step; the resulting pose is rendered by `Physics.update()` on the next frame.
- Matter writes body world coordinates directly into the GameObject's
  `Transform2D`; unlike Cannon, it does **not** convert world pose to local pose
  for a parented object. Put physics GameObjects directly under `scene` unless
  you intentionally handle the coordinate mismatch yourself.
- Pause / resume: `PhysicsSystem` automatically stops / starts the Matter
  runner when `Game.pause()` / `Game.resume()` is called.
- Changing `bodyParams` (deep observer) rebuilds the Matter body.

## Common pitfalls

| Symptom | Fix |
|---------|-----|
| `gravity` complaint at runtime | Provide `world: { gravity: { x, y, scale } }` (`scale` is required) |
| Bodies misaligned with sprites | `PhysicsSystem.resolution` must match `RendererSystem.resolution` |
| Rotation drift | Use `stopRotation: true` on `Physics` if the visual must stay axis-aligned |
| Bodies pile up at origin | Set `position: { x, y }` on `Physics` constructor params |
| Cannot tap through bodies | Disable mouse constraint or `pointer-events: none` on the canvas overlay |
| `FROM_VERTICES` / `CHAIN` in the wrong place | Vertices are local to the body origin, not world space |
| Parented body drifts or jumps | Matter has no world→local sync; attach the physics GameObject directly to `scene` |
| Nested `COMPOUND` throws | Flatten into one `parts` list, or use `CAPSULE` / `CHAIN` as a part |
| Soft rope expected from `CHAIN` | `CHAIN` is a rigid polyline. Soft ropes need constraints, not this type |
| Collision events missing on capsule / compound | Listen on the parent `Physics`; do not read `pair.bodyA.component` yourself |

## Minimal example

```ts
import { Game, GameObject } from '@combos-fun/engine';
import { RendererSystem, Transform2D } from '@combos-fun/plugin-renderer';
import { GraphicsSystem, Graphics } from '@combos-fun/plugin-renderer-graphics';
import { PhysicsSystem, Physics, PhysicsType } from '@combos-fun/plugin-matterjs';

const canvas = document.querySelector<HTMLCanvasElement>('canvas')!;
const game = new Game({
  systems: [
    new RendererSystem({ canvas, width: 750, height: 1334, resolution: 1 }),
    new GraphicsSystem(),
    new PhysicsSystem({
      resolution: 1,
      fps: 60,
      world: { gravity: { x: 0, y: 1, scale: 0.001 } },
    }),
  ],
  onSystemsBootstrapComplete: (readyGame, error) => {
    if (error) return;
    const ball = new GameObject('ball');
    ball.addComponent(new Transform2D({
      size: { width: 50, height: 50 },
      position: { x: 375, y: 100 },
      origin: { x: 0.5, y: 0.5 },
    }));
    const g = ball.addComponent(new Graphics());
    g.graphics.circle(25, 25, 25).fill({ color: 0xff6b6b });
    ball.addComponent(new Physics({ type: PhysicsType.CIRCLE, radius: 25 }));
    readyGame.scene.addChild(ball);
  },
});
```

## Verification

- `pnpm --filter @combos-fun/plugin-matterjs run build`
- Run a 2D example and verify falling, collisions, and render alignment.
