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

forked from benjivers/val

workbenchthis machine
Files 54c3aae2ea16956d3123256dfdd05930d31be4c5
1
/**
2
 * The board from above — the drawing this game started as.
3
 *
4
 * It is three things now: 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
9
import { AGENT_COLOUR } from "./scene.js";
10
11
const TILE_COLOUR = {
12
  "#": "#20242d",
13
  A: "#2c2a22",
14
  B: "#2c2a22",
15
  a: "#2a2024",
16
  d: "#1e2630",
17
  ".": "#171a21",
18
};
19
20
/**
21
 * Draw the whole map into `box` (canvas pixels). `look(player)` gives the
22
 * smoothed position; `opts.labels` draws site letters and names, which only
23
 * fit on the big version; `opts.marks` are the last-seen rings from
24
 * `presence.js`, which answer "where did they go" without claiming anybody
25
 * is there now; `opts.tiles: false` leaves out the map itself, for a caller
26
 * that has already drawn it once and kept it — it is 570 rectangles of a
27
 * map that never changes.
28
 *
29
 * Called with no snapshot it draws the tiles and stops, which is exactly
30
 * that layer.
31
 */
32
export function drawTopdown(ctx, map, snap, look, box, opts = {}) {
33
  const cols = map.rows[0].length;
34
  const s = box.w / (cols * map.tile);
35
  const labels = opts.labels !== false;
36
  const tiles = opts.tiles !== false;
37
  ctx.save();
38
  ctx.beginPath();
39
  ctx.rect(box.x, box.y, box.w, box.h);
40
  ctx.clip();
41
  ctx.translate(box.x, box.y);
42
43
  const t = map.tile * s;
44
  if (tiles)
45
    for (let row = 0; row < map.rows.length; row++)
46
      for (let col = 0; col < map.rows[row].length; col++) {
47
        const cell = map.rows[row][col];
48
        ctx.fillStyle = TILE_COLOUR[cell] ?? "#171a21";
49
        ctx.fillRect(col * t, row * t, t + 0.6, t + 0.6);
50
        if (labels && (cell === "A" || cell === "B")) {
51
          ctx.fillStyle = "#6b5f3a";
52
          ctx.font = `${Math.round(t * 0.5)}px system-ui, sans-serif`;
53
          ctx.fillText(cell, col * t + t * 0.3, row * t + t * 0.68);
54
        }
55
      }
56
57
  if (!snap) {
58
    ctx.restore();
59
    return;
60
  }
61
62
  for (const orb of snap.orbs) {
63
    ctx.fillStyle = "#c9a0ff";
64
    ctx.beginPath();
65
    ctx.arc(orb.x * s, orb.y * s, Math.max(2, 9 * s), 0, 7);
66
    ctx.fill();
67
  }
68
  for (const zone of snap.zones) if (zone.k !== "smoke") drawZone(ctx, zone, s);
69
70
  if (snap.spike.x !== null) {
71
    const x = snap.spike.x * s;
72
    const y = snap.spike.y * s;
73
    const r = Math.max(2.5, 7 * s);
74
    ctx.fillStyle = snap.spike.planted ? (Math.floor(Date.now() / 250) % 2 ? "#ff5b4a" : "#8a2a22") : "#c8b45c";
75
    ctx.fillRect(x - r, y - r, r * 2, r * 2);
76
    if (snap.spike.planted && snap.spike.defuse > 0) {
77
      ctx.fillStyle = "#4fd6a0";
78
      ctx.fillRect(x - 20 * s, y + 12 * s, 40 * s * (snap.spike.defuse / 7), 3);
79
    }
80
  }
81
82
  for (const shot of snap.shots) {
83
    ctx.strokeStyle = shot.hit ? "rgba(255,120,110,0.85)" : "rgba(255,240,180,0.5)";
84
    ctx.lineWidth = Math.max(1, 2 * s);
85
    ctx.beginPath();
86
    ctx.moveTo(shot.x1 * s, shot.y1 * s);
87
    ctx.lineTo(shot.x2 * s, shot.y2 * s);
88
    ctx.stroke();
89
  }
90
91
  // Under the living: a ring where somebody was last seen is never allowed
92
  // to sit on top of a body that is actually in sight.
93
  if (opts.marks) for (const mark of opts.marks) drawMark(ctx, mark, s);
94
  for (const player of snap.players) drawPlayer(ctx, player, look(player), s, snap.me, labels);
95
  // Smoke last: on this view it hides what is under it, as it should.
96
  for (const zone of snap.zones) if (zone.k === "smoke" && zone.f <= 0) drawZone(ctx, zone, s);
97
  ctx.restore();
98
}
99
100
function drawZone(ctx, z, s) {
101
  const x = z.x * s;
102
  const y = z.y * s;
103
  const r = z.r * s;
104
  ctx.lineWidth = Math.max(1, s);
105
  if (z.k === "smoke") {
106
    ctx.fillStyle = "rgba(190,195,205,0.72)";
107
    disc(ctx, x, y, r, true);
108
  } else if (z.k === "molly") {
109
    ctx.fillStyle = "rgba(230,110,50,0.45)";
110
    ctx.strokeStyle = "#f08a4b";
111
    disc(ctx, x, y, r, true, true);
112
  } else if (z.k === "wall") {
113
    ctx.fillStyle = "rgba(120,200,255,0.55)";
114
    disc(ctx, x, y, r, true);
115
  } else if (z.k === "flash") {
116
    ctx.fillStyle = z.f > 0 ? "#ffe9a8" : "rgba(255,255,255,0.9)";
117
    disc(ctx, x, y, z.f > 0 ? 5 * s : 30 * s, true);
118
  } else if (z.k === "dart") {
119
    ctx.strokeStyle = "rgba(90,220,170,0.7)";
120
    disc(ctx, x, y, r, false, true);
121
  } else if (z.k === "trip") {
122
    ctx.strokeStyle = "rgba(230,200,90,0.8)";
123
    disc(ctx, x, y, r, false, true);
124
  } else if (z.k === "heal") {
125
    ctx.fillStyle = "rgba(120,230,140,0.5)";
126
    disc(ctx, x, y, 14 * s, true);
127
  }
128
}
129
130
/**
131
 * Where somebody was last seen: a hollow ring and the way they were facing,
132
 * dimming away. Hollow because a filled dot on this map means a body, and
133
 * this is a memory of one.
134
 */
135
function drawMark(ctx, m, s) {
136
  const x = m.x * s;
137
  const y = m.y * s;
138
  const r = Math.max(2.5, 16 * s);
139
  ctx.save();
140
  ctx.globalAlpha = 0.8 * m.fade;
141
  ctx.strokeStyle = m.mate ? "#4fb3d9" : "#e35d5b";
142
  ctx.lineWidth = Math.max(1, 2 * s);
143
  disc(ctx, x, y, r, false, true);
144
  ctx.beginPath();
145
  ctx.moveTo(x, y);
146
  ctx.lineTo(x + Math.cos(m.angle) * r * 1.7, y + Math.sin(m.angle) * r * 1.7);
147
  ctx.stroke();
148
  ctx.restore();
149
}
150
151
function disc(ctx, x, y, r, fill, stroke) {
152
  ctx.beginPath();
153
  ctx.arc(x, y, r, 0, 7);
154
  if (fill) ctx.fill();
155
  if (stroke) ctx.stroke();
156
}
157
158
/**
159
 * Everybody is a dot and you are an arrow — but a team mate is on the map
160
 * whether you can see them or not, with their initial in the dot and a cross
161
 * when they are down, and an enemy is only ever drawn while they are alive
162
 * and in sight. The map stays honest about vision and legible about your own.
163
 */
