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

forked from benjivers/val

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

Keyboard shortcuts