benjivers/val Public Public: anyone can read this; the crew can change it.

Files af76783003008f39b49ed099fd4a39b3eb6afc55
136 lines · 4.8 KB · ~1.4k tok Raw History
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
}
78
79
/**
80
 * Where a body's arrival begins: a stride back down the line from where it
81
 * was last seen. Somebody who turns up across the map is not the same walk,
82
 * and somebody who has not moved has no direction to come from, so neither
83
 * slides.
84
 */
85
function entryPoint(before, body) {
86
  const dx = body.x - before.x;
87
  const dy = body.y - before.y;
88
  const span = Math.hypot(dx, dy);
89
  if (span < 2 || span > REACH) return null;
90
  const back = Math.min(ENTRY, span) / span;
91
  return { x: body.x - dx * back, y: body.y - dy * back };
92
}
93
94
/** How far in a body is: 0 the moment it appears, 1 once it is solid. */
95
export function shown(presence, id, now) {
96
  const it = presence.live.get(id);
97
  return it ? clamp01((now - it.since) / FADE) : 1;
98
}
99
100
/** The size a body is drawn at while it arrives: small, then itself. */
101
export const swell = (t) => SMALL + (1 - SMALL) * clamp01(t);
102
103
/**
104
 * The position to draw a body at: the interpolated one, or a point on the
105
 * way there from where it came in, while it is still arriving.
106
 */
107
export function slide(presence, id, at, now, out) {
108
  const it = presence.live.get(id);
109
  if (!it || !it.from) return at;
110
  const t = clamp01((now - it.since) / FADE);
111
  if (t >= 1) return at;
112
  out.x = it.from.x + (at.x - it.from.x) * t;
113
  out.y = it.from.y + (at.y - it.from.y) * t;
114
  out.angle = at.angle;
115
  return out;
116
}
117
118
/** The last-seen marks, freshest first in nothing in particular: ≤ a team. */
119
export function ghosts(presence, now) {
120
  const out = [];
121
  for (const [id, it] of presence.last) {
122
    const left = 1 - (now - it.gone) / GHOST;
123
    if (left <= 0) {
124
      presence.last.delete(id);
125
      continue;
126
    }
127
    out.push({ id, x: it.x, y: it.y, angle: it.angle, mate: it.mate, fade: left });
128
  }
129
  return out;
130
}
131
132
/** A new round: nobody has been seen, and nobody is anywhere in particular. */
133
export function forget(presence) {
134
  presence.live.clear();
135
  presence.last.clear();
136
}
↑↓ navigate ↵ open esc close
↑↓ navigate ↵ open esc close

Keyboard shortcuts