# The DevTune substrate stack — build a game that is born owned

**This document is written to be handed to an AI model.** It is complete: everything needed to
build a three.js game that a designer can tune the moment it boots, with nothing to reverse-engineer
afterwards. Nothing here assumes you have seen this project before.

Companion files: [`PROMPT.md`](PROMPT.md) is the generation prompt ·
[`../scaffold/`](../scaffold/) is the code to start from ·
[`../tools/verify.mjs`](../tools/verify.mjs) is the checker you run on your own output.

⭐ **Building something a person walks around in? Read [`PACKS.md`](PACKS.md) too.** Physics,
movement and camera are default medium now — a game that hand-rolls them gets a dial vocabulary
nobody else shares. This page stays the contract either way.

---

## 0. What you are building, and the one thing that makes it different

An ordinary three.js game keeps its feel in constants:

```js
const WALK_SPEED = 7;                    // legible to a person, invisible to any tool
```

A born-owned game keeps the same numbers in a declared object it re-reads every frame:

```js
const feel = thing('player', { walkSpeed: 7 }, { walkSpeed: [1, 20] });
…
pos.x += feel.walkSpeed * dt;
```

Both play identically. The difference is that the second one can be **tuned while it runs, and the
new number written back into the source file it was typed in**. That is the entire product, and the
declaration is the whole price of admission.

⚠️ **Reader-legible and tool-reachable are unrelated properties.** `const WALK_SPEED = 7` at the top
of a file is about as clear as source gets, and a runtime tool cannot reach it, change it, or write
to it. Measured on a real 2,275-line FPS: automatic discovery found **19 values, 0 of them
settings.** Do not assume "clean code" gets you any of this for free. It does not.

---

## 1. The four contracts

Import them as **bare names** from your own `src/substrate.js`. Copy that file in unchanged.

```js
import { thing, bind, collide, bindAll } from './substrate.js';
```

```js
thing(id, [anchor,] settings, [ranges], [meta])   // these numbers are mine
bind(id, object, [state])                         // this object is one of `id`
collide(id, shape)                                // `id` already collides; here is the shape
bindAll(id, instancedMesh, [adopt])               // every instance in this buffer is one of `id`
```

Every one is **additive**. Adopt none and the game still runs. Adopt one and you get exactly what
that one buys.

### `thing(id, [anchor,] settings, [ranges], [meta])`

The only call that declares values. Returns `settings` **by identity**.

| argument | rule |
|---|---|
| `id` | A **string literal** written at the call site. Dots make namespaces: `weapon.rifle`. |

| `anchor` | Optional `Object3D`. Told apart **by type**, never by argument count. Use it when one real object in the scene **is** the thing. |
| `settings` | An **object literal** written at the call site. Its numbers become the dials. |
| `ranges` | Optional `{ key: [min, max] }`. Authored. A key with no entry has no range, and the desk says `NO RANGE DECLARED` rather than inventing ends. |
| `meta` | Optional `{ readAt: { key: 'live' \| 'spawn' \| 'nextWave' } }`. See §3. |

```js
const feel = thing('player', {
  walkSpeed: 7,
  jumpForce: 8.5,
  gravity: 22,
}, {
  walkSpeed: [1, 20],
  jumpForce: [0, 20],
});
```

⚠️ **Do not write a `hex` key in `meta` yourself.** Which values were written as hex is worked out
when the file is parsed, and merged in for you.

⚠️ **A namespaced id must not collide with a parent's own property.** A value key is
`<thingId>.<prop>` and it is split on the **last** dot — so `thing('forest.moves', …)` beside a
`forest` that has a setting called `moves` makes the key `forest.moves` mean two different things,
and one of them becomes silently unreachable. `verify.mjs` fails on this.

### `bind(id, object, [state])`

Says an object is one of `id`. Declares no values.

```js
bind('enemy', zombieGroup, zombieRecord);   // clickable, and it can say what condition it is in
bind('player', document.getElementById('hp'));   // a HUD node is a DOOR, not another instance
```

- An **`Object3D`** becomes a clickable instance.
- A **DOM element** becomes a *door*: clicking the ammo readout opens the weapon. It is not a second
  weapon.
- **`state`** is your own live record for that instance — read, never written, never renamed. Pass
  the actual object, not a copy: the inspector shows what is true now. `isAlive: false` is what a
  designer sees, because `isAlive` is what you wrote.
- Bind **after** the object is added to the scene. An object whose ancestry no longer reaches a
  `Scene` stops counting as an instance, which is how dead actors leave the list on their own.

### `collide(id, shape)`

Says `id` already collides, and hands over the shape. A `Box3`, anything with `.min`/`.max`, or a
function returning one. **Held by reference**, so a box you recompute is followed rather than
snapshotted.

