Abstracted the "hydrodynamic" steering logic which set me up to reuse it for -- a yacht! It just picks a random target and steers towards it for now, but that's enough to start testing things like relative speeds, masses etc. I am also visualizing collision locations and intensities as little red circles. The results are a bit surprising, some collisions I would expect to be big are not, but I am only looking at collisionStart event and may need to get more sophisticated about things.

The collision stuff makes great use of the new Ajeeb Signals I am basing this game on. In the past I used to only be able to write coroutines that waited for the next frame, so anything beyond that would have to poll some value once per frame. Not ideal. Now with signals I can trivially wrap anything that emits events as a signal and get a waitable value! In my physics module I have

export function collisionSignals(engine) {
    const start = new Signal()
    const active = new Signal()
    const end = new Signal()
    Matter.Events.on(engine, 'collisionStart', start.emit)
    Matter.Events.on(engine, 'collisionActive', active.emit)
    Matter.Events.on(engine, 'collisionEnd', end.emit)
    return { start, active, end }
}

which gives me signals for all of Matter.js's collision events. Then in my main module I can just do:

const app = new PIXI.Application()
app.ticker.add(ticker => bus.frame.emit(ticker.deltaMS))

// ...

const engine = Matter.Engine.create({ gravity: { x: 0, y: 0 } })
const collision = physics.collisionSignals(engine)

// ...

go(function* () {
  while (true) {
    const c = yield collision.start // <== wait for a collision event
    for (const p of c.pairs)
      go(visualizeCollision(p))
  }
})

function* visualizeCollision(c, life = 1000) {
  const x = c.collision.supports[0].x
  const y = c.collision.supports[0].y
  const r = c.collision.depth
  while (life > 0) {
    const delta = yield bus.frame
    gizmos.circle(x, y, r * 2, 'rgba(255, 0, 0, 0.5)')
    life -= delta
  }
}

Where go runs a coroutine up to its first yield (basically runs the coroutine "in the background"). Very tidy!

And here it is:

If it's weird to play in an iframe try the full thing. Only tested on mouse + desktop.