/** * Who came into sight, and where the last one went. * * The server only sends bodies you can actually see, and that rule is not * negotiable — but it means a body stepping into a sightline arrives with no * history at all, and the interpolation makes it worse: a body that was out * of sight starts fresh at its exact position, a render delay further along * than where it entered view. So enemies do not walk into the room, they * materialise mid-stride, and it reads as a broken game rather than a fast * one. * * This is the fix, and it is entirely in the drawing: * * - A body that has just appeared is drawn faded and slightly small for a * tenth of a second, so the eye reads an arrival rather than a pop. * - If it was seen before and not far from where it is now, the fade starts * a stride back along the line it came in on, so it slides into view from * the edge instead of appearing at its destination. * - Where somebody was last seen alive leaves a mark that dims away over a * second. It answers "where did they go", and it is information you had * already: nothing here is ever sent anywhere, and nothing is drawn that * the server did not already show you. * * Plain data in and out. Time is seconds, positions are world units. */ /** Seconds from first sight to a solid body. */ export const FADE = 0.12; /** How small a body starts, as a fraction of itself. */ export const SMALL = 0.82; /** Seconds a last-seen mark lingers. */ export const GHOST = 1; /** How far back along its path a body slides in from, in world units. */ export const ENTRY = 42; /** Last seen further away than this and it is a new sighting, not a return. */ export const REACH = 320; const clamp01 = (n) => (n < 0 ? 0 : n > 1 ? 1 : n); export const makePresence = () => ({ live: new Map(), last: new Map(), seq: 0 }); /** * A snapshot landed at `at`, carrying exactly these bodies. Anybody in it is * in sight; anybody who was and is not has just gone out of it. */ export function sighted(presence, bodies, at) { const seq = ++presence.seq; for (const body of bodies) { const have = presence.live.get(body.id); if (have) { remember(have, body, seq); continue; } const before = presence.last.get(body.id); const fresh = { since: at, seq, from: before ? entryPoint(before, body) : null }; remember(fresh, body, seq); presence.live.set(body.id, fresh); presence.last.delete(body.id); } for (const [id, it] of presence.live) { if (it.seq === seq) continue; presence.live.delete(id); // A body last seen dead is a corpse where you left it, not a question // about where somebody went: no mark. if (it.alive) presence.last.set(id, { ...it, gone: at }); else presence.last.delete(id); } } function remember(it, body, seq) { it.seq = seq; it.x = body.x; it.y = body.y; it.angle = body.angle; it.alive = body.alive; it.mate = body.mate; } /** * Where a body's arrival begins: a stride back down the line from where it * was last seen. Somebody who turns up across the map is not the same walk, * and somebody who has not moved has no direction to come from, so neither * slides. */ function entryPoint(before, body) { const dx = body.x - before.x; const dy = body.y - before.y; const span = Math.hypot(dx, dy); if (span < 2 || span > REACH) return null; const back = Math.min(ENTRY, span) / span; return { x: body.x - dx * back, y: body.y - dy * back }; } /** How far in a body is: 0 the moment it appears, 1 once it is solid. */ export function shown(presence, id, now) { const it = presence.live.get(id); return it ? clamp01((now - it.since) / FADE) : 1; } /** The size a body is drawn at while it arrives: small, then itself. */ export const swell = (t) => SMALL + (1 - SMALL) * clamp01(t); /** * The position to draw a body at: the interpolated one, or a point on the * way there from where it came in, while it is still arriving. */ export function slide(presence, id, at, now, out) { const it = presence.live.get(id); if (!it || !it.from) return at; const t = clamp01((now - it.since) / FADE); if (t >= 1) return at; out.x = it.from.x + (at.x - it.from.x) * t; out.y = it.from.y + (at.y - it.from.y) * t; out.angle = at.angle; return out; } /** The last-seen marks, freshest first in nothing in particular: ≤ a team. */ export function ghosts(presence, now) { const out = []; for (const [id, it] of presence.last) { const left = 1 - (now - it.gone) / GHOST; if (left <= 0) { presence.last.delete(id); continue; } out.push({ id, x: it.x, y: it.y, angle: it.angle, mate: it.mate, fade: left }); } return out; } /** A new round: nobody has been seen, and nobody is anywhere in particular. */ export function forget(presence) { presence.live.clear(); presence.last.clear(); }