diff --git a/web/scripts/hangar.test.mjs b/web/scripts/hangar.test.mjs index efe9be5..a665879 100644 --- a/web/scripts/hangar.test.mjs +++ b/web/scripts/hangar.test.mjs @@ -23,6 +23,7 @@ import { withinTarget, } from "../src/force.ts"; import { + FILTER_KEYS, matchesFilter, parseQuery, search, @@ -444,6 +445,31 @@ test("the filter reference says what is not supported yet, and marks it", async assert.equal(parseQuery('equipment:"ER PPC"').filters.length, 0); }); +test("the reference explains every key the box accepts", async () => { + // The list under Filters is the only place a key's meaning is written down, + // so a key the parser takes and the dialog omits is a key nobody can learn. + const hangar = await readFile(`${web}src/screens/hangar.ts`, "utf8"); + const documented = new Set( + [...hangar.matchAll(/filter\(\s*"([^"]+)"/g)].flatMap((m) => + m[1].split(",").map((key) => key.trim()), + ), + ); + for (const key of FILTER_KEYS) { + assert.ok(documented.has(key), `${key} is accepted but never explained`); + } + // And each row carries a term a player can paste into the box. + for (const [, doubled, singled] of hangar.matchAll( + /filter\(\s*"(?:[^"\\]|\\.)*"\s*,\s*"(?:[^"\\]|\\.)*"\s*,\s*(?:"((?:[^"\\]|\\.)*)"|'([^']*)')\s*,?\s*\)/g, + )) { + const query = doubled ?? singled; + assert.equal( + parseQuery(query).filters.length, + 1, + `${query} is not a term the parser takes`, + ); + } +}); + test("the reference is a dialog rather than a caption", async () => { const hangar = await readFile(`${web}src/screens/hangar.ts`, "utf8"); assert.match(hangar, /el\("dialog", \{ className: "hangar-help" \}/); diff --git a/web/src/screens/hangar.ts b/web/src/screens/hangar.ts index 5b6080f..d4fa2af 100644 --- a/web/src/screens/hangar.ts +++ b/web/src/screens/hangar.ts @@ -71,6 +71,20 @@ function example(query: string, means: string): HTMLElement { ]); } +/** + * One key in the reference: what it names, what it means, and a term using it. + * + * Aliases share a row, because they are the same column and a reader who + * learns `mass` should not have to meet `tonnage` as news further down. + */ +function filter(keys: string, means: string, eg: string): HTMLElement { + return el("li", {}, [ + el("code", { className: "hangar-key", textContent: keys }), + el("span", { className: "hint", textContent: ` ${means}` }), + el("code", { className: "hangar-key-eg", textContent: eg }), + ]); +} + /** One question the library cannot answer yet, and what it would look like. */ function soon(key: string, query: string, means: string): HTMLElement { return el("li", {}, [ @@ -182,29 +196,81 @@ export function hangarScreen(): Node[] { el("h2", { textContent: "Searching the library" }), el("p", { textContent: - "Bare words match a design's name. A term filters a column: key, then one of : = < <= > >=, then a value. Everything combines with and.", + "Bare words match a design's name. A term filters one column: a key, then one of : = < <= > >=, then a value. Terms combine with and.", }), el("ul", { className: "hangar-help-examples" }, [ - example("atlas", "every Atlas"), + example( + "atlas", + "39 designs: the Atlas, the Atlas II and the Atlas III", + ), example( "hawk", - "193 designs: Phoenix Hawk, Black Hawk, Battle Hawk and 23 other chassis", + "193, because a bare word reads the model as well as the chassis", ), example( 'chassis:"black hawk"', - "33: the Black Hawks and nothing else. Quoted, because the name has a space in it", + "33: the Black Hawks alone, quoted because the name has a space in it", ), - example("bv<=1500", "nothing dearer than 1,500 battle value"), example( - "era<=3025 tech:inner", - "Inner Sphere designs of 3025 or earlier", + "class:heavy jump>0 era<=3025", + "heavies that jump, introduced by 3025", ), - example("class:heavy jump>0", "heavies that jump"), - example("role:brawler mass>=70", "brawlers of 70 tons and up"), ]), - el("h3", { textContent: "Every key" }), - el("p", { className: "hangar-help-keys" }, [ - el("code", { textContent: FILTER_KEYS.join(", ") }), + el("h3", { textContent: "Filters" }), + el("ul", { className: "hangar-help-filters" }, [ + filter( + "name", + "chassis and model together, the way a bare word reads them.", + "name:atlas", + ), + filter( + "chassis", + "the chassis on its own, without the variant.", + 'chassis:"shadow hawk"', + ), + filter("model", "the variant code.", "model:as7-d"), + filter( + "class", + "weight class by name: light, medium, heavy, assault.", + "class:assault", + ), + filter("tech", "inner, clan or mixed.", "tech:clan"), + filter( + "role", + "the role MegaMek assigns: brawler, striker, sniper, scout, skirmisher, juggernaut, missile boat.", + "role:sniper", + ), + filter( + "type", + "the kind of unit. Everything in the library is a Mek today.", + "type:mek", + ), + filter("bv", "battle value.", "bv<=1500"), + filter("cost", "the price in C-bills.", "cost<5000000"), + filter("mass, tons, tonnage", "tonnage.", "mass>=70"), + filter("era, year", "the year the design was introduced.", "era<=3025"), + filter( + "armor, armour", + "armour points over the whole machine.", + "armor>=250", + ), + filter("walk", "walking movement points.", "walk>=5"), + filter("jump", "jumping movement points.", "jump>0"), + filter( + "fp, firepower", + "average damage with every weapon fired once. Physical weapons are not counted.", + "fp>=40", + ), + filter( + "reach", + "the longest range any weapon carried reaches, in hexes.", + "reach>=18", + ), + filter( + "heat", + "heat shed in a turn less the heat of firing everything, so a negative number cannot alpha strike twice.", + "heat>=0", + ), ]), el("h3", { textContent: "Not yet" }), el("ul", { className: "hangar-help-soon" }, [ diff --git a/web/src/styles.css b/web/src/styles.css index f7aa2fd..9d87bb3 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -6289,6 +6289,7 @@ footer .debug { } .hangar-help-examples, +.hangar-help-filters, .hangar-help-soon { display: flex; flex-direction: column; @@ -6300,6 +6301,7 @@ footer .debug { } .hangar-help-examples code, +.hangar-help-filters code, .hangar-help-soon code, .hangar-help-keys code { font-family: var(--font-data); @@ -6310,6 +6312,31 @@ footer .debug { border-radius: 3px; } +/* The reference reads down the keys, so they line up and the example sits at + the end of the line rather than in the middle of the sentence. */ +.hangar-help-filters li { + display: flex; + flex-wrap: wrap; + align-items: baseline; + gap: 0.35rem; +} + +.hangar-help-filters .hangar-key { + flex: 0 0 auto; + min-width: 9rem; + color: var(--ink); +} + +.hangar-help-filters .hint { + flex: 1 1 12rem; +} + +.hangar-help-filters .hangar-key-eg { + flex: 0 0 auto; + margin-left: auto; + color: var(--muted); +} + .hangar-help-keys code { display: block; padding: 0.4rem 0.5rem;