pike/val Public Public: anyone can read this; nobody can change it.
forked from benjivers/val
workbenchthis machine
Files 8cdb40fdc72ecc54208635d67e1055af412c7408
| 1 | /** |
| 2 | * Who came into sight, and where the last one went. |
| 3 | * |
| 4 | * The server only sends bodies you can actually see, and that rule is not |
| 5 | * negotiable — but it means a body stepping into a sightline arrives with no |
| 6 | * history at all, and the interpolation makes it worse: a body that was out |
| 7 | * of sight starts fresh at its exact position, a render delay further along |
| 8 | * than where it entered view. So enemies do not walk into the room, they |
| 9 | * materialise mid-stride, and it reads as a broken game rather than a fast |
| 10 | * one. |
| 11 | * |
| 12 | * This is the fix, and it is entirely in the drawing: |
| 13 | * |
| 14 | * - A body that has just appeared is drawn faded and slightly small for a |
| 15 | * tenth of a second, so the eye reads an arrival rather than a pop. |
| 16 | * - If it was seen before and not far from where it is now, the fade starts |
| 17 | * a stride back along the line it came in on, so it slides into view from |
| 18 | * the edge instead of appearing at its destination. |
| 19 | * - Where somebody was last seen alive leaves a mark that dims away over a |
| 20 | * second. It answers "where did they go", and it is information you had |
| 21 | * already: nothing here is ever sent anywhere, and nothing is drawn that |
| 22 | * the server did not already show you. |
| 23 | * |
| 24 | * Plain data in and out. Time is seconds, positions are world units. |
| 25 | */ |
| 26 | |
| 27 | /** Seconds from first sight to a solid body. */ |
| 28 | export const FADE = 0.12; |
| 29 | /** How small a body starts, as a fraction of itself. */ |
| 30 | export const SMALL = 0.82; |
| 31 | /** Seconds a last-seen mark lingers. */ |
| 32 | export const GHOST = 1; |
| 33 | /** How far back along its path a body slides in from, in world units. */ |
| 34 | export const ENTRY = 42; |
| 35 | /** Last seen further away than this and it is a new sighting, not a return. */ |
| 36 | export const REACH = 320; |
| 37 | |
| 38 | const clamp01 = (n) => (n < 0 ? 0 : n > 1 ? 1 : n); |
| 39 | |
| 40 | export const makePresence = () => ({ live: new Map(), last: new Map(), seq: 0 }); |
| 41 | |
| 42 | /** |
| 43 | * A snapshot landed at `at`, carrying exactly these bodies. Anybody in it is |
| 44 | * in sight; anybody who was and is not has just gone out of it. |
| 45 | */ |
| 46 | export function sighted(presence, bodies, at) { |
| 47 | const seq = ++presence.seq; |
| 48 | for (const body of bodies) { |
| 49 | const have = presence.live.get(body.id); |
| 50 | if (have) { |
| 51 | remember(have, body, seq); |
| 52 | continue; |
| 53 | } |
| 54 | const before = presence.last.get(body.id); |
| 55 | const fresh = { since: at, seq, from: before ? entryPoint(before, body) : null }; |
| 56 | remember(fresh, body, seq); |
| 57 | presence.live.set(body.id, fresh); |
| 58 | presence.last.delete(body.id); |
| 59 | } |
| 60 | for (const [id, it] of presence.live) { |
| 61 | if (it.seq === seq) continue; |
| 62 | presence.live.delete(id); |
| 63 | // A body last seen dead is a corpse where you left it, not a question |
| 64 | // about where somebody went: no mark. |
| 65 | if (it.alive) presence.last.set(id, { ...it, gone: at }); |
| 66 | else presence.last.delete(id); |
| 67 | } |
| 68 | } |
| 69 | |
| 70 | function remember(it, body, seq) { |
| 71 | it.seq = seq; |
| 72 | it.x = body.x; |
| 73 | it.y = body.y; |
| 74 | it.angle = body.angle; |
| 75 | it.alive = body.alive; |
| 76 | it.mate = body.mate; |
| 77 | it.agent = body.agent; |
| 78 | } |
| 79 | |
| 80 | /** |
| 81 | * Where a body's arrival begins: a stride back down the line from where it |
| 82 | * was last seen. Somebody who turns up across the map is not the same walk, |
| 83 | * and somebody who has not moved has no direction to come from, so neither |
| 84 | * slides. |
| 85 | */ |
| 86 | function entryPoint(before, body) { |
| 87 | const dx = body.x - before.x; |
| 88 | const dy = body.y - before.y; |
| 89 | const span = Math.hypot(dx, dy); |
| 90 | if (span < 2 || span > REACH) return null; |
| 91 | const back = Math.min(ENTRY, span) / span; |
| 92 | return { x: body.x - dx * back, y: body.y - dy * back }; |
| 93 | } |
| 94 | |
| 95 | /** How far in a body is: 0 the moment it appears, 1 once it is solid. */ |
| 96 | export function shown(presence, id, now) { |
| 97 | const it = presence.live.get(id); |
| 98 | return it ? clamp01((now - it.since) / FADE) : 1; |
| 99 | } |
| 100 | |
| 101 | /** The size a body is drawn at while it arrives: small, then itself. */ |
| 102 | export const swell = (t) => SMALL + (1 - SMALL) * clamp01(t); |
| 103 | |
| 104 | /** |
| 105 | * The position to draw a body at: the interpolated one, or a point on the |
| 106 | * way there from where it came in, while it is still arriving. |
| 107 | */ |
| 108 | export function slide(presence, id, at, now, out) { |
| 109 | const it = presence.live.get(id); |
| 110 | if (!it || !it.from) return at; |
| 111 | const t = clamp01((now - it.since) / FADE); |
| 112 | if (t >= 1) return at; |
| 113 | out.x = it.from.x + (at.x - it.from.x) * t; |
| 114 | out.y = it.from.y + (at.y - it.from.y) * t; |
| 115 | out.angle = at.angle; |
| 116 | return out; |
| 117 | } |
| 118 | |
| 119 | /** The last-seen marks, freshest first in nothing in particular: ≤ a team. */ |
| 120 | export function ghosts(presence, now) { |
| 121 | const out = []; |
| 122 | for (const [id, it] of presence.last) { |
| 123 | const left = 1 - (now - it.gone) / GHOST; |
| 124 | if (left <= 0) { |
| 125 | presence.last.delete(id); |
| 126 | continue; |
| 127 | } |
| 128 | out.push({ id, x: it.x, y: it.y, angle: it.angle, agent: it.agent, mate: it.mate, fade: left }); |
| 129 | } |
| 130 | return out; |
| 131 | } |
| 132 | |
| 133 | /** A new round: nobody has been seen, and nobody is anywhere in particular. */ |
| 134 | export function forget(presence) { |
| 135 | presence.live.clear(); |
| 136 | presence.last.clear(); |
| 137 | } |