# V0.2 Game Authoring

V0.2 game authoring is the performance-mode path: deterministic simulation code writes schema-backed
plain data, render code reads a mirror, and gameplay code uses the `ctx.world()` facade as the primary
authoring surface.

The implemented world-facade API lives in
[experimental/performance-mode/engine-context.js](../experimental/performance-mode/engine-context.js).
The schema backend helpers live in
[experimental/performance-mode/state-backend.js](../experimental/performance-mode/state-backend.js).
The focused API tests are
[tests/performance-mode-world-facade-api.test.js](../tests/performance-mode-world-facade-api.test.js).

## Mental Model

Think of V0.2 as three separate layers:

1. **Canonical simulation state is schema-backed plain data.** Declare tables with `schemaScalar`,
   `schemaRecord`, and `schemaTable`. Rows contain scalar fields only. Do not store class instances,
   canvas objects, sounds, DOM nodes, timers, or renderer handles in canonical state.
2. **Gameplay code runs inside `PerformanceModeEngineContext`.** In schema-generated mode the engine
   hands each tick a context that exposes deterministic state authoring plus routed engine services.
   The nicer authoring slice is `ctx.world()`.
3. **Presentation is mirror-backed.** The simulation emits canonical row changes; the render side reads
   a `RenderMirror` fed by bus messages. Render code must not read live backend wrappers or make the
   renderer authoritative.

The important rule: game logic should feel like normal entity code, but the backing data remains
rollback-safe plain records.

Deterministic simulation is mandatory. A `step(ctx)` must produce the same state from the same previous
state, tick, and routed inputs. Do not read wall-clock time, random browser globals, DOM state, renderer
state, or transport state inside simulation code. Use `world.tick`, `world.dt`, schema rows, and
`world.input()` / `world.query()` as the game-visible inputs.

Schemas define performant state. The schema-generated backend can patch, snapshot, hash, transfer, and
mirror rows because the schema declares every canonical table and scalar field ahead of time. That is
the reason V0.2 examples start with `schemaScalar`, `schemaRecord`, and `schemaTable` instead of a loose
object graph.

Entity ids plus generations provide stable identity. Game code names entities by stable table/id pairs
such as `paddles` + `paddle:p1`. Internally, the schema backend also tracks a per-lifetime generation so
destroying an id and later respawning the same id is not mistaken for the old object. Public snapshots
stay clean, but authoritative backend and mirror paths preserve identity through generation metadata.
The regression coverage for that behavior lives in
[tests/performance-mode-entity-generation-snapshot-transfer.test.js](../tests/performance-mode-entity-generation-snapshot-transfer.test.js)
and
[tests/performance-mode-render-mirror-lifetime.test.js](../tests/performance-mode-render-mirror-lifetime.test.js).

Rollback/replay is engine-owned. Game code does not run manual rollback loops or keep its own recovery
history. It writes the current deterministic tick through `ctx.world()`, and the engine/backend own
snapshot materialization, query-driven rollback, replay, checkpoint hashes, bootstrap/recovery transfer,
and render-mirror patching. The world-facade Pac-Man browser shell follows that boundary in
[examples/performance-mode-pacman-world-trystero/main.js](../examples/performance-mode-pacman-world-trystero/main.js).

Game code writes through `ctx.world()`. That means row reads/writes, spawns, destroys, input reads, and
participant discovery happen through the world facade or the lower-level context API it delegates to,
not by reaching into backend internals.

## Core API Walkthrough

Import schema helpers from the actual backend module:

```js
import {
  schemaScalar,
  schemaRecord,
  schemaTable,
} from '../experimental/performance-mode/state-backend.js';
```

Declare entity classes with `static table` so the world facade can resolve the schema table:

```js
class Ball {
  static table = 'balls';
}

class Paddle {
  static table = 'paddles';
}
```

Declare schema tables with string or auto IDs:

