Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions apps/web/e2e/coins-of-eight.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,9 @@ test("eight tabs agree on every kart's coins and on every loss", async ({ browse
await creator.getByRole("button", { name: "Start" }).click();
for (const page of pages) {
await expect(page.locator("#instrument-panel")).toBeVisible({ timeout: 60_000 });
await expect(page.locator('[data-instrument="coins"]')).toHaveText(
`x00 / ${bridge.tuning.coinCap}`,
);
const coins = page.locator('[data-instrument="coins"]');
await expect(coins).toHaveText("0");
await expect(coins).toHaveAttribute("aria-label", `0 of ${bridge.tuning.coinCap} coins`);
}

// Sample each tab's coin count beside the count its own newest snapshot gives.
Expand Down Expand Up @@ -141,7 +141,7 @@ test("eight tabs agree on every kart's coins and on every loss", async ({ browse
// A count that disagrees with its tab's own kart on two samples in a row;
// one alone may straddle a snapshot.
const stuck: string[] = [];
const reads = (own: number) => `x${String(own).padStart(2, "0")} / ${bridge.tuning.coinCap}`;
const reads = (own: number) => String(own);
for (let tab = 0; tab < TABS; tab++) {
const mine = samples.filter((sample) => sample.tab === tab && sample.own !== undefined);
for (let index = 1; index < mine.length; index++) {
Expand Down
6 changes: 5 additions & 1 deletion apps/web/e2e/race.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ for (const [name, url, throttleKey] of [
const surface = page.locator('[data-instrument="surface"]');
await expect(speed).toHaveText("0 km/h");
await expect(page.locator('[data-instrument="gear"]')).toHaveText("N");
await expect(page.locator('[data-instrument="coins"]')).toHaveText("x00 / 10");
await expect(page.locator('[data-instrument="coins"]')).toHaveText("0");
await expect(page.locator('[data-instrument="coins"]')).toHaveAttribute(
"aria-label",
"0 of 10 coins",
);
await expect(surface).toHaveText("asphalt");
// The Instrument panel's lap readouts come from the sim: on lap one, the
// lap clock running and no best lap yet, and last once the seven bots
Expand Down
41 changes: 32 additions & 9 deletions apps/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -305,16 +305,16 @@
}
/*
* The speed cluster (ADR 0031), bottom-left opposite the course map:
* the coin count over the shift lights, the gear on its plate under
* them, km/h beside it and the tyre gauge under that.
* the coin pile down its left (ADR 0105), the shift lights, the gear
* on its plate under them, km/h beside it and the tyre gauge under that.
*/
#instrument-panel [data-cluster] {
position: absolute;
left: calc(var(--hud) * 2);
bottom: calc(var(--hud) * 2);
display: grid;
grid-template-areas: "coins coins" "lights lights" "gear speed" "gear tyre";
grid-template-columns: auto auto;
grid-template-areas: "coins lights lights" "coins gear speed" "coins gear tyre";
grid-template-columns: auto auto auto;
column-gap: calc(var(--hud) * 1.4);
row-gap: calc(var(--hud) * 0.9);
align-items: end;
Expand Down Expand Up @@ -461,20 +461,43 @@
padding: 0.2em 0.5em;
font-size: calc(var(--hud) * 2.6);
}
/* The coin pile (ADR 0105). Empty slots keep a held coin's height so
the cluster holds still as the pile rises and falls. */
[data-instrument="coins"] {
grid-area: coins;
display: flex;
align-items: center;
gap: calc(var(--hud) * 1);
align-self: end;
display: grid;
justify-items: center;
gap: calc(var(--hud) * 0.5);
margin-right: calc(var(--hud) * 0.4);
font-size: calc(var(--hud) * 2.6);
}
[data-instrument="coins"] svg {
width: calc(var(--hud) * 2.8);
[data-coin-slots] {
display: grid;
gap: calc(var(--hud) * 0.28);
filter: drop-shadow(
calc(var(--hud) * 0.3) calc(var(--hud) * 0.3) 0
color-mix(in srgb, var(--ink) 55%, transparent)
);
}
[data-coin-slot] {
display: block;
width: calc(var(--hud) * 3.6);
height: calc(var(--hud) * 0.85);
clip-path: polygon(8% 0, 92% 0, 100% 50%, 92% 100%, 8% 100%, 0 50%);
background: linear-gradient(
transparent 32%,
color-mix(in srgb, var(--carbon-shaded) 60%, transparent) 32% 68%,
transparent 68%
);
}
[data-coin-slot][data-held="true"] {
background: linear-gradient(
var(--gold-lit) 0 40%,
var(--coin) 40% 75%,
var(--coin-rim) 75%
);
}
/* Greybox readouts: a dim line of their own, top-right, clear of the course map and the countdown (ADR 0031). */
#instrument-panel [data-greybox] {
position: absolute;
Expand Down
38 changes: 30 additions & 8 deletions apps/web/src/instrument-panel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,13 +338,13 @@ describe("the finish on the page", () => {
});

describe("the speed cluster on the page", () => {
const mount = () => {
const mount = (tuning: PanelTuning = TUNING) => {
const container = document.createElement("div");
const panel = createInstrumentPanel(
container,
3,
course,
TUNING,
tuning,
nameOf,
livery(course.ownKart),
);
Expand Down Expand Up @@ -385,7 +385,7 @@ describe("the speed cluster on the page", () => {
const { find, drive } = mount();
expect(find("speed")?.textContent).toBe("0 km/h");
expect(find("gear")?.textContent).toBe("N");
expect(find("coins")?.textContent).toBe("x00 / 10");
expect(find("coins")?.textContent).toBe("0");
drive(23.6);
expect(find("speed")?.textContent).toBe("85 km/h");
expect(find("gear")?.textContent).toBe("5");
Expand All @@ -396,12 +396,35 @@ describe("the speed cluster on the page", () => {

it("reads the coins the own kart carries, rising as it takes them and falling on a spin-out", () => {
const { find, drive } = mount();
const coins = find("coins");
expect(coins?.getAttribute("role")).toBe("img");
expect(coins?.getAttribute("aria-label")).toBe("0 of 10 coins");
drive(20, 7);
expect(find("coins")?.textContent).toBe("x07 / 10");
expect(coins?.textContent).toBe("7");
expect(coins?.getAttribute("aria-label")).toBe("7 of 10 coins");
drive(20, 10);
expect(find("coins")?.textContent).toBe("x10 / 10");
expect(coins?.textContent).toBe("10");
expect(coins?.getAttribute("aria-label")).toBe("10 of 10 coins");
drive(5, 7);
expect(find("coins")?.textContent).toBe("x07 / 10");
expect(coins?.textContent).toBe("7");
expect(coins?.getAttribute("aria-label")).toBe("7 of 10 coins");
});

it("piles the coins from the bottom, with a slot for each up to the cap", () => {
const { find, drive } = mount({ ...TUNING, coinCap: 6 });
// Slots run top to bottom on the page, so a pile filling from the bottom holds the last ones.
const held = () =>
[...(find("coins")?.querySelectorAll<HTMLElement>("[data-coin-slot]") ?? [])].map(
(slot) => slot.dataset.held,
);
expect(held()).toEqual(Array(6).fill("false"));
drive(20, 2);
expect(held()).toEqual(["false", "false", "false", "false", "true", "true"]);
drive(20, 6);
expect(held()).toEqual(Array(6).fill("true"));
expect(find("coins")?.getAttribute("aria-label")).toBe("6 of 6 coins");
drive(5, 3);
expect(held()).toEqual(["false", "false", "false", "true", "true", "true"]);
});

it("shows the RAIN plate as the rain arrives, and only then", () => {
Expand Down Expand Up @@ -505,13 +528,12 @@ describe("the speed cluster on the page", () => {
expect(find("gear")?.dataset.limiter).toBe("false");
});

it("hides the shift lights, the kart icon and the coin icon from assistive technology", () => {
it("hides the shift lights and the kart icon from assistive technology", () => {
const { container, find } = mount();
const lights = container.querySelector("[data-shift-lights]");
expect(lights?.getAttribute("aria-hidden")).toBe("true");
expect(lights?.children).toHaveLength(12);
expect(find("tyre")?.querySelector("svg")?.getAttribute("aria-hidden")).toBe("true");
expect(find("coins")?.querySelector("svg")?.getAttribute("aria-hidden")).toBe("true");
});

it("lights the shift lights with the revs and marks the limiter on the gear so it can flash", () => {
Expand Down
48 changes: 18 additions & 30 deletions apps/web/src/instrument-panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@ import {
} from "./low-poly-svg";
import {
CARBON,
COIN_GOLD,
COIN_RIM,
DRONE_EYE,
FIRING,
GO_GREEN,
Expand All @@ -51,7 +49,6 @@ import {
import { type ItemSlot, type RaceReadout, raceCount } from "./race-run";
import { rainPlateShowing } from "./rain-look";
import {
coinCount,
coinTopSpeed,
drivetrain,
type SpeedClusterTuning,
Expand Down Expand Up @@ -321,7 +318,8 @@ export function createInstrumentPanel(
standingsList.dataset.instrument = "standings";
standingsList.hidden = true;
// The speed cluster (ADR 0031): the gear, speed, coins and tyres stay as
// text; the shift lights, the coin and the kart from above are decorative.
// text; the shift lights, the coin pile's slots and the kart from above
// are decorative.
const cluster = document.createElement("div");
cluster.dataset.cluster = "";
const lightRow = svg(`0 0 ${SHIFT_LIGHT_PITCH * 12} 14`);
Expand Down Expand Up @@ -392,9 +390,18 @@ export function createInstrumentPanel(
stopShownUntil = null;
}
};
// The coin pile (ADR 0105). Its accessible name carries the cap the slots
// draw, so the slots stay out of the accessibility tree.
const coins = instrument("coins");
coins.append(coinIcon());
coins.setAttribute("role", "img");
const coinValue = part(coins, "coinCount");
const coinSlots = part(coins, "coinSlots");
const slots = Array.from({ length: tuning.coinCap }, () => {
const slot = document.createElement("i");
slot.dataset.coinSlot = "";
coinSlots.append(slot);
return slot;
});
cluster.append(coins, gear, speed, tyre, boxPlate, rainPlate, stopPlate);
const showSpeed = (metresPerSecond: number, carried: number, inPitLane: boolean) => {
const topSpeed = coinTopSpeed(tuning, carried);
Expand All @@ -417,7 +424,12 @@ export function createInstrumentPanel(
speedUnit.textContent = parts.unit;
};
const showCoins = (carried: number) => {
coinValue.textContent = coinCount(carried, tuning.coinCap);
coinValue.textContent = String(carried);
coins.setAttribute("aria-label", `${carried} of ${tuning.coinCap} coins`);
// The pile grows upward: the slots run top to bottom, so the last ones hold.
slots.forEach((slot, index) => {
slot.dataset.held = String(index >= slots.length - carried);
});
};
// Every kart starts the race with no coins.
showSpeed(0, 0, false);
Expand Down Expand Up @@ -871,30 +883,6 @@ function tyreFromAbove(parent: Element, x: number, y: number, width: number, hei
}
}

/** The coin's octagon as the mockup draws it, a vertex up, on a 100-unit square. */
const COIN_OCTAGON: readonly Point[] = [
[50, 6],
[81, 18],
[94, 50],
[81, 82],
[50, 94],
[19, 82],
[6, 50],
[19, 18],
];

/**
* The coin for the coin count (ADR 0056): the track's coin face on, a poster
* gold octagon in its rim's darker gold. Decorative: the count is the text.
*/
function coinIcon(): SVGSVGElement {
const coin = svg("0 0 100 100");
const face = polygon(coin, COIN_OCTAGON, COIN_GOLD);
face.setAttribute("stroke", hexCss(COIN_RIM));
face.setAttribute("stroke-width", "8");
return coin;
}

/**
* The own kart from above for the tyre gauge (ADR 0037): wings in
* secondary, tub and sidepods in primary lit on the left, the accent
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/palette.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@ const PROPERTIES: Readonly<Record<string, number | Tones>> = {
label: LABEL_GREY,
carbon: CARBON,
gold: ITEM_BOX_GOLD,
coin: COIN_GOLD,
"coin-rim": COIN_RIM,
firing: FIRING,
white: WHITE,
green: SIGNAL_GREEN,
Expand Down
9 changes: 0 additions & 9 deletions apps/web/src/speed-cluster.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { describe, expect, it } from "vitest";
import type { Tuning } from "./bridge";
import {
coinCount,
coinTopSpeed,
drivetrain,
shiftLights,
Expand Down Expand Up @@ -116,14 +115,6 @@ describe("the shift lights", () => {
});
});

describe("the coin count", () => {
it("reads the coins carried as two digits over the most a kart carries", () => {
expect(coinCount(0, COIN_CAP)).toBe("x00 / 10");
expect(coinCount(7, COIN_CAP)).toBe("x07 / 10");
expect(coinCount(10, COIN_CAP)).toBe("x10 / 10");
});
});

describe("the tyre gauge's colour", () => {
it("is green above 60%, yellow above 30% and red below", () => {
expect(tyreWearBand(100)).toBe("good");
Expand Down
5 changes: 0 additions & 5 deletions apps/web/src/speed-cluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,3 @@ export function coinTopSpeed(tuning: SpeedClusterTuning, coins: number): number
const speeds = tuning.coinTopSpeedsMPerS;
return speeds[Math.min(Math.max(coins, 0), speeds.length - 1)] ?? 0;
}

/** The coin count as the panel prints it: the coins carried in two digits over the cap. */
export function coinCount(coins: number, cap: number): string {
return `x${String(coins).padStart(2, "0")} / ${cap}`;
}
Loading
Loading