# Lag compensation

> Check a shot against where the target was on the shooter's screen, by rewinding recent entity positions on the room.

A player aims at what their screen shows, and that is the past: other
players are drawn with [interpolation](/docs/guides/prediction#the-client),
a little behind the room's clock, and the shot takes half a round trip
to reach the room. By the time the room sees the shot, the target has
moved on, so a check against where the target is now misses a shot that
was on target.

Lag compensation fixes this on the room. Each input carries the tick of
the state the player was looking at, `connection.viewTick`. The room
records where the entities you choose were at each recent tick, and
`room.rewind` hands back those positions at the view tick to check the
shot against.

## Recording a history

`defineHistory(kind, fields)` declares which fields of a kind the room
records, and the room's `history` lists the declarations:

```ts
export const poses = defineHistory(Player, ["x", "y"]);

export default defineRoom({ schema, stepper, history: [poses] /* ... */ });
```

After each tick's state update, the room copies those fields of every
entity of the kind into a ring of recent ticks. Only `bool` and number
fields can be recorded. Record what a hit check reads, usually the
position and maybe a rotation, and not the rest: each field costs memory
for every entity of the kind.

`room.rewind(history, tick)` returns the entities as that tick's state
update sent them, as a `ReadonlyMap` from entity ID to a frozen object
holding `id` and the recorded fields:

- An entity created after `tick`, or deleted at or before it, is
  absent. An ID that was deleted and created again gives whichever
  entity had it at `tick`.
- A tick older than the oldest the room kept is clamped to the oldest.
- A tick the room hasn't recorded yet gives the entities as they are
  now. In `onTick` that includes `room.tick` itself, since the tick is
  recorded once its state update goes out.
- A history the room's `history` doesn't list throws.

## Checking a shot

The shot travels in the player's input, so the room knows which tick the
player was looking at when they fired. The stepper moves players, and
`onTick`, which runs right after it, checks shots: `connection.input` is
the input applied this tick, after `parseInput`, and `connection.viewTick`
is the tick the player saw when sending it.

A stepper can't rewind, and shouldn't decide hits. It also runs on the
client to predict the local player, and a client keeps no history of the
room; a hit is the room's call. Keep the shot in the input, ignore it in
the stepper, and handle it in `onTick`.

A top-down arena, where a shot is a ray from the shooter that hits the
first other player it passes within half a unit of:

```ts
// shared.ts
import { defineHistory, defineSchema, defineStepper, entity } from "@irt/room";

export const Player = entity("player", { owner: "string", x: "f32", y: "f32", hp: "u8" });
export const schema = defineSchema({ entities: [Player] });
export const poses = defineHistory(Player, ["x", "y"]);

export interface Input {
  dx: number;
  dy: number;
  /** The direction of a shot fired this tick. */
  fire?: { x: number; y: number };
}

const SPEED = 8;
const finite = (v: unknown): v is number => typeof v === "number" && Number.isFinite(v);

export const stepper = defineStepper<Input>({
  parseInput(raw) {
    const { dx, dy, fire } = raw as { dx?: unknown; dy?: unknown; fire?: { x?: unknown; y?: unknown } };
    if (!finite(dx) || !finite(dy) || Math.hypot(dx, dy) > 1.0001) throw new TypeError("input is { dx, dy, fire? }");
    if (fire === undefined) return { dx, dy };
    if (!finite(fire.x) || !finite(fire.y) || (fire.x === 0 && fire.y === 0)) throw new TypeError("fire is a direction { x, y }");
    return { dx, dy, fire: { x: fire.x, y: fire.y } };
  },
  step(world, inputs, dt) {
    for (const p of world.all(Player)) {
      const input = inputs.get(p.owner);
      if (!input) continue;
      p.x += input.dx * SPEED * dt;
      p.y += input.dy * SPEED * dt;
    }
  },
});
```

```ts
// room.ts
import { defineRoom, type RoomEntity } from "@irt/room";
import { type Input, Player, poses, schema, stepper } from "./shared.js";

const RADIUS = 0.5;
const RANGE = 40;
const SPAWNS = [
  { x: -8, y: 0 },
  { x: 0, y: 10 },
];

type Point = { x: number; y: number };

/** How far along the shot it passes within RADIUS of `p`, or undefined if it doesn't. */
function along(from: Point, dir: Point, p: Point): number | undefined {
  const len = Math.hypot(dir.x, dir.y);
  const t = ((p.x - from.x) * dir.x + (p.y - from.y) * dir.y) / len;
  if (t < 0 || t > RANGE) return undefined;
  const miss = Math.hypot(from.x + (dir.x / len) * t - p.x, from.y + (dir.y / len) * t - p.y);
  return miss <= RADIUS ? t : undefined;
}

export default defineRoom({
  schema,
  stepper,
  history: [poses],
  onJoin(room, conn) {
    const spawn = SPAWNS[(room.connections.size - 1) % SPAWNS.length]!;
    conn.data.player = [...room.entities.all(Player)].find((e) => e.owner === conn.player) ?? room.entities.create(Player, { owner: conn.player, ...spawn, hp: 100 });
  },
  onPlayerLeave(room, player) {
    for (const e of room.entities.all(Player)) if (e.owner === player) room.entities.delete(e);
  },
  onTick(room) {
    for (const conn of room.connections.values()) {
      const fire = (conn.input as Input | undefined)?.fire;
      const shooter: RoomEntity<typeof Player> = conn.data.player;
      if (!fire) continue;
      // The shot leaves from where the shooter is now, and hits others where the shooter saw them.
      let nearest: { id: number; t: number } | undefined;
      for (const target of room.rewind(poses, conn.viewTick).values()) {
        if (target.id === shooter.id) continue;
        const t = along(shooter, fire, target);
        if (t !== undefined && (!nearest || t < nearest.t)) nearest = { id: target.id, t };
      }
      // The target may have left since the tick the shooter saw.
      const hit = nearest && (room.entities.get(nearest.id) as RoomEntity<typeof Player> | undefined);
      if (!hit) continue;
      hit.hp = Math.max(0, hit.hp - 25);
      room.broadcast({ type: "hit", shooter: conn.player, target: hit.owner });
    }
  },
});
```

The client draws other players with `room.interpolate`, which is what
the view tick describes, and fires toward a click:

```ts
// client.ts
import { connect } from "@irt/client";
import { type Input, Player, schema, stepper } from "./shared.js";

const room = await connect({ router: "wss://eu.irt.io", project: "arena", room: "pit-1", schema });
const UNIT = 32;

const keys = new Set<string>();
addEventListener("keydown", (e) => keys.add(e.key));
addEventListener("keyup", (e) => keys.delete(e.key));
let clicked: { x: number; y: number } | undefined;
addEventListener("pointerdown", (e) => (clicked = { x: e.clientX / UNIT, y: e.clientY / UNIT }));

const me = room.predict(stepper, {
  find: (r) => [...r.entities.all(Player)].find((p) => p.owner === r.player),
  sample: (prediction): Input => {
    const input: Input = {
      dx: (keys.has("ArrowRight") ? 1 : 0) - (keys.has("ArrowLeft") ? 1 : 0),
      dy: (keys.has("ArrowDown") ? 1 : 0) - (keys.has("ArrowUp") ? 1 : 0),
    };
    const at = prediction.entity;
    if (clicked && at && (clicked.x !== at.x || clicked.y !== at.y)) input.fire = { x: clicked.x - at.x, y: clicked.y - at.y };
    clicked = undefined;
    return input;
  },
});
const others = room.interpolate(Player, ["x", "y"]);
room.on("message", (m) => console.log(m));

function frame() {
  const local = me.render(["x", "y"]);
  for (const p of room.entities.all(Player)) {
    const at = p.owner === room.player ? local : others.get(p);
    console.log(p.owner, p.hp, at.x * UNIT, at.y * UNIT);
  }
  requestAnimationFrame(frame);
}
frame();
```

The test has the target run past while the shooter fires at it as the
shooter's client draws it. By the time the room checks the shot, the
target is more than a unit further on, three times the hit radius:

```ts
// arena.test.ts
import type { RoomClient } from "@irt/client";
import { testRoom } from "@irt/testing";
import { expect, test } from "vitest";
import room from "./room.js";
import { type Input, Player, stepper } from "./shared.js";

/** Fires once at the target as the shooter's client draws it, `offset` units along x; returns the target's hp. */
async function arena(offset: number) {
  const t = testRoom(room);
  const target = await t.connect({ player: "target" });
  const shooter = await t.connect({ player: "shooter" });
  const drawn = shooter.client.interpolate(Player, ["x", "y"]);
  const find = (r: RoomClient) => [...r.entities.all(Player)].find((p) => p.owner === r.player);
  target.predict(stepper, { find, sample: () => ({ dx: 1, dy: 0 }) });
  let shots = 0;
  shooter.predict(stepper, {
    find,
    sample: (prediction): Input => {
      const me = prediction.entity;
      const other = [...shooter.client.entities.all(Player)].find((p) => p.owner === "target");
      if (t.live.tick < 20 || shots > 0 || !me || !other) return { dx: 0, dy: 0 };
      shots++;
      const at = drawn.get(other);
      return { dx: 0, dy: 0, fire: { x: at.x + offset - me.x, y: at.y - me.y } };
    },
  });
  await t.host.tick(30);
  return [...t.live.entities.all(Player)].find((p) => p.owner === "target")!.hp;
}

test("a shot at the target where the shooter saw it hits, though it has moved on", async () => {
  expect(await arena(0)).toBe(75);
});

test("a shot that missed on the shooter's screen misses", async () => {
  expect(await arena(-1)).toBe(100);
});
```

The shooter's own position is the present one: its client predicts it,
so the room's present position is what the shooter saw of itself.

## With physics

A Rapier world holds the present, and rebuilding one for a past tick
costs too much for a hit check. `@irt/physics` instead offers
`castRay(ray, maxToi, targets, shape)`, which casts a ray against
targets placed at their poses with Rapier shapes, with no world. Record
the pose fields in a history, rewind, and cast against the result:

```ts
// soldiers.ts
import { defineHistory, defineRoom, defineSchema, entity, type RoomEntity } from "@irt/room";
import { body, castRay, physics, RAPIER } from "@irt/physics/rapier3d";

const Soldier = entity("soldier", { owner: "string", x: "f32", y: "f32", z: "f32", vy: "f32" });
const poses = defineHistory(Soldier, ["x", "y", "z"]);
const capsule = new RAPIER.Capsule(0.5, 0.3);

type Input = { dx: number; dz: number; fire?: { x: number; y: number; z: number } };

const stepper = physics<Input>({
  gravity: { x: 0, y: -9.81, z: 0 },
  bodies: [
    body(Soldier, {
      type: "character",
      colliders: () => RAPIER.ColliderDesc.capsule(0.5, 0.3),
      player: (s) => s.owner,
      control({ entity: s, move }, input, dt) {
        s.vy -= 9.81 * dt;
        if (move((input?.dx ?? 0) * 6 * dt, s.vy * dt, (input?.dz ?? 0) * 6 * dt) && s.vy < 0) s.vy = 0;
      },
    }),
  ],
});

export default defineRoom({
  schema: defineSchema({ entities: [Soldier] }),
  stepper,
  history: [poses],
  onJoin(room, conn) {
    conn.data.soldier = room.entities.create(Soldier, { owner: conn.player, y: 1 });
  },
  onTick(room) {
    for (const conn of room.connections.values()) {
      const fire = (conn.input as Input | undefined)?.fire;
      const me: RoomEntity<typeof Soldier> = conn.data.soldier;
      if (!fire) continue;
      const others = [...room.rewind(poses, conn.viewTick).values()].filter((s) => s.id !== me.id);
      const ray = new RAPIER.Ray({ x: me.x, y: me.y + 0.6, z: me.z }, fire);
      const hit = castRay(ray, 100, others, () => capsule);
      if (hit) room.broadcast({ type: "hit", target: hit.target.id, distance: hit.toi });
    }
  },
});
```

Walls and other fixed bodies don't move, so their present is their
past: to stop shots at walls, cast against the room's own world with
`stepper.rapierWorld(room.entities)!.castRay(ray, maxToi, true)` and
count a player hit only if it's nearer than the wall. In 2D, `castRay`
takes poses of `x`, `y` and an optional `angle`, as the
[physics reference](/docs/reference/physics#castray) shows.

## What the room keeps

- **How far back.** A view tick is never more than `MAX_REWIND_MS`
  (250 ms) behind the room; the room clamps it. A history keeps that
  many ticks plus two: 7 ticks at 20 ticks a second, 17 at 60, 32 at 120.
- **Memory.** Each recorded entity costs 8 bytes per field per kept
  tick, plus about 128 bytes. Three fields at 60 ticks a second is 536
  bytes an entity, so 1,000 players take about half a megabyte. A room's
  histories together are capped at 8 MB (`MAX_HISTORY`). Over it, the
  room logs an error once and new entities aren't recorded, so they're
  absent from `room.rewind`, until deleted entities leave the window and
  free their share.
- **Not room state.** The history is in the room host's memory only. It
  isn't saved, isn't checkpointed, and starts empty when the room wakes
  or its room host restarts; until the room has run a tick again,
  `room.rewind` gives the present.

## Common mistakes

- **Checking hits in the stepper.** The stepper runs on the client too,
  and has no history or view tick there. Check hits in `onTick`.
- **Sending shots as messages.** A message carries no view tick, and
  `connection.viewTick` is that of the last input, which may be from
  another tick. Put the shot in the input.
- **Rewinding the shooter.** Its client predicts it, so it saw itself
  where the room has it now. Fire from the present position.
- **Recording every field.** Each field costs 8 bytes per tick for every
  entity of the kind. Record what the hit check reads.
- **Drawing others without interpolation.** The view tick is what
  `room.interpolate` draws. A client that draws the latest state shows
  entities about two ticks ahead of its view tick, and shots there land
  behind the target.
- **Trusting a rewound hit absolutely.** Rewinding favours the shooter:
  a target that just ran behind a wall can still be hit, by up to the
  shooter's lag. The 250 ms cap bounds it.
