/** * Where you are, drawn at the frame rate. * * Everybody else in a snapshot is smoothed toward the last position the * server sent, which is right for a body you only hear about fifteen times a * second. Doing it to your own body is what made the view skip: the camera * was always being dragged toward a position taken sixty-six milliseconds * ago, and against a wall the server kept correcting it besides. * * So your own body is run forward here instead, from your own keys, through * the same step-and-slide the server uses (`src/glide.ts` — one copy, or the * guess fights the answer at every wall). The server is still the only thing * that decides where you are: every snapshot leans the drawn body back * toward its answer, and a real disagreement is taken outright. This is a * drawing optimisation and nothing here is ever sent anywhere. */ import { circleBlocked, slideCircle } from "../src/glide.ts"; /** Past this much disagreement the server simply wins: a spawn, a teleport. */ export const SNAP = 40; /** How fast the rest of a disagreement is leaned off, a second. */ export const PULL = 12; /** How far ahead of a snapshot your own input is ever carried, in seconds. */ export const LEAD_MAX = 0.25; export const makeLocal = () => ({ x: 0, y: 0, ready: false, gap: 0 }); /** Take the server's word for it outright. */ export function anchor(local, at) { local.x = at.x; local.y = at.y; local.ready = true; local.gap = 0; return local; } /** * One frame of your own movement: a direction, the speed the server will * move you at, and the world to slide against — `{ solid, tile, r }`. */ export function advance(local, mx, my, speed, dt, world) { if (!local.ready) return local; const len = Math.hypot(mx, my); if (len > 1) { mx /= len; my /= len; } if (speed <= 0 || (mx === 0 && my === 0) || dt <= 0) return local; const to = slideCircle(local.x, local.y, mx * speed * dt, my * speed * dt, world.r, world.solid, world.tile); local.x = to.x; local.y = to.y; return local; } /** * Where the server's last answer has got to by now. * * A snapshot is a position from a moment ago, and the input you have held * since it was sent has not reached it yet. Rather than a sequence number * the server echoes back, the unacknowledged part of your own movement is * measured in time: the age of the snapshot, capped, walked through the * same slide. Hold nothing and this is the server's answer exactly, which * is the point — it is what the drawn body settles onto. */ export function project(at, mx, my, speed, age, world) { const len = Math.hypot(mx, my); if (len > 1) { mx /= len; my /= len; } const ahead = Math.min(LEAD_MAX, Math.max(0, age)); if (speed <= 0 || ahead === 0 || (mx === 0 && my === 0)) return { x: at.x, y: at.y }; return slideCircle(at.x, at.y, mx * speed * ahead, my * speed * ahead, world.r, world.solid, world.tile); } /** * Lean the drawn body toward the server's answer. A small disagreement — * the width of the guess, a shot of lag — is walked off over a moment; a * large one is not a disagreement but a different place, and is taken. */ export function reconcile(local, server, dt) { if (!local.ready) return anchor(local, server); const dx = server.x - local.x; const dy = server.y - local.y; local.gap = Math.hypot(dx, dy); if (local.gap > SNAP) return anchor(local, server); const k = 1 - Math.exp(-PULL * Math.max(0, dt)); local.x += dx * k; local.y += dy * k; return local; } /** * Never past the answer. The guess may fill the gap to where the server's * word and your own held keys put you and no further: left to run, a guess * that moves faster than its target is corrected finds its own equilibrium * somewhere beyond it, which is a claim about a position rather than a * drawing of one. */ export function holdBack(local, target, mx, my) { const len = Math.hypot(mx, my); if (len === 0) return local; const dx = mx / len; const dy = my / len; const over = (local.x - target.x) * dx + (local.y - target.y) * dy; if (over > 0) { local.x -= dx * over; local.y -= dy * over; } return local; } /** A body drawn inside a wall is worse than a body drawn late. */ export const insideWall = (local, world) => circleBlocked(local.x, local.y, world.r, world.solid, world.tile);