```js
export const SCHEMA = {
  balls: schemaTable(schemaRecord({
    x: schemaScalar('number'),
    y: schemaScalar('number'),
    vx: schemaScalar('number'),
    vy: schemaScalar('number'),
  }), { id: 'string', order: 'id' }),
  paddles: schemaTable(schemaRecord({
    y: schemaScalar('number'),
    score: schemaScalar('number'),
  }), { id: 'string', order: 'id' }),
};
```

### Creating Initial State

Initial state is plain data that matches the schema table names. For table fields, seed rows under
`__emRows`; the backend keeps internal generation metadata outside public snapshots.

```js
export function createInitialState() {
  return {
    balls: {
      __emRows: {
        'ball:main': { x: 50, y: 50, vx: 1, vy: 1 },
      },
    },
    paddles: { __emRows: {} },
  };
}
```

This shape is the same style used by the focused world-facade tests in
[tests/performance-mode-world-facade-api.test.js](../tests/performance-mode-world-facade-api.test.js).

### Writing `step(ctx)`

Inside a schema-generated step, get the world facade and use class-bound methods:

```js
export function step(ctx) {
  const world = ctx.world();

  const ball = world.get(Ball, 'ball:main');
  for (const playerId of world.participants()) {
    const input = world.input(playerId) || {};
    let paddle = world.get(Paddle, `paddle:${playerId}`);
    if (!paddle) {
      paddle = world.spawn(Paddle, `paddle:${playerId}`, { y: 50, score: 0 });
    }

    paddle.s.y += (input.dy || 0) * world.dt;
  }

  if (ball) {
    ball.s.x += ball.s.vx * world.dt;
    ball.s.y += ball.s.vy * world.dt;
  }
}
```

`step(ctx)` can call `ctx.emit(...)` for deterministic one-shot events. The event leaves simulation
through the engine boundary; browser/audio code consumes rollback-safe events by stable key. Pac-Man's
sound tests assert stable keys and rollback-confirmed payloads in
[tests/performance-mode-pacman-sound-events.test.js](../tests/performance-mode-pacman-sound-events.test.js).

```js
function emitPaddleHit(ctx, world, ballId) {
  ctx.emit('sound', {
    name: 'PaddleHit',
    key: `pong:sound:PaddleHit:tick:${world.tick}:ball:${ballId}`,
    confirm: 'rollback-confirmed',
  });
}
```

Use this for one-shot, rollback-safe events such as sound. Continuous render/audio intent should be
derived from mirror data or canonical rows, not from one-shot replay.

Implemented `world` surface:

- `world.tick` and `world.dt` expose tick metadata.
- `world.state` exposes the live schema state for advanced cases.
- `world.table(name)` returns a declared schema table by name.
- `world.get(Class, id)` resolves `Class.table` and returns a wrapper or `null`.
- `world.all(Class)` iterates wrappers from the class-bound table.
- `world.spawn(Class, id, initialRecord, options?)` creates a row and returns its wrapper.
- `world.destroy(entityOrClass, id?, options?)` destroys by wrapper or by class/id.
- `world.input(playerId)` routes through `ctx.query` and returns the game input payload.
- `world.query(id, queryCtx, predicate)` exposes the lower-level routed query service.
- `world.participants()` and `world.discoverParticipants(predicate, limit)` expose membership.

The lower-level context API still exists (`ctx.entity`, `ctx.entities`, `ctx.spawn`, `ctx.destroy`,
`ctx.query`, `ctx.participants`, `ctx.discoverParticipants`). Use it when explicit table handles are
clearer. Prefer `ctx.world()` for release-facing V0.2 examples.

### Reading Inputs

Read inputs through `world.input(playerId)` or `world.query(...)`, never through a raw input map.
`world.input(playerId)` delegates to `ctx.query`, so the engine records the read for rollback/replay.
The engine strips control metadata first; the returned value is the game input payload.

