# Gravity

# Gravity

Package `@ferret/physics-gravity`, capability name `gravity`, phase `field`.

## What it is for

Without this capability the kernel pulls everything down with one constant. With it, **gravity is a field**: one law that the cannonball, the character and every other capability read, which can change from place to place. Use it for low-gravity rooms, zero-g, a moon you can fall onto, a pit that pulls things in, a level set on another planet. It is not needed for a game where "down at 9.81" is all you want: the kernel's own gravity does that.

## The model

At a point `p` the gravity acceleration is

```
g(p) = ( field vector of the narrowest scope  +  Σ attractor pulls at p )  ×  scale of the narrowest scope
```

and the force on a body is `m · g(p) · body.gravityScale`. Three dials, all independent:

| Dial | Where | What it does |
|---|---|---|
| `vector` | scoped value (world, level, zone, entity) | the uniform field, m/s², default `[0, -9.81, 0]` |
| `scale` | scoped value | multiplies the whole field: 0 = weightless, 0.16 = the moon |
| `gravityScale` | the body (`RigidBody` parameter) | per body, on top |

The force is evaluated at **every sub-step position**, so a fast or orbiting body feels the field where it *is*, not where it started the step. While `gravity` is on, the kernel's own constant is set to zero and restored when you switch the capability off.

## First use

```ts
import { PhysicsHost } from '@ferret/physics';
import { gravity } from '@ferret/physics-gravity';

const host = world.addService(new PhysicsHost({ capabilities: [gravity()] }));
host.enable('gravity');
host.config('gravity').set('world', 'vector', [0, -3.7, 0]);        // Mars

const rock = world.spawn('rock', { position: new BABYLON.Vector3(0, 100, 0) });
rock.add('RigidBody', { shape: 'sphere', radius: 0.2, mass: 1 });
world.run(2);
```

## Recipes

### A low-gravity room

A zone changes the field for bodies inside it and only them. Zones come from the `ZoneService`; a body is "inside" by its position.

```ts
import { PhysicsHost } from '@ferret/physics';
import { gravity } from '@ferret/physics-gravity';
import { ZoneService } from '@ferret/zones';

const host = world.addService(new PhysicsHost({ capabilities: [gravity()] }));
const zones = world.addService(new ZoneService());
zones.add({ name: 'moon-room', shape: { kind: 'box', centre: [0, 50, 0], size: [10, 200, 10] } });
host.enable('gravity');
host.config('gravity').set('zone', 'vector', [0, -1.6, 0], 'moon-room');

const ball = (name: string, x: number) => {
  const e = world.spawn(name, { position: new BABYLON.Vector3(x, 100, 0) });
  e.add('RigidBody', { shape: 'sphere', radius: 0.2, mass: 1 });
  return e;
};
const inside = ball('in', 0), outside = ball('out', 30);
world.run(2);
```

The same from a level manifest: `"gravity": { "vector": [0, -1.6, 0] }` for the level, or per-zone values through the `gravity.set` command with `scope: "zone"` and the zone's name.

### Weightless: one object, one room, the whole world

- one entity: `host.config('gravity').set('entity', 'scale', 0, entity.id)`;
- one body at creation: `gravityScale: 0` in the `RigidBody` parameters;
- a zone: `set('zone', 'scale', 0, 'zero-g')`;
- everything: `set('world', 'scale', 0)`.

`scale` and `gravityScale` multiply, so `0.5` and `0.5` give a quarter.

### A planet, a moon, a pit

A **point attractor** pulls toward a centre, inverse square. Give it either the gravitational parameter `mu = G·M` (m³/s²) or the **surface gravity** and the **radius** of the body, which is easier to author: "3 m/s² at 50 m".

```ts
import { PhysicsHost } from '@ferret/physics';
import { gravity } from '@ferret/physics-gravity';

const g = gravity();
const host = world.addService(new PhysicsHost({ capabilities: [g] }));
host.enable('gravity');
host.config('gravity').set('world', 'vector', [0, 0, 0]);                       // no uniform field: only the planet
g.addAttractor({ id: 'rock', position: [0, 0, 0], surfaceGravity: 3, radius: 50 });
const onSurface = g.at(host, [50, 0, 0]);
const twiceAway = g.at(host, [100, 0, 0]);
```

Attractor options: `position` (a fixed point) **or** `entity` (the centre follows a node: a moving moon), `softening` (below this distance the pull stops growing; default 0.1 × radius, or 0.5 m, so nothing divides by zero at the centre), `range` (beyond it the attractor does nothing: keep a small pit from pulling the whole level).

A circular orbit needs speed `v = √(mu / r)`. Orbits are good for decoration and puzzles; do not rely on one lasting for hours.

### Reading gravity from your own code

`host.fields.gravity(p, ctx?)` returns the acceleration at a point **while the capability is on**. The character controller uses it when present and keeps its own (stronger, game-feel) gravity when it is off. Your own code should do the same: ask the field, fall back to a default.

```ts
const g = host.fields.gravity?.([x, y, z]) ?? [0, -9.81, 0];
```

## Parameters

| Value | Scope | Unit | Default | Safe range | Effect |
|---|---|---|---|---|---|
| `vector` | world, level, zone, entity | m/s² | `[0, -9.81, 0]` | any | the uniform field; `[0,0,0]` for attractors only |
| `scale` | world, level, zone, entity | – | 1 | 0 – 10 | multiplies the whole field at that point |

| Attractor field | Meaning |
|---|---|
| `id` | name, for removal |
| `position` / `entity` | where the centre is |
| `mu` | G·M, m³/s²; or `surfaceGravity` and `radius` instead |
| `softening` | metres; the pull is flat inside it |
| `range` | metres; zero pull beyond |

## Commands

`gravity.at {position, entity?}` returns the vector; `gravity.set {vector, scope?, id?}`; `gravity.scale {scale, scope?, id?}`; `gravity.attractor.add {id, position | entity, mu | surfaceGravity+radius, softening?, range?}`; `gravity.attractor.remove {id}`. An agent can build a planet with these alone.

## Interplay

- **`media`**: buoyancy uses the field's direction and strength, so water in a zone with lower gravity floats things higher. A vessel computes its own weight and buoyancy and is marked `exempt` from `gravity` and `media`; see vehicles (skill `ferret-physics-vehicles`).
- **`heat`**, **`liquids`**: convection and the liquid's surface read the field too.
- **Characters and AI agents**: read `fields.gravity` when on.
- The `gravity` capability does not move characters by itself; the character's movement integrates the vector it reads.

## Budget

One vector per body per step plus one term per attractor. Hundreds of bodies and a few attractors are far below a millisecond; do not add an attractor per rock (use one planet).

## Debugging

| Symptom | Cause |
|---|---|
| A body does not fall | `scale` is 0 at its scope (entity, zone or world), its `gravityScale` is 0, it is `static`, or it is `exempt` |
| Gravity is doubled | the capability is off and the kernel's own is on **and** you added your own force: pick one |
| A zone has no effect | the world has no `ZoneService`, the zone's box does not contain the body's *position* (not its extent), or an entity-scope value overrides it |
| `attractor "x" needs "mu", or "surfaceGravity" and "radius"` | give one of the two forms |
| A body shoots away near the centre | softening is smaller than the body's distance steps: raise it |
| The character keeps its own gravity | the capability is off; the character only reads the field while it is on |

## Reference

`gravity`, `GravityCapability`, `Attractor`, `GRAVITY_DEFAULTS`, `PhysicsFields`, `@ferret/physics-gravity`.
