Split The code is public; the app answers only to the crew.

The round's damage report is 64 lines nobody tests: drive drawReport with the recording context hud.test.ts already has #239

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/cs:scout/201-the-round-s-damage-report-is-64-lines-no → main 1 commit fork

  • Merged. scout/201-the-round-s-damage-report-is-64-lines-no was merged into main as 572ecec.

Review

Sign in to review this change.
3 files changed +134 −44
src/client/hud.test.ts +1 −44
@@ -11,54 +11,11 @@ import { drawHud } from "./hud.ts";
1111 import { TICK } from "./loop.ts";
1212 import { SHORT } from "./match.ts";
1313 import type { Motion } from "./motion.ts";
14 import { type Call, recorder } from "./recorder.test-helper.ts";
1415 import type { Camera } from "./renderer.ts";
1516 import { WEAPONS } from "./weapons.ts";
1617 import { applyInput, createWorld, eyePos, NO_INPUT, type Player, type World } from "./world.ts";
1718
18 type Call = {
19 name: string; args: number[]; text: string; alpha: number;
20 stroke: string; fill: string; lineWidth: number;
21 };
22
23 function recorder(): { ctx: CanvasRenderingContext2D; calls: Call[] } {
24 const calls: Call[] = [];
25 // The alpha in force when the call was made, snapshotted: a fade is a
26 // property and not an argument, so it is the only way to see one. The
27 // colours and the line width are read the same way and for the same
28 // reason — a crosshair's colour is a property of the context, not of the
29 // stroke that used it.
30 const log = (name: string) => (...args: unknown[]): void => {
31 calls.push({
32 name,
33 args: args.filter((a) => typeof a === "number") as number[],
34 text: args.filter((a) => typeof a === "string").join(" "),
35 alpha: ctx.globalAlpha,
36 stroke: typeof ctx.strokeStyle === "string" ? ctx.strokeStyle : "",
37 fill: typeof ctx.fillStyle === "string" ? ctx.fillStyle : "",
38 lineWidth: ctx.lineWidth,
39 });
40 };
41 const ctx = {
42 clearRect: log("clearRect"), fillRect: log("fillRect"), rect: log("rect"),
43 beginPath: log("beginPath"), moveTo: log("moveTo"), lineTo: log("lineTo"),
44 arc: log("arc"), stroke: log("stroke"), fill: log("fill"),
45 fillText: log("fillText"), save: log("save"), restore: log("restore"),
46 clip: log("clip"), translate: log("translate"), rotate: log("rotate"),
47 drawImage: log("drawImage"), closePath: log("closePath"),
48 measureText: () => ({ width: 10 }),
49 createRadialGradient: (...args: unknown[]) => {
50 calls.push({
51 name: "createRadialGradient", args: args as number[], text: "", alpha: 1,
52 stroke: "", fill: "", lineWidth: 1,
53 });
54 return { addColorStop: (): void => {} };
55 },
56 globalAlpha: 1, lineWidth: 1, fillStyle: "", strokeStyle: "",
57 font: "", textAlign: "", textBaseline: "",
58 };
59 return { ctx: ctx as unknown as CanvasRenderingContext2D, calls };
60 }
61
6219 const W = 1280, H = 720;
6320
6421 /** The camera `main.ts` builds for the player being drawn, punch and all aside. */
src/client/recorder.test-helper.ts added +49
@@ -0,0 +1,49 @@
1 /**
2 * A recording 2D context: no browser, no canvas, just the calls it would
3 * have made. Shared by every HUD test so there is one dialect of "fake
4 * context", not one per file.
5 */
6
7 export type Call = {
8 name: string; args: number[]; text: string; alpha: number;
9 stroke: string; fill: string; lineWidth: number;
10 };
11
12 export function recorder(): { ctx: CanvasRenderingContext2D; calls: Call[] } {
13 const calls: Call[] = [];
14 // The alpha in force when the call was made, snapshotted: a fade is a
15 // property and not an argument, so it is the only way to see one. The
16 // colours and the line width are read the same way and for the same
17 // reason — a crosshair's colour is a property of the context, not of the
18 // stroke that used it.
19 const log = (name: string) => (...args: unknown[]): void => {
20 calls.push({
21 name,
22 args: args.filter((a) => typeof a === "number") as number[],
23 text: args.filter((a) => typeof a === "string").join(" "),
24 alpha: ctx.globalAlpha,
25 stroke: typeof ctx.strokeStyle === "string" ? ctx.strokeStyle : "",
26 fill: typeof ctx.fillStyle === "string" ? ctx.fillStyle : "",
27 lineWidth: ctx.lineWidth,
28 });
29 };
30 const ctx = {
31 clearRect: log("clearRect"), fillRect: log("fillRect"), rect: log("rect"),
32 beginPath: log("beginPath"), moveTo: log("moveTo"), lineTo: log("lineTo"),
33 arc: log("arc"), stroke: log("stroke"), fill: log("fill"),
34 fillText: log("fillText"), save: log("save"), restore: log("restore"),
35 clip: log("clip"), translate: log("translate"), rotate: log("rotate"),
36 drawImage: log("drawImage"), closePath: log("closePath"),
37 measureText: () => ({ width: 10 }),
38 createRadialGradient: (...args: unknown[]) => {
39 calls.push({
40 name: "createRadialGradient", args: args as number[], text: "", alpha: 1,
41 stroke: "", fill: "", lineWidth: 1,
42 });
43 return { addColorStop: (): void => {} };
44 },
45 globalAlpha: 1, lineWidth: 1, fillStyle: "", strokeStyle: "",
46 font: "", textAlign: "", textBaseline: "",
47 };
48 return { ctx: ctx as unknown as CanvasRenderingContext2D, calls };
49 }
src/client/reportPanel.test.ts added +84
@@ -0,0 +1,84 @@
1 /**
2 * `drawReport` against a recording context: the same fake `hud.test.ts`
3 * drives the rest of the HUD with, so the panel's arithmetic is pinned
4 * without a browser.
5 */
6
7 import { expect, test } from "bun:test";
8 import { SHORT } from "./match.ts";
9 import { drawReport } from "./reportPanel.ts";
10 import { type Call, recorder } from "./recorder.test-helper.ts";
11 import { createWorld, type Player, type World } from "./world.ts";
12
13 const W = 1280, H = 720;
14
15 const draw = (into: { ctx: CanvasRenderingContext2D }, world: World, view: Player = world.me): void =>
16 drawReport(into.ctx, world, view, W, H, 1);
17
18 const texts = (calls: Call[]): string[] => calls.filter((c) => c.name === "fillText").map((c) => c.text);
19
20 for (const phase of ["freeze", "live", "planted"] as const) {
21 test(`nothing is drawn while the round is ${phase}`, () => {
22 const world = createWorld(SHORT, "me", "ct", 5, 111);
23 world.match.phase = phase;
24 const frame = recorder();
25 draw(frame, world);
26 expect(frame.calls.length).toBe(0);
27 });
28 }
29
30 test("the panel appears once the round is over", () => {
31 const world = createWorld(SHORT, "me", "ct", 5, 111);
32 world.match.phase = "over";
33 const frame = recorder();
34 draw(frame, world);
35 expect(frame.calls.length).toBeGreaterThan(0);
36 expect(frame.calls.some((c) => c.name === "fillRect")).toBe(true);
37 });
38
39 test("a round with no damage either way draws \"nothing\" twice", () => {
40 const world = createWorld(SHORT, "me", "ct", 5, 111);
41 world.match.phase = "over";
42 const frame = recorder();
43 draw(frame, world);
44 expect(texts(frame.calls).filter((t) => t === "nothing").length).toBe(2);
45 expect(texts(frame.calls)).toContain("Dealt");
46 expect(texts(frame.calls)).toContain("Took");
47 });
48
49 test("six opponents hit draws five lines, not six", () => {
50 const world = createWorld(SHORT, "me", "ct", 6, 111);
51 world.match.phase = "over";
52 const enemies = world.players.filter((p) => p.team === "t");
53 expect(enemies.length).toBe(6);
54 for (const [i, enemy] of enemies.entries()) {
55 world.damageLog[world.me.id][enemy.id] = { damage: 10 + i, hits: 1, killed: false };
56 }
57 const frame = recorder();
58 draw(frame, world);
59 const dealtLines = texts(frame.calls).filter((t) => t.includes(" dmg"));
60 expect(dealtLines.length).toBe(5);
61 });
62
63 test("the panel's width never exceeds the canvas on a narrow screen", () => {
64 const world = createWorld(SHORT, "me", "ct", 5, 111);
65 world.match.phase = "over";
66 const narrowW = 360;
67 const frame = recorder();
68 drawReport(frame.ctx, world, world.me, narrowW, H, 1);
69 const panel = frame.calls.find((c) => c.name === "fillRect") as Call;
70 const [x, , w] = panel.args;
71 expect(w).toBeLessThanOrEqual(narrowW - 24);
72 expect(x).toBeGreaterThanOrEqual(0);
73 expect(x + w).toBeLessThanOrEqual(narrowW);
74 });
75
76 test("the total line reads what report() says", () => {
77 const world = createWorld(SHORT, "me", "ct", 5, 111);
78 world.match.phase = "over";
79 const enemy = world.players.find((p) => p.team === "t") as Player;
80 world.damageLog[world.me.id][enemy.id] = { damage: 42, hits: 2, killed: true };
81 const frame = recorder();
82 draw(frame, world);
83 expect(texts(frame.calls)).toContain("Dealt 42 — took 0");
84 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs