quill/val Public Public: anyone can read this; nobody can change it.

forked from benjivers/val

workbenchthis machine
Files 2e870ef8994867709476cca0f461c0ff071a8c69
192 lines · 8.1 KB · ~2.4k tok Raw History
1
/**
2
 * Where you are, drawn at the frame rate.
3
 *
4
 * Everybody else in a snapshot is smoothed toward the last position the
5
 * server sent, which is right for a body you only hear about fifteen times a
6
 * second. Doing it to your own body is what made the view skip: the camera
7
 * was always being dragged toward a position taken sixty-six milliseconds
8
 * ago, and against a wall the server kept correcting it besides.
9
 *
10
 * So your own body is run forward here instead, from your own keys, through
11
 * the same step-and-slide the server uses (`src/glide.ts` — one copy, or the
12
 * guess fights the answer at every wall). The server is still the only thing
13
 * that decides where you are: every snapshot leans the drawn body back
14
 * toward its answer, and a real disagreement is taken outright. This is a
15
 * drawing optimisation and nothing here is ever sent anywhere.
16
 */
17
18
import { circleBlocked, slideCircle } from "../../src/engine/glide.ts";
19
20
/** Past this much disagreement the server simply wins: a spawn, a teleport. */
21
export const SNAP = 40;
22
/** How fast the rest of a disagreement is leaned off, a second. */
23
export const PULL = 12;
24
/** How far ahead of a snapshot your own input is ever carried, in seconds. */
25
export const LEAD_MAX = 0.25;
26
/**
27
 * How long your own keys take to show in the server's answer: a send every
28
 * 50ms, a hop each way, a tick. The drawn body is allowed this much of a
29
 * lead over the answer without being called wrong, in seconds of running —
30
 * and a disagreement inside it is leaned off gently rather than at PULL,
31
 * because most of what is inside it is not a disagreement at all but the
32
 * lag itself, which a hard pull turns into a yank at every start and stop.
33
 */
34
export const LEAD_SLACK = 0.08;
35
export const SOFT_PULL = 2;
36
37
/**
38
 * `anchored` counts the times the server's word was taken outright: every one
39
 * is a jump. `still` is how long no key has been held, in seconds.
40
 */
41
export const makeLocal = () => ({ x: 0, y: 0, ready: false, gap: 0, anchored: 0, still: 0 });
42
43
/** Take the server's word for it outright. */
44
export function anchor(local, at) {
45
  local.x = at.x;
46
  local.y = at.y;
47
  local.ready = true;
48
  local.gap = 0;
49
  local.anchored += 1;
50
  return local;
51
}
52
53
/**
54
 * One frame of your own movement: a direction, the speed the server will
55
 * move you at, and the world to slide against — `{ solid, tile, r }`.
56
 */
57
export function advance(local, mx, my, speed, dt, world) {
58
  if (!local.ready) return local;
59
  const len = Math.hypot(mx, my);
60
  if (len > 1) {
61
    mx /= len;
62
    my /= len;
63
  }
64
  if (speed <= 0 || (mx === 0 && my === 0) || dt <= 0) return local;
65
  const to = slideCircle(local.x, local.y, mx * speed * dt, my * speed * dt, world.r, world.solid, world.tile);
66
  local.x = to.x;
67
  local.y = to.y;
68
  return local;
69
}
70
71
/**
72
 * Where the server's last answer has got to by now.
73
 *
74
 * A snapshot is a position from a moment ago, and the input you have held
75
 * since it was sent has not reached it yet. Rather than a sequence number
76
 * the server echoes back, the unacknowledged part of your own movement is
77
 * measured in time: the age of the snapshot, capped, walked through the
78
 * same slide. Hold nothing and this is the server's answer exactly, which
79
 * is the point — it is what the drawn body settles onto.
80
 */
81
export function project(at, mx, my, speed, age, world) {
82
  const len = Math.hypot(mx, my);
83
  if (len > 1) {
84
    mx /= len;
85
    my /= len;
86
  }
87
  const ahead = Math.min(LEAD_MAX, Math.max(0, age));
88
  if (speed <= 0 || ahead === 0 || (mx === 0 && my === 0)) return { x: at.x, y: at.y };
89
  return slideCircle(at.x, at.y, mx * speed * ahead, my * speed * ahead, world.r, world.solid, world.tile);
90
}
91
92
/**
93
 * Lean the drawn body toward the server's answer. A small disagreement —
94
 * the width of the guess, a shot of lag — is walked off over a moment; a
95
 * large one is not a disagreement but a different place, and is taken.
96
 *
97
 * With keys held (`mx, my`), the part of the gap along the way you are going
98
 * is the lag when the drawn body is ahead: the server has not heard the keys
99
 * yet. Inside `slack` that is left exactly alone — pulling it is what made
100
 * every start a yank and every stop a drift. Across the way you are going,
101
 * or with the server ahead, a gap is real and is leaned off at PULL. Having
102
 * just stopped, a gap inside the slack is the server still catching up to
103
 * where you stopped: held for the lag, then leaned off gently enough not to
104
 * show.
105
 */
