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

forked from benjivers/val

workbenchthis machine
Files d9bf83f2db4463f3e78b5c70a7dc50007df057db
1
/**
2
 * How a body moves through a grid of solid tiles: the collision, the slide
3
 * along a wall, and how fast it goes.
4
 *
5
 * One module because two programs have to agree about it. The server owns
6
 * where everybody is; the browser predicts where *you* are between the
7
 * snapshots it is sent, and a prediction that resolves a wall differently
8
 * from the server is a camera that fights the server every tick. Written
9
 * twice this drifts; written once, the guess lands on the answer.
10
 *
11
 * Nothing here knows about the map, the match or a player: the caller says
12
 * which tiles are solid and how big one is. It is the server's `map.ts` and
13
 * the browser's `map.json` asking the same question of the same maths.
14
 */
15
16
export type Solid = (col: number, row: number) => boolean;
17
export type Point = { x: number; y: number };
18
19
/** The radius of a body on this map, and how fast one runs. */
20
export const BODY_R = 16;
21
export const RUN = 250;
22
23
/** How near a wall a refused step creeps: halvings of what was refused. */
24
const CREEP_STEPS = 6;
25
26
/** True when a circle at (x,y) overlaps any solid tile. */
27
export function circleBlocked(x: number, y: number, r: number, solid: Solid, tile: number): boolean {
28
  const c0 = Math.floor((x - r) / tile);
29
  const c1 = Math.floor((x + r) / tile);
30
  const r0 = Math.floor((y - r) / tile);
31
  const r1 = Math.floor((y + r) / tile);
32
  for (let row = r0; row <= r1; row++)
33
    for (let col = c0; col <= c1; col++) {
34
      if (!solid(col, row)) continue;
35
      const nearX = Math.max(col * tile, Math.min(x, col * tile + tile));
36
      const nearY = Math.max(row * tile, Math.min(y, row * tile + tile));
37
      const dx = x - nearX;
38
      const dy = y - nearY;
39
      if (dx * dx + dy * dy < r * r) return true;
40
    }
41
  return false;
42
}
43
44
/** The largest fraction of a refused step that is still free, by halving. */
45
function creep(delta: number, free: (v: number) => boolean): number {
46
  let lo = 0;
47
  let hi = delta;
48
  for (let i = 0; i < CREEP_STEPS; i++) {
49
    const mid = (lo + hi) / 2;
50
    if (free(mid)) lo = mid;
51
    else hi = mid;
52
  }
53
  return lo;
54
}
55
56
/**
57
 * Move a circle by (dx,dy), sliding along whatever it runs into.
58
 *
59
 * The motion is projected onto the wall it meets — the blocked component is
60
 * dropped, never snapped to the tile edge. A snap makes a body against a
61
 * wall alternate between a snapped coordinate and a free one, which is a
62
 * position that disagrees with the input every other tick; a projection is
63
 * a fixed point, so a body pressed into a corner stays exactly where it is.
64
 *
65
 * Which component survives is decided by the motion (the longer one wins a
66
 * corner it is clipping), not by an axis resolved first, so walking into a
67
 * corner ends in one place whichever way the diagonal is written.
68
 */
69
export function slideCircle(
70
  x: number,
71
  y: number,
72
  dx: number,
73
  dy: number,
74
  r: number,
75
  solid: Solid,
76
  tile: number,
77
): Point {
78
  if (dx === 0 && dy === 0) return { x, y };
79
  const free = (px: number, py: number): boolean => !circleBlocked(px, py, r, solid, tile);
80
  if (free(x + dx, y + dy)) return { x: x + dx, y: y + dy };
81
82
  const okX = dx !== 0 && free(x + dx, y);
83
  const okY = dy !== 0 && free(x, y + dy);
84
  const longerIsX = Math.abs(dx) >= Math.abs(dy);
85
  let ax = okX ? dx : 0;
86
  let ay = okY ? dy : 0;
87
  // Both axes open but the diagonal shut is a corner being clipped: the
88
  // longer component goes through whole and the shorter one creeps.
89
  if (okX && okY) {
90
    if (longerIsX) ay = 0;
91
    else ax = 0;
92
  }
93
94
  let nx = x + ax;
95
  let ny = y + ay;
96
  // Whatever was refused still creeps as near the wall as it fits, rather
97
  // than stopping a whole step short of it — a step is a third of a body.
98
  const creepX = (): void => {
99
    if (ax === 0 && dx !== 0) nx = x + creep(dx, (v) => free(x + v, ny));
100
  };
101
  const creepY = (): void => {
102
    if (ay === 0 && dy !== 0) ny = y + creep(dy, (v) => free(nx, y + v));
103
  };
104
  if (longerIsX) {
105
    creepX();
106
    creepY();
107
  } else {
108
    creepY();
109
    creepX();
110
  }
111
  return { x: nx, y: ny };
112
}
113
114
/**
115
 * How fast a body moves, given what is on it. The browser reads the same
116
 * four facts out of its snapshot that the match reads off a player, so a
117
 * prediction runs at the speed the server is about to use.
118
 */
119
export function moveSpeed(state: { haste?: boolean; slow?: boolean; stopped?: boolean; heavy?: boolean }): number {
120
  if (state.stopped) return 0;
121
  let speed = RUN;
122
  if (state.haste) speed *= 1.35;
123
  if (state.slow) speed *= 0.55;
124
  if (state.heavy) speed *= 0.86;
125
  return speed;
126
}
↑↓ navigate ↵ open esc close
↑↓ navigate ↵ open esc close

Keyboard shortcuts