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

Files af76783003008f39b49ed099fd4a39b3eb6afc55
206 lines · 6.9 KB · ~2.0k tok Raw History
1
/**
2
 * The board from above.
3
 *
4
 * It is three things: the minimap in the corner, the full-screen map Tab
5
 * holds open, and the view a phone plays in, where a raycaster with touch
6
 * look would be miserable. One function, drawn into whatever box it is given.
7
 *
8
 * Nothing here knows what the game is. The tiles, the shots the server let
9
 * us see, the last-seen marks and the bodies are generic; what a letter on
10
 * the grid is coloured, what else lies on the floor and what a body wears
11
 * for a badge, the game says through `opts` — see `drawTopdown`.
12
 */
13
14
const WALL_COLOUR = "#20242d";
15
const FLOOR_COLOUR = "#171a21";
16
17
const plainTile = (cell) => (cell === "#" ? WALL_COLOUR : FLOOR_COLOUR);
18
const never = () => false;
19
const nothing = () => {};
20
const grey = () => "#888";
21
22
/**
23
 * Draw the whole map into `box` (canvas pixels). `look(player)` gives the
24
 * smoothed position; `opts.labels` draws the grid's letters and names, which
25
 * only fit on the big version; `opts.marks` are the last-seen rings from
26
 * `presence.js`, which answer "where did they go" without claiming anybody
27
 * is there now; `opts.tiles: false` leaves out the map itself, for a caller
28
 * that has already drawn it once and kept it.
29
 *
30
 * The game's holes, each optional: `opts.tile(cell)` colours a letter,
31
 * `opts.label(cell)` says whether the letter is written on the big map,
32
 * `opts.under(ctx, snap, s)` draws what lies on the floor beneath the bodies,
33
 * `opts.over(ctx, snap, s)` what hides them from above, `opts.trim(player)`
34
 * is the ring round a body and `opts.badges(ctx, player, x, y, r)` whatever it
35
 * wears. `s` is world units to canvas pixels.
36
 *
37
 * Called with no snapshot it draws the tiles and stops, which is exactly
38
 * that layer.
39
 */
40
export function drawTopdown(ctx, map, snap, look, box, opts = {}) {
41
  const cols = map.rows[0].length;
42
  const s = box.w / (cols * map.tile);
43
  const labels = opts.labels !== false;
44
  const tiles = opts.tiles !== false;
45
  const tileColour = opts.tile || plainTile;
46
  const label = opts.label || never;
47
  ctx.save();
48
  ctx.beginPath();
49
  ctx.rect(box.x, box.y, box.w, box.h);
50
  ctx.clip();
51
  ctx.translate(box.x, box.y);
52
53
  const t = map.tile * s;
54
  if (tiles)
55
    for (let row = 0; row < map.rows.length; row++)
56
      for (let col = 0; col < map.rows[row].length; col++) {
57
        const cell = map.rows[row][col];
58
        ctx.fillStyle = tileColour(cell);
59
        ctx.fillRect(col * t, row * t, t + 0.6, t + 0.6);
60
        if (labels && label(cell)) {
61
          ctx.fillStyle = "#6b5f3a";
62
          ctx.font = `${Math.round(t * 0.5)}px system-ui, sans-serif`;
63
          ctx.fillText(cell, col * t + t * 0.3, row * t + t * 0.68);
64
        }
65
      }
66
67
  if (!snap) {
68
    ctx.restore();
69
    return;
70
  }
71
72
  (opts.under || nothing)(ctx, snap, s);
73
74
  for (const shot of snap.shots) {
75
    ctx.strokeStyle = shot.hit ? "rgba(255,120,110,0.85)" : "rgba(255,240,180,0.5)";
76
    ctx.lineWidth = Math.max(1, 2 * s);
77
    ctx.beginPath();
78
    ctx.moveTo(shot.x1 * s, shot.y1 * s);
79
    ctx.lineTo(shot.x2 * s, shot.y2 * s);
80
    ctx.stroke();
81
  }
82
83
  // Under the living: a ring where somebody was last seen is never allowed
84
  // to sit on top of a body that is actually in sight.
85
  if (opts.marks) for (const mark of opts.marks) drawMark(ctx, mark, s);
86
  const trim = opts.trim || grey;
87
  const badges = opts.badges || nothing;
88
  for (const player of snap.players) drawPlayer(ctx, player, look(player), s, snap.me, labels, trim, badges);
89
  (opts.over || nothing)(ctx, snap, s);
90
  ctx.restore();
91
}
92
93
/**
94
 * Where somebody was last seen: a hollow ring and the way they were facing,
95
 * dimming away. Hollow because a filled dot on this map means a body, and
96
 * this is a memory of one.
97
 */
