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

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

Keyboard shortcuts