# Generation defaults — build through the packs

> ## ⚠️ THE IMPORT PATH DEPENDS ON YOUR LAYOUT — get this right first
>
> **The rule is about the URL, not the disk.** `serve` mounts the packs at `/medium/…` whatever the
> layout, so every import just has to *resolve to that URL*. What changes is how many `../` get you
> to the site root from your source file.
>
> | You installed with | Your game source is at | Import packs as |
> |---|---|---|
> | **`npx devtune init`** (this is you) | `<project>/src/` | **`../medium/physics/pack.js`** |
> | the DevTune monorepo | `product/games/<name>/src/` | `../../../medium/physics/pack.js` |
>
> ⚠️ **Using the monorepo path in a `.devtune` install is a FATAL, not a warning.** `verify` resolves
> relative imports on disk, and `../../../medium/…` from `<project>/src/` lands *above your project* —
> it reports `import-outside-serve` and, if you push past it, the pack 404s and the page is blank.
> MEASURED both ways on a real `devtune init` project, 2026-08-05. This page taught only the monorepo
> path until then, and an outside reviewer building a real game hit exactly this.
>
> Rapier URL when served with the monorepo desk: `/__devtune/shell/vendor/rapier.mjs`.

**Read [`STACK.md`](STACK.md) first.** That is the contract: `thing` · `bind` · `collide` ·
`bindAll`, and the five rules. This page says *what to build the game out of* — the default medium
for physics, movement and camera — so a generated game has a stable dial vocabulary instead of
inventing one per title.

Pack detail lives with the medium folders:
[`../medium/physics/PHYSICS.md`](../medium/physics/PHYSICS.md) ·
[`../medium/movement/MOVEMENT.md`](../medium/movement/MOVEMENT.md) ·
[`../medium/camera/CAMERA.md`](../medium/camera/CAMERA.md).

---

## ⭐ The one rule that decides whether any of this is tunable

> **Declare the tune at YOUR call site, and pass it in. Never let a pack build it for you.**

All three packs will construct a settings object if you do not give them one. **Take the fallback
and the values do not reach the desk at all.** Measured through the real transform:

```
thing(`camera.${id}`, { distance: 6, fov: 55 })   ->  keep targets: NONE   rewritten: false
deps.thing('physics.world', { gravityY: -9.81 })  ->  keep targets: NONE   rewritten: false
thing('camera.player',      { distance: 6, fov: 55 })
                                                  ->  keep targets: camera.player.distance,
                                                                    camera.player.fov
```

Two different reasons, one outcome. A **template-literal id** cannot be read when the file is
parsed, and a **namespaced call** (`deps.thing(…)`) is not a contract call as far as the scanner is
concerned. Either way the callee is never rewritten, so the declaration goes to the game's own local
shim and the desk never hears about it. Nothing throws. The game plays perfectly.

⚠️ `physics/pack.js` describes its own fallback as *"session-only for Keep"*. **Measured, it is
weaker than that: the value is not on the desk at all.** Same conclusion either way — always pass
`tune`.

So every pack construction looks like this:

```js
const camTune = thing('camera.player', {   // ← YOUR file, YOUR literal, YOUR Keep target
  distance: 6, height: 2.2, shoulderOffset: 0.55, fov: 55,
  positionLag: 0.12, pitchMin: -35, pitchMax: 65, lookSensitivity: 0.0035,
}, {
  distance: [1, 20], fov: [25, 110],       // ← ranges are yours too. See below.
});
cams.createFollowCamera('player', { tune: camTune });
```

## ⚠️ The packs declare no ranges, and that is correct

Zero `[min, max]` pairs across all three packs — verified. A pack cannot know what *your* game means
by "fast", so it refuses to guess, and every pack value arrives as a rangeless stepper.

**Authoring ranges is the game's job, at the game's call site.** Declare one only where you actually
intend the ends; leave it off otherwise and the desk says `NO RANGE DECLARED`, which is honest.
Never add a range to make the surface look tidier.

---

## The three packs

### Physics — `medium/physics/pack.js`

