rook/val Public Public: anyone can read this; nobody can change it.
forked from benjivers/val
workbenchthis machine
Files cdce38683437375e2145e657f55d282173240782
| 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/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 | export const makeLocal = () => ({ x: 0, y: 0, ready: false, gap: 0 }); |
| 28 | |
| 29 | /** Take the server's word for it outright. */ |
| 30 | export function anchor(local, at) { |
| 31 | local.x = at.x; |
| 32 | local.y = at.y; |
| 33 | local.ready = true; |
| 34 | local.gap = 0; |
| 35 | return local; |
| 36 | } |
| 37 | |
| 38 | /** |
| 39 | * One frame of your own movement: a direction, the speed the server will |
| 40 | * move you at, and the world to slide against — `{ solid, tile, r }`. |
| 41 | */ |
| 42 | export function advance(local, mx, my, speed, dt, world) { |
| 43 | if (!local.ready) return local; |
| 44 | const len = Math.hypot(mx, my); |
| 45 | if (len > 1) { |
| 46 | mx /= len; |
| 47 | my /= len; |
| 48 | } |
| 49 | if (speed <= 0 || (mx === 0 && my === 0) || dt <= 0) return local; |
| 50 | const to = slideCircle(local.x, local.y, mx * speed * dt, my * speed * dt, world.r, world.solid, world.tile); |
| 51 | local.x = to.x; |
| 52 | local.y = to.y; |
| 53 | return local; |
| 54 | } |
| 55 | |
| 56 | /** |
| 57 | * Where the server's last answer has got to by now. |
| 58 | * |
| 59 | * A snapshot is a position from a moment ago, and the input you have held |
| 60 | * since it was sent has not reached it yet. Rather than a sequence number |
| 61 | * the server echoes back, the unacknowledged part of your own movement is |
| 62 | * measured in time: the age of the snapshot, capped, walked through the |
| 63 | * same slide. Hold nothing and this is the server's answer exactly, which |
| 64 | * is the point — it is what the drawn body settles onto. |
| 65 | */ |
| 66 | export function project(at, mx, my, speed, age, world) { |
| 67 | const len = Math.hypot(mx, my); |
| 68 | if (len > 1) { |
| 69 | mx /= len; |
| 70 | my /= len; |
| 71 | } |
| 72 | const ahead = Math.min(LEAD_MAX, Math.max(0, age)); |
| 73 | if (speed <= 0 || ahead === 0 || (mx === 0 && my === 0)) return { x: at.x, y: at.y }; |
| 74 | return slideCircle(at.x, at.y, mx * speed * ahead, my * speed * ahead, world.r, world.solid, world.tile); |
| 75 | } |
| 76 | |
| 77 | /** |
| 78 | * Lean the drawn body toward the server's answer. A small disagreement — |
| 79 | * the width of the guess, a shot of lag — is walked off over a moment; a |
| 80 | * large one is not a disagreement but a different place, and is taken. |
| 81 | */ |
| 82 | export function reconcile(local, server, dt) { |
| 83 | if (!local.ready) return anchor(local, server); |
| 84 | const dx = server.x - local.x; |
| 85 | const dy = server.y - local.y; |
| 86 | local.gap = Math.hypot(dx, dy); |
| 87 | if (local.gap > SNAP) return anchor(local, server); |
| 88 | const k = 1 - Math.exp(-PULL * Math.max(0, dt)); |
| 89 | local.x += dx * k; |
| 90 | local.y += dy * k; |
| 91 | return local; |
| 92 | } |
| 93 | |
| 94 | /** |
| 95 | * Never past the answer. The guess may fill the gap to where the server's |
| 96 | * word and your own held keys put you and no further: left to run, a guess |
| 97 | * that moves faster than its target is corrected finds its own equilibrium |
| 98 | * somewhere beyond it, which is a claim about a position rather than a |
| 99 | * drawing of one. |
| 100 | */ |
| 101 | export function holdBack(local, target, mx, my) { |
| 102 | const len = Math.hypot(mx, my); |
| 103 | if (len === 0) return local; |
| 104 | const dx = mx / len; |
| 105 | const dy = my / len; |
| 106 | const over = (local.x - target.x) * dx + (local.y - target.y) * dy; |
| 107 | if (over > 0) { |
| 108 | local.x -= dx * over; |
| 109 | local.y -= dy * over; |
| 110 | } |
| 111 | return local; |
| 112 | } |
| 113 | |
| 114 | /** A body drawn inside a wall is worse than a body drawn late. */ |
| 115 | export const insideWall = (local, world) => circleBlocked(local.x, local.y, world.r, world.solid, world.tile); |