106
export function reconcile(local, server, dt, mx = 0, my = 0, slack = 0) {
107
  if (!local.ready) return anchor(local, server);
108
  const dx = server.x - local.x;
109
  const dy = server.y - local.y;
110
  local.gap = Math.hypot(dx, dy);
111
  if (local.gap > SNAP) return anchor(local, server);
112
  const hard = 1 - Math.exp(-PULL * Math.max(0, dt));
113
  const len = Math.hypot(mx, my);
114
  if (len === 0) {
115
    if (local.gap <= slack && local.still < LEAD_SLACK) return local;
116
    const k = local.gap > slack ? hard : 1 - Math.exp(-SOFT_PULL * Math.max(0, dt));
117
    local.x += dx * k;
118
    local.y += dy * k;
119
    return local;
120
  }
121
  const ux = mx / len;
122
  const uy = my / len;
123
  const along = dx * ux + dy * uy; // positive: the server is ahead of the drawing
124
  const acrossX = dx - along * ux;
125
  const acrossY = dy - along * uy;
126
  local.x += acrossX * hard;
127
  local.y += acrossY * hard;
128
  if (along > 0 || -along > slack) {
129
    local.x += ux * along * hard;
130
    local.y += uy * along * hard;
131
  }
132
  return local;
133
}
134
135
/**
136
 * Never further past the answer than the lag. The guess may fill the gap to
137
 * where the server's word and your own held keys put you, plus `slack` —
138
 * the distance your keys travel before the server has heard them — and no
139
 * further: left to run, a guess that moves faster than its target is
140
 * corrected finds its own equilibrium somewhere beyond it, which is a claim
141
 * about a position rather than a drawing of one.
142
 */
143
export function holdBack(local, target, mx, my, slack = 0) {
144
  const len = Math.hypot(mx, my);
145
  if (len === 0) return local;
146
  const dx = mx / len;
147
  const dy = my / len;
148
  const over = (local.x - target.x) * dx + (local.y - target.y) * dy - slack;
149
  if (over > 0) {
150
    local.x -= dx * over;
151
    local.y -= dy * over;
152
  }
153
  return local;
154
}
155
156
/** A body drawn inside a wall is worse than a body drawn late. */
157
export const insideWall = (local, world) => circleBlocked(local.x, local.y, world.r, world.solid, world.tile);
158
159
/**
160
 * One frame of your own body, the whole of it: the server's answer `me`
161
 * carried `age` seconds forward by the keys `held`, the drawn body leaned
162
 * onto that, advanced a frame, and never drawn past it.
163
 *
164
 * A lean across a corner can put the drawn body inside a wall, and `refused`
165
 * is the caller's word for anywhere else the server would not let you stand
166
 * (the spawn pen in the buy phase). Both used to take the server's answer,
167
 * which is a jump: pressed against a wall and strafing, that fired most
168
 * frames and read as a stutter. Holding the last good place is invisible and
169
 * just as honest — the next snapshot corrects it anyway. Anchoring is kept
170
 * for the one case where holding still leaves the body in a wall.
171
 */
172
export function stepLocal(local, me, held, speed, age, dt, world, refused) {
173
  const target = project(me, held.mx, held.my, speed, age, world);
174
  const wasX = local.x;
175
  const wasY = local.y;
176
  const slack = speed * LEAD_SLACK;
177
  local.still = held.mx === 0 && held.my === 0 ? local.still + dt : 0;
178
  reconcile(local, target, dt, held.mx, held.my, slack);
179
  advance(local, held.mx, held.my, speed, dt, world);
180
  holdBack(local, target, held.mx, held.my, slack);
181
  if (insideWall(local, world) || (refused !== null && refused(local.x, local.y))) {
182
    local.x = wasX;
183
    local.y = wasY;
184
    // Holding is for a step the server would refuse, never for the server's
185
    // own answer: a barrier dropped on somebody standing still leaves them
186
    // inside it on the server too, and a hold that outranked that would keep
187
    // the drawn body wherever it last stood, a room away, for as long as the
188
    // barrier lasted. Past the snap distance, or in a wall, the answer wins.
189
    if (insideWall(local, world) || Math.hypot(local.x - me.x, local.y - me.y) > SNAP) anchor(local, me);
190
  }
191
  return local;
192
}
↑↓ navigate ↵ open esc close
↑↓ navigate ↵ open esc close

Keyboard shortcuts