```js
const world = await ensurePhysicsWorld({ thing, worldTune, rapierUrl });
world.staticBody(id, { mesh, tune, position, shape });
world.dynamicBody(id, { mesh, tune, position, shape });
// shape: 'box' | 'ball' | 'compound' | 'convex' | 'trimesh' (trimesh static-only)
// compound: parts: [{ shape, size|halfExtents|radius, translation }]
// convex: vertices · trimesh: vertices + indices (capped — see SHAPE)
world.bindMesh(id, mesh)
world.setPose(id, position, quaternion?)   // design place — desk rings this via __devtunePhysicsPack
world.applyOwnedProps() · syncMeshes() · stepPhysics(dt) · removeBody(id) · destroy() · listBodies()
```

| thing | owned keys |
|---|---|
| `physics.world` | `gravityX` `gravityY` `gravityZ` |
| `physics.body.<id>` | `mass` `friction` `restitution` `linearDamping` `angularDamping` `sizeX/Y/Z` or `radius` |

**Construction (not dials):** collider inset equals contact skin so mesh bottoms sit flush; ball meshes
must be **unit-radius** (`SphereGeometry(1)`), pack scales by `radius`. Call `collide(id, shape)` so
Make Solid does not dual-own pack bodies.

**Use it for every interactive solid.** Not a hand-rolled AABB push-out, and not a mesh-only "fake
physics" — those are what make a title need archaeology later.

#### Physics is being deepened in rungs — build against what exists today

Authority: [`../medium/physics/PHYSICS.md`](../medium/physics/PHYSICS.md).

| rung | what it buys a designer | status |
|---|---|---|
| **1 — contact honesty** | less clipping and melt-through. Skin, contact margin, sleep, layer filters, capsule-vs-box | ✅ accepted (residual owned by 2) |
| **2 — shapes match art** | mesh ≈ solid: compound / convex / static trimesh under pack API; inset = skin; ball unit-radius scale; design `setPose` | ✅ accepted 2026-08-03 |
| **3 — simple joints** | doors, lids, springs. Hinge / fixed / spring, **if generation needs them** | not released |

⭐ **None of this changes how you construct.** A rung deepens the solver under the pack; it does not
add a second way to make something solid. `ensurePhysicsWorld` → `staticBody` / `dynamicBody` is the
API at every rung, and **the rule at the top of this page does not move: declare each `tune` at YOUR
call site and pass it in.** A deeper solver with an unownable tune is a better simulation nobody can
turn.

⚠️ **Do not build against a rung that has not landed.** If your game needs a hinge today, it does not
have one — model it, do not reach past the pack for raw Rapier. Raw-Rapier bodies are invisible to
the desk and unowned by anything, which is the whole failure the packs exist to remove.

⚠️ **Clipping is a known residual, not something to work around in game code.** Adding your own
push-out on top of the pack is a second owner of one overlap — the exact thing
[`DUAL_WORLD.md`](DUAL_WORLD.md) forbids. Report it instead.

### Feel layers (PD2) — namespaced things, not one CONFIG

| Layer | Example thing ids | Owns |
|---|---|---|
| **World / physics** | `physics.world`, `physics.body.<id>` | gravity, mass, friction, size |
| **Movement** | `movement.<id>` | speed, jump, capsule, steps |
| **Camera** | `camera.<id>` | distance, fov, lag, pitch |
| **Game systems** | `shift`, `cargo`, `wind`, `pad.hub` | mode-specific feel |

Each system declares **its own** `thing()` at its call site. Do **not** import one `CONFIG` object
shared by every file — Keep cannot write through an import hop, and the desk becomes one opaque bag.

### Feel snapshot (PD3) — identity merge

```js
/* `../medium/…` in a devtune-init project; `../../../medium/…` in the monorepo — see the top. */
import { applyFeelSnapshot, captureFeelSnapshot } from '../medium/feel-snapshot.js';

const targets = new Map([
  ['movement.courier', moveTune],
  ['physics.world', worldTune],
]);
/* Save */
const snap = captureFeelSnapshot(targets);
/* Load — mutates the SAME objects the game already re-reads */
applyFeelSnapshot(targets, snap);
```

Never `moveTune = { ...snap }` — that breaks live dials and Keep identity.

