Public: anyone can read this; the crew can change it.
forked from trevato/cs
The support line moves behind the door, and the panel becomes the shell's #373
Check · Review · Land · Live done — Landed and live in production.
torque/cs-engine:torque/344-the-support-line-moves-behind-the-door-a → main 1 commit fork
16 files
changed +185
−35
README.md +1 −1
| @@ -503,7 +503,7 @@ the picture a machine whose driver refused the shaders actually gets. | ||
| 503 | 503 | browser and the platform, what the WebGL2 driver calls itself — on Windows the |
| 504 | 504 | ANGLE string naming the Direct3D device — the stage's size, the pixel ratio, |
| 505 | 505 | whether the pointer lock is raw and whether the keyboard lock was granted. |
| 506 | `src/client/support.ts` is a pure function of those facts; every one of them | |
| 506 | `src/engine/support.ts` is a pure function of those facts; every one of them | |
| 507 | 507 | comes from outside the page, so every one is flattened of control characters, |
| 508 | 508 | stripped of the separator and cut to 120 characters, and it reaches the DOM |
| 509 | 509 | through `textContent`. When the picture cannot be built at all — ANGLE on D3D11 |
src/client/main.test.ts +1 −1
| @@ -32,7 +32,7 @@ import { expect, test } from "bun:test"; | ||
| 32 | 32 | import { generateLevel, nameOf, seedFrom, seedOf } from "./generate.ts"; |
| 33 | 33 | import type { Level } from "./level.ts"; |
| 34 | 34 | import { boot, COPIED, gameMeshes, NOT_AN_ID, NOT_A_MAP, NOT_COPIED, type Frame } from "./main.ts"; |
| 35 | import { NO_PICTURE } from "./support.ts"; | |
| 35 | import { NO_PICTURE } from "../engine/index.ts"; | |
| 36 | 36 | import { HINT } from "./buy.ts"; |
| 37 | 37 | import { DE_FORGE } from "./map.ts"; |
| 38 | 38 | import { endRound, SHORT } from "./match.ts"; |
src/client/main.ts +3 −3
| @@ -69,13 +69,13 @@ | ||
| 69 | 69 | import { createAudio, type Audio } from "./audio.ts"; |
| 70 | 70 | import { CS_KEYS, CS_MOUSE, CS_TAP, CS_WHEEL, readIntent, slotIndex } from "./binding.ts"; |
| 71 | 71 | import { drawHud, type Paint } from "./hud.ts"; |
| 72 | import { NO_PICTURE, NO_PICTURE_SAYS, supportLine, type Facts, type Running } from "./support.ts"; | |
| 73 | 72 | import { |
| 74 | 73 | attachButtons, backing, clamp, createControls, createEscape, createFullscreen, createLoop, |
| 75 | 74 | createRenderer, |
| 76 | 75 | effectAt, |
| 77 | 76 | painter, |
| 78 | type Button, type Camera, type KeyboardLock, type Listens, type Mesh, | |
| 77 | NO_PICTURE, NO_PICTURE_SAYS, supportLine, | |
| 78 | type Button, type Camera, type Facts, type KeyboardLock, type Listens, type Mesh, type Running, | |
| 79 | 79 | } from "../engine/index.ts"; |
| 80 | 80 | import { generateLevel, seedFrom } from "./generate.ts"; |
| 81 | 81 | import { mapFrom, mapLink, nameOf, seedOf, type MapChoice } from "../mapname.ts"; |
| @@ -304,7 +304,7 @@ export type Host = { | ||
| 304 | 304 | * What this browser is, for the one line a bug report pastes: the user agent, |
| 305 | 305 | * the name the WebGL2 driver gives itself, the stage's size and the pixel |
| 306 | 306 | * ratio. Gathered by the host because every one of those is a browser's, and |
| 307 | * turned into a sentence by `./support.ts`, which is a pure function. | |
| 307 | * turned into a sentence by the engine's `supportLine`, which is a pure function. | |
| 308 | 308 | */ |
| 309 | 309 | facts(canvas: Stage): Facts; |
| 310 | 310 | createAudio(): Audio; |
src/engine/GUIDE.md +36 −1
| @@ -150,7 +150,7 @@ const SPEED = 4, TURN = 120, EYE = 1.6, REACH = 40; | ||
| 150 | 150 | |
| 151 | 151 | ## What is behind the door |
| 152 | 152 | |
| 153 | **Weight**: 47.4 kB raw, 18.9 kB gzipped — every group at once, which is the most any game can pay. | |
| 153 | **Weight**: 50.1 kB raw, 20.1 kB gzipped — every group at once, which is the most any game can pay. | |
| 154 | 154 | |
| 155 | 155 | Every section below opens with what that group costs a bundle, because "what |
| 156 | 156 | does it cost to use?" is a question a page can answer with a number. The number |
| @@ -892,6 +892,40 @@ animates itself: a bob, a fade or a tint is a decision, and decisions are the | ||
| 892 | 892 | game's. One line a frame draws it, and `src/client/main.ts` and |
| 893 | 893 | `src/range/main.ts` are where those two lines live. |
| 894 | 894 | |
| 895 | ### support | |
| 896 | ||
| 897 | **Weight**: 2.0 kB raw, 1.1 kB gzipped — this module alone. | |
| 898 | ||
| 899 | One line of what this browser is doing, for the person who is about to say your | |
| 900 | game is buggy. "Buggy on Windows in Edge" is true and unanswerable: a refused | |
| 901 | shader, a driver that paints nothing, a pointer that was never granted and a | |
| 902 | display at 125% all read the same from outside. `supportLine` is the sentence | |
| 903 | that tells them apart, and it is a pure function of plain data your entry | |
| 904 | gathers when somebody opens the panel. | |
| 905 | ||
| 906 | `Facts` is the browser's half: `Facts.userAgent`, `Facts.renderer` — what the | |
| 907 | WebGL2 driver calls itself, or null where the browser will not say — the stage's | |
| 908 | size in CSS pixels as `Facts.stage`, `Facts.dpr`, and `Facts.commit`, the build | |
| 909 | the page was served from. `Running` is the game's half, and every member of it | |
| 910 | is optional because every one of them is some game's and not another's: | |
| 911 | `Running.pointer` is a `PointerMode` or null, `Running.keys` whether Escape was | |
| 912 | handed over, `Running.fullscreen`, `Running.hz`, `Running.frameMs`, and | |
| 913 | `Running.failure`, which is what the boot threw. A member you leave out is left | |
| 914 | out of the line rather than guessed at — a game with no pointer does not claim | |
| 915 | one — and a game with a word of its own to add writes it after the line. | |
| 916 | ||
| 917 | Every piece comes from outside the page, so every piece is trimmed, stripped of | |
| 918 | control characters and of the separator, and cut to 120 characters before it is | |
| 919 | joined. Write it into the page as text, never as markup. `NO_PICTURE` and | |
| 920 | `NO_PICTURE_SAYS` are the heading and the paragraph for the menu of a game whose | |
| 921 | picture could not be built, with the line underneath them. | |
| 922 | ||
| 923 | The panel the line goes in is not the engine's — the engine renders nothing — | |
| 924 | but it is not one game's either: `src/pages/shell.ts` writes it, closed, at the | |
| 925 | foot of whichever overlay asks with `support: true`, with the ids `#support`, | |
| 926 | `#supportline` and `#copysupport`. The shooter's menu asks; `src/client/main.ts` | |
| 927 | fills the line on the panel's toggle event and copies it on the button. | |
| 928 | ||
| 895 | 929 | ### page |
| 896 | 930 | |
| 897 | 931 | **Weight**: 3.6 kB raw, 1.8 kB gzipped — this module alone. |
| @@ -1163,6 +1197,7 @@ rows, an entry aside, and issue #130 is where it gets one. | ||
| 1163 | 1197 | | what casts a contact shadow | every living player, the bomb and every grenade, traced against the level's boxes — 24 of them, reaching 96 units | every plate, from its feet, traced against the room — one per instance the pool holds, reaching 128 units | |
| 1164 | 1198 | | a pinned run | `src/client/replay.ts` — a script of held inputs, a round and a duel | `src/range/pin.ts` — a script of trigger pulls, one digest per drill | |
| 1165 | 1199 | | no WebGL2 | a sentence in the menu, and nothing starts | the 2D horizon in `src/range/readout.ts`, and it still plays | |
| 1200 | | a support line | `Facts` and `Running` gathered in `src/client/main.ts`, into the panel the shell renders in the menu | none yet — the range asks the shell for no panel | | |
| 1166 | 1201 | | the mover's tuning and hull | `movement.CS_MOVE` and `movement.CS_HULL` in `src/client/movement.ts` | `movement.RANGE_MOVE` and `movement.RANGE_HULL` in `src/range/movement.ts` — its own numbers, the same solver | |
| 1167 | 1202 | | the rest of the tuning | `src/client/weapons.ts`, and `movement.fallDamage` | its own: `targets.STANDARD`, `targets.EYE` and the drills beside them in `src/range/targets.ts` | |
| 1168 | 1203 | | a policy's observation, heads and weights | `src/client/observe.ts`, `src/client/action.ts`, `src/client/weights.ts` | none — the range has no agent and bundles none of the net | |
src/engine/boundary.test.ts +3
| @@ -294,6 +294,9 @@ test("the example imports the door and nothing else, which is what it is for", ( | ||
| 294 | 294 | * effects — `Effects`, `createEffects`, `pushEffect`, `expireEffects`, |
| 295 | 295 | * `effectAt`, `resetEffects`. |
| 296 | 296 | * |
| 297 | * support — `Facts`, `Running`, `supportLine`, `NO_PICTURE`, | |
| 298 | * `NO_PICTURE_SAYS`: moved from the shooter by #344, not written anew. | |
| 299 | * | |
| 297 | 300 | * The list itself is `surface.json` beside this file — data, sorted, one name |
| 298 | 301 | * to a line, so a name that moves is a line in a diff — and it is committed |
| 299 | 302 | * with the *version* that surface belongs to, which is the version |
src/engine/index.ts +12
| @@ -136,6 +136,18 @@ export type { Member, MemberCodec, MemberOptions, MemberSocket, Sentences } from | ||
| 136 | 136 | export { createRenderer } from "./renderer.ts"; |
| 137 | 137 | export type { Renderer, RendererAssets } from "./renderer.ts"; |
| 138 | 138 | |
| 139 | /** | |
| 140 | * One line of what this browser is doing, for a bug report: the browser, the | |
| 141 | * driver behind the WebGL2 context, the stage's size and pixel ratio, then what | |
| 142 | * the game says it is doing — each piece trimmed, stripped and cut short, since | |
| 143 | * all of it comes from outside the page. A pure function of plain `Facts` and an | |
| 144 | * optional `Running`, every member of which is optional and none of which names | |
| 145 | * a game's concept; and the heading and paragraph a menu shows when the picture | |
| 146 | * could not be built at all. The panel it is written into is the shell's. | |
| 147 | */ | |
| 148 | export { NO_PICTURE, NO_PICTURE_SAYS, supportLine } from "./support.ts"; | |
| 149 | export type { Facts, Running } from "./support.ts"; | |
| 150 | ||
| 139 | 151 | /** |
| 140 | 152 | * A headless page: the host the parameters above already describe, made of |
| 141 | 153 | * plain objects. A package that asks a game for a `surface`, a `window` and a |
src/client/support.test.ts → src/engine/support.test.ts renamed
No content changes.
src/client/support.ts → src/engine/support.ts renamed +14 −9
| @@ -12,8 +12,11 @@ | ||
| 12 | 12 | * a frame costs and the build it is all running on — short enough to paste into |
| 13 | 13 | * an issue and boring enough that pasting one costs nothing. |
| 14 | 14 | * |
| 15 | * It is a pure function of plain facts: `main.ts` gathers them from the | |
| 16 | * browser, this builds the sentence, and a test holds the sentence. Every piece | |
| 15 | * It is a pure function of plain facts: a game's entry gathers them from the | |
| 16 | * browser — the shooter's is `src/client/main.ts` — this builds the sentence, | |
| 17 | * and a test holds the sentence. It names no weapon, no round and no map, which | |
| 18 | * is why it is the engine's: the panel it fills is the shell's, `src/pages/shell.ts`, | |
| 19 | * and any game can ask for one. Every piece | |
| 17 | 20 | * of it comes from outside the page — a driver's own string, a user agent — so |
| 18 | 21 | * every piece is trimmed, stripped of control characters and cut to a length |
| 19 | 22 | * before it goes anywhere near the DOM. |
| @@ -52,9 +55,9 @@ export type Facts = { | ||
| 52 | 55 | export type Running = { |
| 53 | 56 | /** |
| 54 | 57 | * How the locked pointer is sending counts — the engine's `PointerMode` — or |
| 55 | * null while the game has no pointer. This is the field that separates "the | |
| 56 | * mouse feels wrong on Windows" from "Play did nothing": `adjusted` is the | |
| 57 | * first, `null` after pressing Play is the second. | |
| 58 | * null while the game has no pointer. This is the field that tells two reports | |
| 59 | * apart — "the mouse feels wrong on Windows" and "Play did nothing": `adjusted` | |
| 60 | * is the first, `null` after pressing Play is the second. | |
| 58 | 61 | */ |
| 59 | 62 | pointer?: "raw" | "adjusted" | "unknown" | null; |
| 60 | 63 | /** |
| @@ -167,9 +170,11 @@ export const NO_PICTURE = "The picture could not be built"; | ||
| 167 | 170 | /** |
| 168 | 171 | * And the paragraph under that heading: what happened, what still works, and |
| 169 | 172 | * what to do with the line below it. Text, escaped where it is rendered. |
| 173 | * | |
| 174 | * One literal, on one long line, because this is the engine now: a sum of | |
| 175 | * literals at module scope is an expression the minifier keeps, and every | |
| 176 | * bundle through the door — the range's, which says none of this — would carry | |
| 177 | * it. `src/range/bundle.test.ts` reads that bundle to say so. | |
| 170 | 178 | */ |
| 171 | 179 | export const NO_PICTURE_SAYS = |
| 172 | "This browser has WebGL2 and then refused the game’s shaders — which is what a driver " + | |
| 173 | "does when it is asked for something it will not compile, and is far more common on Windows, " + | |
| 174 | "where the browser draws through Direct3D. Nothing else on the page is affected. The line " + | |
| 175 | "below says which browser and which driver: paste it into a bug report."; | |
| 180 | "This browser has WebGL2 and then refused the game’s shaders — which is what a driver does when it is asked for something it will not compile, and is far more common on Windows, where the browser draws through Direct3D. Nothing else on the page is affected. The line below says which browser and which driver: paste it into a bug report."; |
src/engine/surface.json +6 −1
| @@ -1,5 +1,5 @@ | ||
| 1 | 1 | { |
| 2 | "version": "0.10.0", | |
| 2 | "version": "0.11.0", | |
| 3 | 3 | "names": [ |
| 4 | 4 | "Activations", |
| 5 | 5 | "Adam", |
| @@ -23,6 +23,7 @@ | ||
| 23 | 23 | "Effects", |
| 24 | 24 | "Escape", |
| 25 | 25 | "EscapeHost", |
| 26 | "Facts", | |
| 26 | 27 | "FakeAudioNode", |
| 27 | 28 | "FakeCanvas", |
| 28 | 29 | "FakeDocument", |
| @@ -66,6 +67,8 @@ | ||
| 66 | 67 | "Mesh", |
| 67 | 68 | "MouseEventish", |
| 68 | 69 | "MoveTuning", |
| 70 | "NO_PICTURE", | |
| 71 | "NO_PICTURE_SAYS", | |
| 69 | 72 | "Options", |
| 70 | 73 | "Overlay", |
| 71 | 74 | "Packed", |
| @@ -80,6 +83,7 @@ | ||
| 80 | 83 | "RendererAssets", |
| 81 | 84 | "Rollout", |
| 82 | 85 | "RoomGame", |
| 86 | "Running", | |
| 83 | 87 | "Scene", |
| 84 | 88 | "SceneCapacity", |
| 85 | 89 | "Screen", |
| @@ -199,6 +203,7 @@ | ||
| 199 | 203 | "softmax", |
| 200 | 204 | "stuck", |
| 201 | 205 | "sub", |
| 206 | "supportLine", | |
| 202 | 207 | "tensorsOf", |
| 203 | 208 | "traceWorld", |
| 204 | 209 | "unpack", |
src/example/bundle.test.ts +13
| @@ -17,6 +17,7 @@ | ||
| 17 | 17 | |
| 18 | 18 | import { afterAll, expect, test } from "bun:test"; |
| 19 | 19 | import { readFileSync } from "node:fs"; |
| 20 | import { NO_PICTURE, NO_PICTURE_SAYS } from "../engine/index.ts"; | |
| 20 | 21 | import { bundlePath, discardBundleCopies, importBundle } from "../../scripts/bundle.test-helper.ts"; |
| 21 | 22 | import { page, type ExamplePage } from "./stage.test-helper.ts"; |
| 22 | 23 | |
| @@ -107,6 +108,18 @@ test("the example bundles none of the engine it does not use", () => { | ||
| 107 | 108 | expect(code).not.toContain(name); |
| 108 | 109 | }); |
| 109 | 110 | |
| 111 | test("the example asks for no support line, so the door's support group costs it nothing", () => { | |
| 112 | // The shooter ships `supportLine` and the sentences beside it; the example imports | |
| 113 | // the same door and none of those names, so not one byte of them may come | |
| 114 | // with it. The paragraph is the one to watch: written as a sum of literals at | |
| 115 | // module scope it is an expression the minifier keeps, and every bundle | |
| 116 | // through the door paid its four hundred bytes for nothing. | |
| 117 | const code = readFileSync(bundlePath("example"), "utf8"); | |
| 118 | expect(code).toContain("webgl2"); // the reader is alive | |
| 119 | for (const text of [NO_PICTURE, NO_PICTURE_SAYS.slice(0, 40), "driver not named"]) | |
| 120 | expect(code).not.toContain(text); | |
| 121 | }); | |
| 122 | ||
| 110 | 123 | test("the stub page leaves nothing of itself behind", () => { |
| 111 | 124 | // Every boot above undid its install, so no other test inherits a document. |
| 112 | 125 | expect("document" in globalThis).toBe(HAD_DOCUMENT); |
src/pages/game.test.ts +14 −1
| @@ -36,7 +36,7 @@ import { GAMES } from "../games.ts"; | ||
| 36 | 36 | import { digitsIn, proseOf, unsourced } from "./digits.test-helper.ts"; |
| 37 | 37 | import { gamePage, policyHtml, shareHref, statsHtml, STYLE, BAD_ID, BAD_MAP, type PageOptions } from "./game.ts"; |
| 38 | 38 | import { WEAPONS_HREF, WEAPONS_SECTION, WEAPONS_SUMMARY } from "./weapons.ts"; |
| 39 | import { escape } from "./shell.ts"; | |
| 39 | import { escape, SUPPORT } from "./shell.ts"; | |
| 40 | 40 | |
| 41 | 41 | const db = (): ReturnType<typeof openDatabase> => openDatabase(mkdtempSync(join(tmpdir(), "game-"))); |
| 42 | 42 | |
| @@ -91,6 +91,19 @@ test("the leaderboard and the menu are in the first paint, not fetched", () => { | ||
| 91 | 91 | expect(html).toContain(`<button type="button" class="primary" id="play" disabled>Play</button>`); |
| 92 | 92 | }); |
| 93 | 93 | |
| 94 | test("the menu asks the shell for the support panel, and it lands inside the menu", () => { | |
| 95 | const html = gamePage(db(), options); | |
| 96 | const menuAt = html.indexOf(`<div class="overlay" id="menu">`); | |
| 97 | const pauseAt = html.indexOf(`<div class="overlay" id="pause"`); | |
| 98 | // The same ids `src/client/main.ts` finds, once each, between the menu and the pause. | |
| 99 | for (const id of ["support", "supportline", "copysupport"]) { | |
| 100 | expect(html.split(`id="${id}"`).length - 1).toBe(1); | |
| 101 | expect(html.indexOf(`id="${id}"`)).toBeGreaterThan(menuAt); | |
| 102 | expect(html.indexOf(`id="${id}"`)).toBeLessThan(pauseAt); | |
| 103 | } | |
| 104 | expect(html).toContain(SUPPORT); | |
| 105 | }); | |
| 106 | ||
| 94 | 107 | test("the menu has a Motion control, its first option the game's default for the client to move", () => { |
| 95 | 108 | // No `selected`: a select picks its first option, and the page's one |
| 96 | 109 | // `selected` word stays the map's. |
src/pages/game.ts +2 −12
| @@ -410,17 +410,7 @@ const menu = (visitor: Who, asked: Asked, daily: Daily): string => ` | ||
| 410 | 410 | <p class="hint">Every weapon's numbers — what it costs, what it fires and what one shot of it |
| 411 | 411 | does to a person — are on <a href="${WEAPONS_HREF}">the weapons page</a>, rendered from the |
| 412 | 412 | table the fight is tuned by. It is a page of its own so it is a link you can send.</p> |
| 413 | <details id="support"> | |
| 414 | <summary>What this browser is doing</summary> | |
| 415 | <p class="hint">One line for a bug report: the browser, the graphics driver behind the | |
| 416 | picture and the size it is drawn at. "Buggy on Windows" and this line are two different | |
| 417 | reports, and only one of them can be answered. Nothing is sent anywhere — it is text on | |
| 418 | this page until you copy it.</p> | |
| 419 | <p class="line" id="supportline">This line is filled in by the game, so a browser with | |
| 420 | scripts off has nothing to paste.</p> | |
| 421 | <button type="button" id="copysupport" hidden>Copy this line</button> | |
| 422 | </details> | |
| 423 | `; | |
| 413 | `; | |
| 424 | 414 | |
| 425 | 415 | /** |
| 426 | 416 | * What the page says about the bots' policy — the only paragraph on it made of |
| @@ -575,7 +565,7 @@ export function gamePage( | ||
| 575 | 565 | ], |
| 576 | 566 | ], |
| 577 | 567 | overlays: [ |
| 578 | { id: "menu", html: menu(visitor, asked, daily) }, | |
| 568 | { id: "menu", html: menu(visitor, asked, daily), support: true }, | |
| 579 | 569 | { id: "pause", html: PAUSE, hidden: true }, |
| 580 | 570 | // After the pause, so a match that ends while it is up is read over it |
| 581 | 571 | // rather than under it. `src/client/main.ts` hides the pause anyway. |
src/pages/shell.test.ts +31 −1
| @@ -6,7 +6,8 @@ | ||
| 6 | 6 | */ |
| 7 | 7 | |
| 8 | 8 | import { expect, test } from "bun:test"; |
| 9 | import { escape, nav, noscript, page, stage, styleFor, terminal, where } from "./shell.ts"; | |
| 9 | import { escape, nav, noscript, page, stage, styleFor, SUPPORT, terminal, where } from "./shell.ts"; | |
| 10 | import { errorPage, notFoundPage } from "./miss.ts"; | |
| 10 | 11 | import { GAMES as REGISTRY } from "../games.ts"; |
| 11 | 12 | import { GAMES, navFor, noscriptFor } from "./games.ts"; |
| 12 | 13 | |
| @@ -296,6 +297,35 @@ test("the stage carries the full screen button, hidden, over everything else", ( | ||
| 296 | 297 | expect(document).toContain("aspect-ratio:auto"); |
| 297 | 298 | }); |
| 298 | 299 | |
| 300 | test("an overlay that asks for the support panel gets it, with the ids a game finds it by", () => { | |
| 301 | const html = stage({ | |
| 302 | touch: [], | |
| 303 | overlays: [{ id: "menu", html: "<h2>go</h2>", support: true }, { id: "pause", html: "", hidden: true }], | |
| 304 | }); | |
| 305 | // At the foot of the overlay that asked, and only that one. | |
| 306 | expect(html).toContain(`<div class="overlay" id="menu"><h2>go</h2>${SUPPORT}</div>`); | |
| 307 | expect(html).toContain(`<div class="overlay" id="pause" hidden></div>`); | |
| 308 | expect(html.match(/<details/g)?.length).toBe(1); | |
| 309 | // The three ids are the contract: the toggle, the line and the button. | |
| 310 | expect(html).toContain(`<details id="support">`); | |
| 311 | expect(html).toContain(`<p class="line" id="supportline">`); | |
| 312 | expect(html).toContain(`<button type="button" id="copysupport" hidden>Copy this line</button>`); | |
| 313 | // Closed in the first paint, and the button hidden until a script can copy. | |
| 314 | expect(html).not.toContain(`<details id="support" open`); | |
| 315 | }); | |
| 316 | ||
| 317 | test("a page that asks for no support panel gets no element of it", () => { | |
| 318 | const html = stage({ touch: [], overlays: [{ id: "menu", html: "<h2>go</h2>" }] }); | |
| 319 | for (const id of ["support", "supportline", "copysupport"]) expect(html).not.toContain(`id="${id}"`); | |
| 320 | expect(html).not.toContain("<details"); | |
| 321 | // And the pages that run nothing ask for none: no line to fill, no panel. | |
| 322 | const miss = { env: "production", pull: undefined, commit: "abc", ssh: undefined }; | |
| 323 | for (const document of [notFoundPage(miss), errorPage(miss)]) { | |
| 324 | expect(document).not.toContain(`id="support`); | |
| 325 | expect(document).not.toContain("copysupport"); | |
| 326 | } | |
| 327 | }); | |
| 328 | ||
| 299 | 329 | test("a touch label and an action are text, not markup", () => { |
| 300 | 330 | const html = stage({ touch: [[{ action: XSS, label: XSS }]], overlays: [{ id: XSS, html: "" }] }); |
| 301 | 331 | expect(html).not.toContain("<script>"); |
src/pages/shell.ts +35 −4
| @@ -184,8 +184,38 @@ export const endpoints = (posts: readonly Linked[] = []): string => | ||
| 184 | 184 | /** One touch button: the action `src/engine/controls.ts` holds, and its label. */ |
| 185 | 185 | export type TouchButton = { action: string; label: string }; |
| 186 | 186 | |
| 187 | /** One overlay over the stage — a menu, a pause screen — by the id the client finds it with. */ | |
| 188 | export type Overlay = { id: string; html: string; hidden?: boolean }; | |
| 187 | /** | |
| 188 | * One overlay over the stage — a menu, a pause screen — by the id the client | |
| 189 | * finds it with. `support` asks for the support panel at its foot: see `SUPPORT`. | |
| 190 | */ | |
| 191 | export type Overlay = { id: string; html: string; hidden?: boolean; support?: boolean }; | |
| 192 | ||
| 193 | /** | |
| 194 | * The support panel: a closed `<details>` a person opens to get one line for a | |
| 195 | * bug report, and the button that copies it. The line itself is the engine's | |
| 196 | * `supportLine`, written in by a game's entry through `textContent` — so what | |
| 197 | * is rendered here is only what is true before a script has run, and it is the | |
| 198 | * shell's because nothing in it names a game. The ids are the contract a game's | |
| 199 | * entry finds it by: `support` for the toggle, `supportline` for the line and | |
| 200 | * `copysupport` for the button, which is `hidden` because it does nothing | |
| 201 | * without a script. | |
| 202 | * | |
| 203 | * A page asks for it on the overlay it wants it in, with `support: true`, and a | |
| 204 | * page that does not ask gets no element — the 404 and the 500 run nothing, so | |
| 205 | * they have no line to fill and no panel to show. Constant markup: there is no | |
| 206 | * text in it from anywhere but this file. | |
| 207 | */ | |
| 208 | export const SUPPORT = ` <details id="support"> | |
| 209 | <summary>What this browser is doing</summary> | |
| 210 | <p class="hint">One line for a bug report: the browser, the graphics driver behind the | |
| 211 | picture and the size it is drawn at. "Buggy on Windows" and this line are two different | |
| 212 | reports, and only one of them can be answered. Nothing is sent anywhere — it is text on | |
| 213 | this page until you copy it.</p> | |
| 214 | <p class="line" id="supportline">This line is filled in by the game, so a browser with | |
| 215 | scripts off has nothing to paste.</p> | |
| 216 | <button type="button" id="copysupport" hidden>Copy this line</button> | |
| 217 | </details> | |
| 218 | `; | |
| 189 | 219 | |
| 190 | 220 | export type StageOptions = { |
| 191 | 221 | overlays: Overlay[]; |
| @@ -197,7 +227,8 @@ export type StageOptions = { | ||
| 197 | 227 | * The stage: the two canvases the renderer draws into, the fps readout, the |
| 198 | 228 | * touch pads, the overlays and the full screen button. Every id in here is |
| 199 | 229 | * found by `getElementById` in a game's entry — `stage`, `scene`, `hud`, `fps`, |
| 200 | * `touch`, `full` and each overlay's — so they are the contract, not decoration. | |
| 230 | * `touch`, `full`, each overlay's, and the support panel's where an overlay | |
| 231 | * asked for it — so they are the contract, not decoration. | |
| 201 | 232 | * |
| 202 | 233 | * `#full` is rendered last, so it paints over the overlays rather than under |
| 203 | 234 | * them, and `hidden`, because it is the one control on this page that does |
| @@ -220,7 +251,7 @@ ${pad.map((b) => ` <button type="button" data-action="${escape(b.action)} | ||
| 220 | 251 | <div id="touch" hidden> |
| 221 | 252 | ${pads} |
| 222 | 253 | </div> |
| 223 | ${overlays.map((o) => ` <div class="overlay" id="${escape(o.id)}"${o.hidden === true ? " hidden" : ""}>${o.html}</div>`).join("\n")} | |
| 254 | ${overlays.map((o) => ` <div class="overlay" id="${escape(o.id)}"${o.hidden === true ? " hidden" : ""}>${o.html}${o.support === true ? SUPPORT : ""}</div>`).join("\n")} | |
| 224 | 255 | <button type="button" id="full" hidden aria-pressed="false" aria-label="Full screen" title="Full screen">⛶</button> |
| 225 | 256 | </div>`; |
| 226 | 257 | } |
src/range/bundle.test.ts +13
| @@ -16,6 +16,7 @@ | ||
| 16 | 16 | |
| 17 | 17 | import { afterAll, expect, test } from "bun:test"; |
| 18 | 18 | import { readFileSync } from "node:fs"; |
| 19 | import { NO_PICTURE, NO_PICTURE_SAYS } from "../engine/index.ts"; | |
| 19 | 20 | import { bundlePath, discardBundleCopies, importBundle } from "../../scripts/bundle.test-helper.ts"; |
| 20 | 21 | import { RANGE_MOVE } from "./movement.ts"; |
| 21 | 22 | import type { GlRecorder } from "../engine/index.ts"; |
| @@ -152,6 +153,18 @@ test("the range bundles the engine's mover, on the range's own numbers", () => { | ||
| 152 | 153 | expect(code).not.toContain("maxSpeed:250"); |
| 153 | 154 | }); |
| 154 | 155 | |
| 156 | test("the range asks for no support line, so the door's support group costs it nothing", () => { | |
| 157 | // The shooter ships `supportLine` and the sentences beside it; the range imports | |
| 158 | // the same door and none of those names, so not one byte of them may come | |
| 159 | // with it. The paragraph is the one to watch: written as a sum of literals at | |
| 160 | // module scope it is an expression the minifier keeps, and every bundle | |
| 161 | // through the door paid its four hundred bytes for nothing. | |
| 162 | const code = readFileSync(bundlePath("range"), "utf8"); | |
| 163 | expect(code).toContain("webgl2"); // the reader is alive | |
| 164 | for (const text of [NO_PICTURE, NO_PICTURE_SAYS.slice(0, 40), "driver not named"]) | |
| 165 | expect(code).not.toContain(text); | |
| 166 | }); | |
| 167 | ||
| 155 | 168 | test("the stub page leaves nothing of itself behind", () => { |
| 156 | 169 | // Every boot above undid its install, so no other test inherits a document. |
| 157 | 170 | expect("document" in globalThis).toBe(HAD_DOCUMENT); |
src/range/readout.test.ts +1 −1
| @@ -145,7 +145,7 @@ test("the horizon follows the pitch and the compass follows the yaw", () => { | ||
| 145 | 145 | |
| 146 | 146 | test("the fallback scene is the same size on the glass at every device ratio", () => { |
| 147 | 147 | // This is the picture a machine whose driver refused the shaders gets — see |
| 148 | // `client/support.ts` — so at 150% its lines cannot be two thirds of a pixel | |
| 148 | // `engine/support.ts` — so at 150% its lines cannot be two thirds of a pixel | |
| 149 | 149 | // and its compass numbers cannot be eight pixels tall. |
| 150 | 150 | const tickHeight = (paint: ReturnType<typeof recorder>): number => { |
| 151 | 151 | // The compass's first tick: the second moveTo/lineTo pair, after the horizon. |
new above
offline