```js
const input = world.input(playerId) || {};
paddle.s.y += (input.dy || 0) * world.dt;
```

### Spawning And Destroying Entities

Spawn with the class, id, and initial record. Destroy by wrapper or class/id. These calls delegate to
the schema backend and preserve per-lifetime generation behavior.

```js
const paddle = world.spawn(Paddle, `paddle:${playerId}`, { y: 50, score: 0 });
world.destroy(Paddle, `paddle:${playerId}`, { reason: 'left-game' });
```

### Rendering From Mirror Data

Render reads from a mirror. The render side consumes `RenderMirror` tables, not live backend wrappers.
`RenderMirror` is implemented in
[experimental/performance-mode/render-mirror.js](../experimental/performance-mode/render-mirror.js).

```js
export function drawPongMirror(mirror, canvasContext) {
  const balls = mirror.tryTable('balls');
  if (balls) {
    for (const ball of balls.values()) {
      canvasContext.fillRect(ball.x - 2, ball.y - 2, 4, 4);
    }
  }

  const paddles = mirror.tryTable('paddles');
  if (paddles) {
    for (const paddle of paddles.values()) {
      canvasContext.fillRect(4, paddle.y - 12, 4, 24);
    }
  }
}
```

For a production-scale adapter, see the Pac-Man world renderer at
[examples/performance-mode-pacman-world-trystero/renderer.js](../examples/performance-mode-pacman-world-trystero/renderer.js).
It converts V0.2 mirror rows into the shared Pac-Man draw data without reading live simulation state.

## Minimal Ball/Pong-Sized Example

This is a complete simulation slice. It has one ball and one paddle per admitted participant. Rendering
and networking host setup are intentionally outside the snippet; the step is deterministic and only
touches `ctx.world()`.

```js
import {
  schemaScalar,
  schemaRecord,
  schemaTable,
} from '../experimental/performance-mode/state-backend.js';

class Ball {
  static table = 'balls';
}

class Paddle {
  static table = 'paddles';
}

export const SCHEMA = {
  balls: schemaTable(schemaRecord({
    x: schemaScalar('number'),
    y: schemaScalar('number'),
    vx: schemaScalar('number'),
    vy: schemaScalar('number'),
  }), { id: 'string', order: 'id' }),
  paddles: schemaTable(schemaRecord({
    y: schemaScalar('number'),
    score: schemaScalar('number'),
  }), { id: 'string', order: 'id' }),
};

export function createInitialState() {
  return {
    balls: {
      __emRows: {
        'ball:main': { x: 50, y: 50, vx: 1, vy: 1 },
      },
    },
    paddles: { __emRows: {} },
  };
}

export function step(ctx) {
  const world = ctx.world();
  world.discoverParticipants(() => true, 2);

  for (const playerId of world.participants()) {
    const input = world.input(playerId) || {};
    let paddle = world.get(Paddle, `paddle:${playerId}`);
    if (!paddle) {
      paddle = world.spawn(Paddle, `paddle:${playerId}`, { y: 50, score: 0 });
    }
    paddle.s.y = Math.max(0, Math.min(100, paddle.s.y + (input.dy || 0) * world.dt));
  }

  const ball = world.get(Ball, 'ball:main');
  if (!ball) return;

  ball.s.x += ball.s.vx * world.dt;
  ball.s.y += ball.s.vy * world.dt;

  if (ball.s.y < 0 || ball.s.y > 100) ball.s.vy *= -1;
  if (ball.s.x < 0 || ball.s.x > 100) {
    ball.s.x = 50;
    ball.s.y = 50;
  }
}
```

Typical engine wiring keeps the same schema-generated backend boundary. `SimulationEngine` lives at
[SimulationEngine.js](../SimulationEngine.js), and the engine creates `PerformanceModeEngineContext`
for schema-generated steps.