### Movement — `medium/movement/pack.js`

```js
const movement = createMovementPack({ thing, physics });
movement.createCharacter(id, { mesh, position, tune });
movement.setInput(id, { x, z, jump }) · step(dt) · getPosition(id) · bindMesh · remove · destroy · list
```

| thing | owned keys |
|---|---|
| `movement.<id>` | `maxSpeed` `accel` `decel` `airControl` `jumpForce` `fallMultiplier` `coyoteTime` `jumpBuffer` `height` `radius` `stepHeight` `maxSlope` |

Model A: a kinematic capsule over the physics solids. **One fixed clock** — `movement.step(dt)`
steps the physics world too. Do not also call `stepPhysics` for the same frame.

### Camera — `medium/camera/pack.js` · **API FROZEN**

```js
const cams = createCameraPack({ thing, getTargetPosition, camera });
cams.createFollowCamera(id, { tune, yaw, pitch });
cams.setLookInput(id, { dx, dy }) · setYaw · getYaw · getPitch · setActive · step(dt) · destroy · list
```

| thing | owned keys |
|---|---|
| `camera.<id>` | `distance` `height` `shoulderOffset` `fov` `positionLag` `pitchMin` `pitchMax` `lookSensitivity` |

The camera consumes `getTargetPosition()` and **never writes the character transform.** V1 has no
collision pull-in — a recorded residual, not a gap to paper over.

---

## Ownership, in one table

Two writers on one thing is the recurring failure. This is who writes what:

| | owner |
|---|---|
| which values exist, and where Keep writes them | the substrate kernel — **your** `thing()` literals |
| world solidity, dynamic prop motion | physics pack → Rapier. Mesh is read FROM the body |
| character locomotion pose | movement pack → controller result → mesh |
| camera pose | camera pack. Reads the target, writes only the camera |
| what keys mean, win/lose, animation, art | **your game** (not a medium pack) |

**Never integrate a position yourself for a body a pack owns.**

Presentation (skinned characters, landscape art, AnimationMixer) is **game-owned** until a
real medium is designed and shipped on purpose — not a panic trailer add-on.

---

## Which path is this game on

| | |
|---|---|
| **Packs** (this page) | the game needs real solids, an embodied character, or a follow camera. Default for anything a person walks around in. Costs a Rapier WASM load. |
| **[`../scaffold/`](../scaffold/)** | the game's own simple motion and AABB collision is genuinely enough — a top-down arena, a puzzle, a fixed-camera piece. No dependency at all. |

Both are born owned and both pass the same gate. The scaffold is **not** a lesser path; it is the
right one when a solver would be weight without benefit.

⚠️ The scaffold does not use the packs, on purpose — it is the zero-dependency demonstration of the
four contracts. If you build through the packs, take the ownership patterns from the scaffold and
the construction from here.

---

## Serve it

**A `devtune init` project** — your game is at `<project>/src/`:

```js
import { ensurePhysicsWorld } from '../medium/physics/pack.js';
// Browser URL becomes /medium/... — serve maps that to .devtune/medium automatically.
```

**The DevTune monorepo** — games live under `product/games/<name>/`. From `src/`:

```js
import { ensurePhysicsWorld } from '../../../medium/physics/pack.js';
// Same /medium/... URL; three hops because src/ is three deep under product/.
```

Serve from the **monorepo root** (parent of `product/`):

```bash
node bridge/bridge-server.js
node adapters/serve/serve.mjs product/games/<your-game> --port 5430 --shell
# http://127.0.0.1:5430/
```

⚠️ **Do not leave another project on the same port** (e.g. substrate still on 5430) — you will
get the wrong app or a blue screen while the HUD lies.

Rapier: pass `rapierUrl: '/__devtune/shell/vendor/rapier.mjs'` under `--shell`.

```bash
node product/tools/verify.mjs product/games/<your-game>
```

### Bind laws (Horizon failure F2/F3)

- **Never** `bind(id, parentGroup)` when that group holds the whole world → mega-select.
- Bind **each selectable mesh** (or the body mesh the player sees), not empty Groups.
- Shared id for swarms is fine (`beacons` × N). Ephemeral FX may stay unbound.