```js
collide('building.hub', hubBox);
for (let i = 0; i < 4; i++) collide('arena', walls[i]);   // several shapes, one thing
```

Without it, two things are wrong at once: the desk offers *Make Solid* on something that has
collided since the day it was written — two owners of one overlap — and a designer cannot see any
collider they did not personally create. There is nothing to discover here; a `Box3` in an array has
no signature.

⚠️ **Hand each shape over ONCE and mutate it in place.** Pushing a fresh box on every rebuild leaks
a collider per frame.

**Yes, hand over trigger volumes too** — a pickup radius, a checkpoint, a damage zone. `collide()`
says *"this game already resolves an overlap here"*; it does not say *"this is solid."* A volume you
must be able to fly through is exactly where the desk must not offer to make one solid.

#### ⭐ How a collider goes away again — the function form

If a collider's **count is itself an owned dial**, you have a problem the direct form cannot solve:
a collider is handed over once and held by reference, so it cannot be conjured at runtime, and there
is no `uncollide()`.

The answer is already in the signature. **Hand over a function, and return nothing when the slot is
not live.** A slot that reports no shape is not a collider that frame.

```js
const POST_SLOTS = 120;      // ⚠️ allocation, not feel — a plain const, deliberately not owned

// once, at construction: allocate the pool and hand every slot over
this.postBoxes = [];
for (let i = 0; i < POST_SLOTS; i++) {
  const box = new THREE.Box3();
  this.postBoxes.push(box);
  collide('pillars', () => (i < this.posts.length ? box : null));
}

// on every rebuild: mutate the live prefix in place. Nothing is allocated, nothing is left behind.
for (let i = 0; i < this.posts.length; i++) { const q = this.posts[i]; this.postBoxes[i].min.set(…); … }
```

Pin the owned `count`'s range max to `POST_SLOTS` and clamp to it. Raising the ceiling is an edit in
your file — which is the same trade as the placement slots in §4, and for the same reason.

⚠️ `let i` so each closure captures its own index. With `var` they all share the last one and the
whole field becomes one box.

### `bindAll(id, instancedMesh, [adopt])`

An `InstancedMesh` is N matrices behind one `Object3D`, so `bind()` could only ever call the whole
field one thing. This says the buffer holds many; **the index in the buffer is the identity.**

```js
bindAll('forest', trunkInst, (i, x, y, z) => recordTreeMove(i, x, z));
bindAll('forest', leafInst);     // same id: a trunk and its leaves are ONE tree at one index
```

`adopt(i, x, y, z)` is optional and is what makes a per-instance drag **saveable** — see §4. It is
given the full world position; the scaffold's example records only `x` and `z` because its pillars
stand on the floor, not because `y` is unavailable. Record whichever axes your game can put back.

The desk's subline says how many bodies a buffer holds — `declared · 64 in a buffer`. What a swarm
does **not** get yet is the kind-vs-instance scope switch: you can select, move and adopt one
instance, but there is no THIS ONE · ALL N for a buffer.

---

## 2. ⭐ The five rules. Break one and the failure is silent.

Nothing below throws. The game plays perfectly and the desk is empty, or full of dials that do
nothing. This is why [`../tools/verify.mjs`](../tools/verify.mjs) exists.

### 1. The literal must be AT the call site

```js
const CONFIG = { walkSpeed: 7 };
const feel = thing('player', CONFIG);        // ❌ owned, live, and UNSAVEABLE
const feel = thing('player', { walkSpeed: 7 });  // ✅
```

A Keep replaces the **bytes of a literal at a recorded byte range**. An identifier has no bytes to
replace. This is also why there is no central `config.js` in a born-owned game: each system declares
its own numbers in its own file, at the point of use.

### 2. Read from the returned object, every time

```js
const feel = thing('player', { walkSpeed: 7 });
const speed = feel.walkSpeed;                // ❌ read ONCE at module load. Dead dial, forever.
const { walkSpeed } = thing('player', {…});  // ❌ same bug, wearing nicer clothes

pos.x += feel.walkSpeed * dt;                // ✅ re-read, so the change lands next frame
```

`feel.walkSpeed` is re-read. `walkSpeed` is not. A local copy inside a per-frame function is fine
(`const f = feel;` then `f.walkSpeed`) — what is fatal is copying the **number** out of the object
anywhere that runs once.

### 3. Every id is a string literal, and every call is a bare name

```js
sub.thing('player', {…});                    // ❌ a namespaced call is invisible
for (const id of ids) bind(id, mesh[id]);    // ❌ a runtime id registers nothing
bind('house.1', a); bind('house.2', b);      // ✅ one line per id
for (const w of walls) collide('arena', w);  // ✅ a literal INSIDE a loop is still a literal
```

