# V0.3 Game Authoring

V0.3 keeps the V0.2 schema-backed authoring model and makes the public import boundary explicit:

```js
import { EasyMultiplayer } from '../EasyMultiplayer.js';

const {
  schemaScalar,
  schemaRecord,
  schemaTable,
  entityTable,
  stepWithWorld,
} = EasyMultiplayer.performanceMode;
```

## What Changes From V0.2

- Import common authoring helpers from `EasyMultiplayer.performanceMode` instead of older entry modules
  or deep internal paths.
- `ctx.world()` remains the primary game-code facade. It is still deterministic and schema-backed.
- V0.3 adds runtime choices around buses: one bus, multiple time-sliced buses, or threaded worker buses.
  Those choices are host/runtime concerns, not game-step concerns.
- Render history and health signals are renderer/UI APIs. They do not change the `step(ctx)` contract.

## What Stays Compatible

- Existing V0.2 `step(ctx)` functions keep working unless they relied on internal paths or renderer state.
- Schema rows remain plain canonical data. Do not store class instances, DOM nodes, canvas objects, audio
  handles, worker objects, or timers in canonical state.
- Inputs still flow through the engine context (`world.input(...)` or `world.query(...)`).
- Rollback/replay remains engine-owned. Game code does not choose buses, replay ticks, or branch on health.

## Minimal Shape

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

export class Ball {
  static table = 'ball';
  bind(row, meta) { this.s = row; this.id = meta.id; }
}

export function createInitialState() {
  return {
    ball: entityTable({
      main: { x: 10, y: 10, vx: 1, vy: 0 },
    }, { id: 'string', order: 'id' }),
  };
}

export function step(ctx) {
  const world = ctx.world();
  const ball = world.get(Ball, 'main');
  const input = world.input('p1') || {};
  if (ball) {
    ball.s.x += (input.dx ?? ball.s.vx) * world.dt;
    ball.s.y += (input.dy ?? ball.s.vy) * world.dt;
  }
}
```

`stepWithWorld(stepWorld)` is available when a game wants a `step(world)` authoring style while keeping
the engine-facing `step(ctx)` shape:

```js
export const step = stepWithWorld((world) => {
  for (const ball of world.all(Ball)) ball.s.x += ball.s.vx * world.dt;
});
```

## Rendering

Renderers read from a mirror, not live simulation state:

```js
import { EasyMultiplayer } from '../EasyMultiplayer.js';

const { RenderMirror } = EasyMultiplayer.performanceMode;

function draw(mirror) {
  const balls = mirror.tryTable('ball');
  const ball = balls?.get('main');
  if (ball) drawBall(ball.x, ball.y);
}
```

Renderers must not write canonical rows, mutate wrappers, send gameplay inputs directly into schema
state, or make authoritative decisions. They may request render history and read health signals for
display.

## Loud Failures

V0.3 tries to fail early with named errors when game or host code crosses deterministic boundaries:

- Worker game modules that touch browser-only globals such as `document`, `window`, canvas, or audio
  during module initialization fail as `EM_WORKER_MODULE_DOM_ACCESS`.
- `Date.now()` and `Math.random()` during deterministic `step(ctx)` fail as
  `EM_PERF_NONDETERMINISTIC_DATE_NOW` or `EM_PERF_NONDETERMINISTIC_MATH_RANDOM`.
- Schema rows can be mutated only during an active step. Writes outside `step(ctx)` fail as
  `EM_PERF_MUTATION_OUTSIDE_TICK`.
- Writes to fields not declared by the schema fail as `EM_PERF_SCHEMA_UNKNOWN_FIELD`.
- Canonical state must be plain cloneable schema data. Functions, symbols, and class instances fail as
  `EM_PERF_UNSUPPORTED_CANONICAL_TYPE`, `EM_PERF_CANONICAL_NON_CLONEABLE`, or
  `EM_PERF_CANONICAL_NON_PLAIN` depending on which boundary sees them first.
- Worker/render bus messages reject functions, class instances, and `__em*` internals before crossing
  the boundary with `EM_BUS_LEAK_*` errors.
- Runtime health/status is render-only. Attempts to read it through `step(ctx)` or `ctx.world()` fail as
  `EM_PERF_RENDER_HEALTH_UNAVAILABLE_IN_STEP`.
- Malformed worker protocol messages fail as `EM_BUS_INVALID_*` before mutating input logs or canonical
  state.

Current enforcement limit: JavaScript cannot reliably detect every nondeterministic API a game might
call. The temporary `test-only executor`-backed proof path traps the common `Date.now()` and `Math.random()`
cases, but `test-only executor` is not the V0.3 production/runtime engine. The production replacement split is:
main-thread coordinator owns recovery/protocol, and worker runtime owns deterministic sim execution.
The remaining nondeterministic APIs stay documented as authoring constraints.

## V0.4 Deferrals

- Easy proxy-mode authoring remains future work.
- Advanced authoring sugar can keep improving.
- Bulk static asset/config transfer is still a separate V0.4-style concern.
- Renderer history is request-based in V0.3; richer interpolation helpers can be added later.