98
function drawMark(ctx, m, s) {
99
  const x = m.x * s;
100
  const y = m.y * s;
101
  const r = Math.max(2.5, 16 * s);
102
  ctx.save();
103
  ctx.globalAlpha = 0.8 * m.fade;
104
  ctx.strokeStyle = m.mate ? "#4fb3d9" : "#e35d5b";
105
  ctx.lineWidth = Math.max(1, 2 * s);
106
  disc(ctx, x, y, r, false, true);
107
  ctx.beginPath();
108
  ctx.moveTo(x, y);
109
  ctx.lineTo(x + Math.cos(m.angle) * r * 1.7, y + Math.sin(m.angle) * r * 1.7);
110
  ctx.stroke();
111
  ctx.restore();
112
}
113
114
export function disc(ctx, x, y, r, fill, stroke) {
115
  ctx.beginPath();
116
  ctx.arc(x, y, r, 0, 7);
117
  if (fill) ctx.fill();
118
  if (stroke) ctx.stroke();
119
}
120
121
/**
122
 * Everybody is a dot and you are an arrow — but a team mate is on the map
123
 * whether you can see them or not, with their initial in the dot and a cross
124
 * when they are down, and an enemy is only ever drawn while they are alive
125
 * and in sight. The map stays honest about vision and legible about your own.
126
 */
127
function drawPlayer(ctx, p, pos, s, me, labels, trim, badges) {
128
  const mine = p.id === me.id;
129
  if (!p.alive && !p.mate) return; // a corpse is not a threat and not a call-out
130
  const x = pos.x * s;
131
  const y = pos.y * s;
132
  const r = Math.max(2.5, 16 * s);
133
  const body = !p.alive ? "#555a63" : p.mate ? (mine ? "#eaeaea" : "#4fb3d9") : "#e35d5b";
134
135
  if (mine && p.alive) {
136
    ctx.save();
137
    ctx.translate(x, y);
138
    ctx.rotate(pos.angle);
139
    ctx.fillStyle = body;
140
    ctx.beginPath();
141
    ctx.moveTo(r * 1.5, 0);
142
    ctx.lineTo(-r, r * 0.9);
143
    ctx.lineTo(-r * 0.4, 0);
144
    ctx.lineTo(-r, -r * 0.9);
145
    ctx.closePath();
146
    ctx.fill();
147
    ctx.restore();
148
    return;
149
  }
150
151
  // The dead of your own team keep their place on the map, crossed out.
152
  if (!p.alive) {
153
    ctx.strokeStyle = "#6a707c";
154
    ctx.lineWidth = Math.max(1, 2 * s);
155
    ctx.beginPath();
156
    ctx.moveTo(x - r * 0.8, y - r * 0.8);
157
    ctx.lineTo(x + r * 0.8, y + r * 0.8);
158
    ctx.moveTo(x + r * 0.8, y - r * 0.8);
159
    ctx.lineTo(x - r * 0.8, y + r * 0.8);
160
    ctx.stroke();
161
    if (labels) {
162
      ctx.fillStyle = "rgba(180,185,195,0.6)";
163
      ctx.font = `${Math.round(11 * Math.max(1, s * 2))}px system-ui, sans-serif`;
164
      ctx.textAlign = "center";
165
      ctx.fillText(p.name, x, y - r * 1.9);
166
      ctx.textAlign = "left";
167
    }
168
    return;
169
  }
170
171
  ctx.fillStyle = body;
172
  ctx.beginPath();
173
  ctx.arc(x, y, r, 0, 7);
174
  ctx.fill();
175
  ctx.strokeStyle = trim(p);
176
  ctx.lineWidth = Math.max(1, 3 * s);
177
  ctx.stroke();
178
  ctx.strokeStyle = body;
179
  ctx.beginPath();
180
  ctx.moveTo(x, y);
181
  ctx.lineTo(x + Math.cos(pos.angle) * r * 2, y + Math.sin(pos.angle) * r * 2);
182
  ctx.stroke();
183
184
  // The initial inside the dot, when the dot is big enough to hold one.
185
  if (p.mate && r >= 6) {
186
    ctx.fillStyle = "#0c1016";
187
    ctx.font = `${Math.round(r * 1.2)}px system-ui, sans-serif`;
188
    ctx.textAlign = "center";
189
    ctx.fillText((p.name[0] || "?").toUpperCase(), x, y + r * 0.45);
190
    ctx.textAlign = "left";
191
  }
192
  badges(ctx, p, x, y, r);
193
  if (labels) {
194
    ctx.fillStyle = "rgba(235,235,235,0.75)";
195
    ctx.font = `${Math.round(11 * Math.max(1, s * 2))}px system-ui, sans-serif`;
196
    ctx.textAlign = "center";
197
    ctx.fillText(p.name, x, y - r * 1.9);
198
    ctx.textAlign = "left";
199
  }
200
  if (p.hp < 100) {
201
    ctx.fillStyle = "#0008";
202
    ctx.fillRect(x - r, y + r * 1.2, r * 2, 3);
203
    ctx.fillStyle = "#6fcf6f";
204
    ctx.fillRect(x - r, y + r * 1.2, r * 2 * (p.hp / 100), 3);
205
  }
206
}
↑↓ navigate ↵ open esc close
↑↓ navigate ↵ open esc close

Keyboard shortcuts