The rewrite happens when the file is parsed, so anything that can only be known at runtime cannot be
read. If you need N separate ids, write N lines — the repetition is load-bearing. If N things share
one id, a loop is fine.

### 4. One owner per overlap, per transform, per motion

Every hard bug this product has had was two writers on one value. Decide who owns each conflict and
give the other side a way to ask:

```js
// world.js owns every collider. The player asks and accepts the answer.
this.onGround = this.world.resolve(this.pos, this.vel, f.radius, f.height);
```

Never resolve the same overlap in two places. Never let two systems write one object's position.

### 5. The game must be correct with the contracts as identity functions

`substrate.js` is what runs when DevTune is not attached — served statically, built for production,
opened by somebody who has never heard of any of this. If your game needs `thing()` to *do*
something, the design is wrong.

---

## 3. Live vs deferred — say when a number is actually read

Most owned numbers are read every frame. Some are **copied into an instance at a boundary**: an
enemy's hp at spawn, a wave's pressure at the start of the wave. Owning those without saying so
gives a designer a dial that moves while nothing on screen does — a **fake-live dial**, which is
worse than no dial, because it teaches them the tool lies.

```js
const birth = thing('enemy.birth', {
  hp: 30,
  speed: 2.4,
}, undefined, {
  readAt: { hp: 'spawn', speed: 'spawn' },
});
```

| `readAt` | means | the desk shows |
|---|---|---|
| `'live'` (default, omit it) | re-read continuously | an ordinary dial |
| `'spawn'` | copied when a new instance is created | `SPAWN ONLY` |
| `'nextWave'` | read at the next pressure boundary | `NEXT WAVE` |

Deferred values are visible and keepable, exactly like live ones. What changes is the label, and the
promise: **already-living instances are never rewritten.**

⚠️ **`undefined` holds the ranges slot.** The order is settings → ranges → meta, and
`thing(id, settings, meta)` is read as a range map that happens to contain no ranges.

⚠️ **Only declare a timing the game really has.** Omitting `readAt` means `live`. Inventing a
boundary is worse than leaving a value live and wrong: a designer can see a live dial doing nothing,
but cannot see a made-up boundary at all.

⚠️ **There is no token for "a new run."** A value copied when a run, a level or a new game begins —
a starting battery, a starting score, a level's par time — is read once and never again, and neither
`spawn` nor `nextWave` describes that. **Leave it `live` and say so in a comment.** `spawn` would be
a claim the desk then makes to a designer on your behalf, and it would be wrong. This is a known gap
in the substrate, not something for you to work around.

---

## 4. ⭐ Write back the INPUT, not the output

The rule that decides what can be saved.

A collider computed as `new Box3(cx - w/2, …)` has no literal of its own and looks unsaveable —
until you notice `cx` and `w` were typed by hand ten lines up. **A derived value is keepable at
whatever it was derived from.**

| what a designer changes | what actually gets written |
|---|---|
| a building's collider | `x, z, w, h, d` — the arguments it is computed from |
| one random house's width | `minW, spreadW` — **the range the roll is drawn from** |
| the whole arena's four walls | `bound` — one number behind all four |

So: **own the inputs.** Never own a value your game computes every frame — a velocity, a current hp,
a distance. Those are outputs; a dial on one is a dial on the weather.

### The one place this needs help from you

DevTune's writer replaces literals whose byte range it recorded. **It does not author source** —
that restraint is why a Keep can be trusted. So it cannot invent a home for a value that has none,
and a hand-dragged instance's new position has none.

If you want hand-placed exceptions saved, declare slots for them:

```js
const moves = thing('pillars.moves', {
  which0: -1, x0: 0, z0: 0,
  which1: -1, x1: 0, z1: 0,      // four slots is four hand-moved pillars
  …
});

bindAll('pillars', inst, (i, x, _y, z) => recordMove(i, x, z));   // adopt writes into them
```

Its absence is honest: no `adopt` still gives a movable instance, and the desk already says the move
is session-only.

---

## 5. Patterns, by system

| system | shape |
|---|---|
| **movement / feel** | one `thing()` per controller, read live every frame. The most valuable thing you can own — do this first. |
| **camera** | `thing('camera', cameraObject, { distance, height, fov, followLag })`. Anchored, because one real object IS the thing. |
| **world / geometry** | own the **generator** (`count`, `spread`, `min*`, `*Spread`, `seed`), not the results. Rebuild when a value changes; use a seeded RNG so a rebuild gives back the same layout. |
| **colliders** | one owner (usually a `World`), boxes derived from owned numbers, handed over once with `collide()` and mutated in place. If the **count** is owned, use the function form (§1) and a fixed pool. |
| **enemies / actors** | `thing('enemy', {…})` unanchored for the shared table, `bind('enemy', group, record)` per body as it spawns. Spawn-copied stats go in a second `thing()` with `readAt: 'spawn'`. |
| **waves / pressure** | `readAt: 'nextWave'`. In a survival game the pressure curve **is** the design — own it, and label it. |
| **weapons / economy** | ordinary live `thing()`s. Namespace them: `weapon.rifle`, `weapon.shotgun`. |
| **colours** | a `thing()` of their **own**, written as hex. They are dropped from the dial list automatically, so a thing that is all colours shows no row at all — which is correct, and surprising. |
| **HUD** | `bind(id, element)` for readouts. A door into the system, never a second instance. |

