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

forked from benjivers/val

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

Keyboard shortcuts