```js
import { SimulationEngine } from '../SimulationEngine.js';
import { SCHEMA, createInitialState, step } from './pong-world.js';

const engine = new SimulationEngine(transport, {
  initialState: createInitialState(),
  getLocalInputs: () => ({ input: readLocalInput(), discoverable: true }),
  experimentalStateBackend: {
    mode: 'schema-generated',
    schema: SCHEMA,
    validateAgainstSnapshot: false,
  },
  step: (state, _inputs, _tick, ctx) => {
    step(ctx);
    return state;
  },
});
```

For a tested small shape, see
[tests/performance-mode-world-facade-api.test.js](../tests/performance-mode-world-facade-api.test.js).

## V0.2 Non-Goals

V0.2 does **not** make performance mode an Easy Mode/proxy object graph. The facade is a thin authoring
layer over schema-backed records.

V0.2 does **not** store class instances in canonical state. Wrappers are runtime authoring handles; the
backend snapshots and hashes plain records.

V0.2 does **not** expose the raw engine, transport, renderer, backend internals, or `__em*` metadata on
the world facade.

V0.2 does **not** make render state authoritative. Render code reads mirror rows fed through
[experimental/performance-mode/render-mirror.js](../experimental/performance-mode/render-mirror.js) and
[experimental/performance-mode/schema-mirror-bridge.js](../experimental/performance-mode/schema-mirror-bridge.js).

V0.2 does **not** remove the lower-level context API or the baseline Pac-Man implementation. Those stay
as compatibility and parity references.

V0.2 does **not** promise stable package import paths for every `experimental/performance-mode/` module.
The release-facing contract is documented here and in
[docs/em-performance-mode-engine-context.md](em-performance-mode-engine-context.md).

Threaded buses are not release-blocking. The V0.2 authoring contract works with the current
performance-mode engine/backend path; worker or threaded-bus expansion can improve throughput later
without changing how game code uses `ctx.world()`.

Easy proxy mode is future work. V0.2 intentionally keeps schema-backed records and explicit wrapper
classes rather than pretending canonical state is a transparent object graph.

Advanced renderer history access is future work. V0.2 renderers read current mirror tables and normal
mirror patches; richer renderer-side history or interpolation APIs can be added after the core
simulation/mirror boundary is stable.

Full authoring sugar can keep improving. V0.2 ships the first world-facade slice, not the final authoring
language; future releases can add aliases, shorter spawn forms, render facades, and additional helper
APIs while keeping this deterministic schema-backed model intact.

## Pac-Man Reference

The world-facade Pac-Man implementation is
[experimental/performance-mode/games/pacman-world.js](../experimental/performance-mode/games/pacman-world.js).
It is intentionally parallel to the lower-level baseline implementation at
[experimental/performance-mode/games/pacman.js](../experimental/performance-mode/games/pacman.js), and
the focused parity tests live in
[tests/performance-mode-pacman-world-facade.test.js](../tests/performance-mode-pacman-world-facade.test.js).

The browser demo shell for the world-facade implementation is
[examples/performance-mode-pacman-world-trystero/main.js](../examples/performance-mode-pacman-world-trystero/main.js).
Its verification note is
[examples/performance-mode-pacman-world-trystero/VERIFY.md](../examples/performance-mode-pacman-world-trystero/VERIFY.md),
which lists the deployed demo URL:
`https://libs.letsinspire.com/em-pacman-v02-world-trystero/`.

The older V0.2 browser shell at
[examples/performance-mode-pacman-trystero/main.js](../examples/performance-mode-pacman-trystero/main.js)
is still valuable baseline coverage, but release-facing examples should use the world-facade source.

Related docs:

- [docs/em-performance-mode-developer-api.md](em-performance-mode-developer-api.md)
- [docs/em-performance-mode-engine-context.md](em-performance-mode-engine-context.md)
- [docs/em-state-backend-modes.md](em-state-backend-modes.md)
- [docs/v0.2-release-readiness.md](v0.2-release-readiness.md)