---

## 6. The silent-failure table

Every row is something that runs, plays, and lies.

| what you wrote | what happens | write instead |
|---|---|---|
| `thing('p', CONFIG)` | live dials, **nothing saveable** | the object literal at the call site |
| `const { speed } = thing(…)` | dial moves, game never changes | `const feel = thing(…)`, read `feel.speed` |
| `const speed = feel.speed` at module scope | same | read inside the frame |
| `sub.thing('p', {…})` | nothing registers at all | `import { thing }` and call it bare |
| `bind(id, m)` with a variable `id` | nothing registers; warns once at runtime | one line per literal id |
| `thing('p', {…}, { readAt: … })` | `readAt` read as a range map — dial looks live | `thing('p', {…}, undefined, { readAt: … })` |
| `readAt: 'onRestart'` | falls back to `live` with a console warning | `live` · `spawn` · `nextWave` |
| two `thing()`s with one id | the later wins; the first is unreachable | namespace one: `player.dash` |
| owning a velocity or a current hp | a dial on an output; it snaps back every frame | own the input it is computed from |
| `KeyC` as a game key | swallowed — **C** opens Design | any other key |
| pushing a new `Box3` into `collide()` each rebuild | a collider leak per frame | hand it over once, mutate in place |
| `thing('a.b')` beside `thing('a', { b: … })` | the key `a.b` means two things; one is unreachable | rename either |
| `readAt: 'spawn'` on a start-of-run value | the desk promises a boundary the game does not have | leave it live |

---

## 7. Boot requirements

- **three.js `0.160.x`.** The desk vendors r160 and picking across two very different majors is
  untested.
- **The renderer must be constructed after page parse.** DevTune's runtime is injected right after
  `<head>` and captures the scene, renderer and play camera through `__THREE_DEVTOOLS__`, which
  three announces at **construction**. A normal `<script type="module">` game satisfies this.
  Nothing to do — just do not build a renderer inside an inline `<head>` script.
- **Avoid `C` as a game key** (opens Design). Also protect `P F K Z G X Tab /` where possible.
  Desk keys other than **C** are mostly Design-only (softer conflict in Play).

### ⚠️ Console noise that is EXPECTED under `--shell`, and is not your bug

Four lines, on every load, on a game that is doing everything right. The untouched scaffold prints
all four. **None of them appears on a plain static server.**

```
WARNING: Multiple instances of Three.js being imported.        ×2
using deprecated parameters for the initialization function     ×2
```

The three.js one is **not** a version mismatch — it is the desk's own vendored r160 and your r160
being two module instances in one page, and it appears even when your pin is exactly right. The
other two come from the desk's physics library initialising. Do not go hunting a pin you already
got correct.

### ⚠️ `?pump` gives you a loop, not real time

Keep the `?pump` block at the top of `index.html`. A hidden or backgrounded browser pane suspends
`requestAnimationFrame` entirely (measured: 0 Hz), so without it nothing about the game can be
checked except by a person watching it.

But it is a timer, not a clock. Measured in an undisplayed pane: the shim fired at ~63 Hz while the
game's own loop advanced at roughly 6 Hz, so one wall-second was about a tenth of a game-second —
and after several minutes with the pane never displayed, the loop stopped altogether.

**So: game-seconds ≠ wall-seconds under `?pump`.** If you want to measure anything about your game,
drive its systems yourself at a fixed step (`for (let i = 0; i < 600; i++) thing.update(1/60)`)
rather than waiting on wall-clock time. And when you do, **check the reading is not about something
else** — a body parked inside a collider, or pressed against a wall, reports the wall's behaviour and
not the dial's.

---

## 8. Check your own work before you say you are done

```bash
node tools/verify.mjs <yourGameDir>
```

It reads your files with the same parser the host uses and reports every rule above. **Exit code 0
and `✓ BORN OWNED` is the gate.** Then run it:

```bash
node bridge/bridge-server.js
node adapters/serve/serve.mjs <yourGameDir> --port 5420 --shell
```

Open `http://127.0.0.1:5420/`, press **`C`** (Design), and the desk should already show your systems with no
archaeology of any kind. If it does not, the checker was wrong and that is worth reporting.