164
function drawPlayer(ctx, p, pos, s, me, labels) {
165
  const mine = p.id === me.id;
166
  if (!p.alive && !p.mate) return; // a corpse is not a threat and not a call-out
167
  const x = pos.x * s;
168
  const y = pos.y * s;
169
  const r = Math.max(2.5, 16 * s);
170
  const body = !p.alive ? "#555a63" : p.mate ? (mine ? "#eaeaea" : "#4fb3d9") : "#e35d5b";
171
172
  if (mine && p.alive) {
173
    ctx.save();
174
    ctx.translate(x, y);
175
    ctx.rotate(pos.angle);
176
    ctx.fillStyle = body;
177
    ctx.beginPath();
178
    ctx.moveTo(r * 1.5, 0);
179
    ctx.lineTo(-r, r * 0.9);
180
    ctx.lineTo(-r * 0.4, 0);
181
    ctx.lineTo(-r, -r * 0.9);
182
    ctx.closePath();
183
    ctx.fill();
184
    ctx.restore();
185
    return;
186
  }
187
188
  // The dead of your own team keep their place on the map, crossed out.
189
  if (!p.alive) {
190
    ctx.strokeStyle = "#6a707c";
191
    ctx.lineWidth = Math.max(1, 2 * s);
192
    ctx.beginPath();
193
    ctx.moveTo(x - r * 0.8, y - r * 0.8);
194
    ctx.lineTo(x + r * 0.8, y + r * 0.8);
195
    ctx.moveTo(x + r * 0.8, y - r * 0.8);
196
    ctx.lineTo(x - r * 0.8, y + r * 0.8);
197
    ctx.stroke();
198
    if (labels) {
199
      ctx.fillStyle = "rgba(180,185,195,0.6)";
200
      ctx.font = `${Math.round(11 * Math.max(1, s * 2))}px system-ui, sans-serif`;
201
      ctx.textAlign = "center";
202
      ctx.fillText(p.name, x, y - r * 1.9);
203
      ctx.textAlign = "left";
204
    }
205
    return;
206
  }
207
208
  ctx.fillStyle = body;
209
  ctx.beginPath();
210
  ctx.arc(x, y, r, 0, 7);
211
  ctx.fill();
212
  ctx.strokeStyle = AGENT_COLOUR[p.agent] || "#888";
213
  ctx.lineWidth = Math.max(1, 3 * s);
214
  ctx.stroke();
215
  ctx.strokeStyle = body;
216
  ctx.beginPath();
217
  ctx.moveTo(x, y);
218
  ctx.lineTo(x + Math.cos(pos.angle) * r * 2, y + Math.sin(pos.angle) * r * 2);
219
  ctx.stroke();
220
221
  // The initial inside the dot, when the dot is big enough to hold one.
222
  if (p.mate && r >= 6) {
223
    ctx.fillStyle = "#0c1016";
224
    ctx.font = `${Math.round(r * 1.2)}px system-ui, sans-serif`;
225
    ctx.textAlign = "center";
226
    ctx.fillText((p.name[0] || "?").toUpperCase(), x, y + r * 0.45);
227
    ctx.textAlign = "left";
228
  }
229
  if (p.spike) {
230
    ctx.fillStyle = "#f0d060";
231
    ctx.fillRect(x - r * 0.4, y - r * 1.8, r * 0.8, r * 0.8);
232
  }
233
  if (p.blind) {
234
    ctx.fillStyle = "rgba(255,255,255,0.8)";
235
    disc(ctx, x, y - r * 1.6, r * 0.35, true);
236
  }
237
  if (labels) {
238
    ctx.fillStyle = "rgba(235,235,235,0.75)";
239
    ctx.font = `${Math.round(11 * Math.max(1, s * 2))}px system-ui, sans-serif`;
240
    ctx.textAlign = "center";
241
    ctx.fillText(p.name, x, y - r * 1.9);
242
    ctx.textAlign = "left";
243
  }
244
  if (p.hp < 100) {
245
    ctx.fillStyle = "#0008";
246
    ctx.fillRect(x - r, y + r * 1.2, r * 2, 3);
247
    ctx.fillStyle = "#6fcf6f";
248
    ctx.fillRect(x - r, y + r * 1.2, r * 2 * (p.hp / 100), 3);
249
  }
250
}
↑↓ navigate ↵ open esc close
↑↓ navigate ↵ open esc close

Keyboard shortcuts