**Final report — phase 07 final verification pass** - **Verified:** all 5 tasks present in `complete/`; app boots (`/healthz` 200, `/` 200, static + atlas + `/api/levels` served); `docs/CLIENT.md` module map matches the on-disk `app/static/` tree; no code defects found. - **Tests/lint/coverage:** `bash .agents/validate.sh` → ruff clean, pyright 0 errors, `undercooked-levels validate` pass, **full pytest 1838 passed, 97.21% coverage (floor 90%)**, slow sim stage 2 passed, repo hygiene clean. `uv run pytest tests/e2e -q` → **53 passed** (all 8 suites added this phase). - **Criteria:** (1) two tabs create→join→ready→start→serve one dish, keyboard only — green via `test_us01/02/03/04` ✔; (2) e2e green ✔; (3) canvas non-blank, pixels change after a move — `test_us04_render_smoke` ✔; (4) HUD score within 200 ms of the `served` event — asserted, green ✔; (5) zero console errors + zero off-origin requests — `test_atlas_loads` / `test_us01_landing_surface` ✔; (6) `docs/CLIENT.md` matches layout ✔; (7) 100% suite + 97.21% ✔; (8) docs pass below ✔; (9) no regression in completed phases (all prior suites green) ✔. - **README.md fixed:** status still said "next pending phase is 07" → now 07 complete, 08 pending; the phase-07 client text buried in the phase-05 bullet → its own "canvas client, art, HUD (phase 07)" bullet; stale "What does not exist yet" (claimed movement/controls missing) → rewritten to phases 08–14; "once the room flow ships" → "live in the browser today"; Layout `app/static/` "renderer arrives with the rest of phase 07" → full shipped-client map; Layout `docs/` "CLIENT lands with phase 07" → `CLIENT.md` listed; added `gen_art_index.py` Layout entry. - **AGENTS.md fixed:** "the eight screens" (7 listed) → "the seven screens" (+ `dom.js` is the shared builder, not a screen); e2e naming line → notes the non-story guard `test_atlas_loads.py`. - **Decisions:** no code changes were needed; only the two doc fixes above. Docs re-verified by `test_docs_commands`/`test_docs_index` (44 passed) plus the final full gate. - **Next pending phase:** `08_client_prediction_and_touch`.
119 lines
3.5 KiB
JavaScript
119 lines
3.5 KiB
JavaScript
// Undercooked — the help overlay (phase 07, task 05; the task's
|
|
// `app/static/ui/help.js`, served from `js/ui/` like the HUD).
|
|
//
|
|
// The `?`-key in-game help: a body-level, fixed, dismissible panel
|
|
// listing the key map. Created once at boot, persists across screens
|
|
// (it is not a screen — the state machine never knows it), hidden by
|
|
// default. `?` toggles it; `Escape` closes it (the keyboard module's
|
|
// `Esc` branch defers to it while it is open); a click on the panel's
|
|
// backdrop or its Close button dismisses it. No external docs needed
|
|
// mid-game.
|
|
//
|
|
// The rows are `getKeyMap()`'s — `input/keyboard.js` re-exports it
|
|
// (the canonical source) and the README's controls section renders the
|
|
// same rows (a test keeps them identical: the browser-rendered map
|
|
// against the README's block).
|
|
|
|
import { el } from "../screens/dom.js";
|
|
|
|
/**
|
|
* The key map the help panel (and the README's controls section — a
|
|
* test keeps them identical) renders: `{ keys, action }` rows in
|
|
* display order. `keys` are joined with " / " when rendered.
|
|
*/
|
|
export function getKeyMap() {
|
|
return [
|
|
{ keys: ["W", "A", "S", "D"], action: "move (or the arrow keys)" },
|
|
{ keys: ["Space", "Enter"], action: "action (pick, work, set down, serve)" },
|
|
{ keys: ["1", "2", "3", "4"], action: "emote (wave, cheer, thumbs up, thumbs down)" },
|
|
{ keys: ["?"], action: "help overlay" },
|
|
{ keys: ["Esc"], action: "close help / leave the room" },
|
|
];
|
|
}
|
|
|
|
/**
|
|
* The help overlay itself.
|
|
*
|
|
* `onToggle` — optional `(visible) => void` (the census seam; the
|
|
* keyboard module's `?`/`Esc` branches need no hook — they call
|
|
* `toggle()` / `hide()` directly).
|
|
*
|
|
* Returns `{ show, hide, toggle, visible }`.
|
|
*/
|
|
export function createHelpOverlay({ onToggle = null } = {}) {
|
|
let panel = null;
|
|
let visible = false;
|
|
|
|
function build() {
|
|
if (panel !== null) return panel;
|
|
const title = el(
|
|
"div",
|
|
{ class: "help-title" },
|
|
el("span", { class: "help-heading", text: "Controls" }),
|
|
el("button", {
|
|
class: "btn help-close",
|
|
type: "button",
|
|
"aria-label": "Close help",
|
|
text: "Close",
|
|
onClick: () => hide(),
|
|
})
|
|
);
|
|
const rows = getKeyMap().map((row) =>
|
|
el(
|
|
"div",
|
|
{ class: "help-row" },
|
|
el("kbd", { class: "help-keys", text: row.keys.join(" / ") }),
|
|
el("span", { class: "help-action", text: row.action })
|
|
)
|
|
);
|
|
panel = el(
|
|
"div",
|
|
{
|
|
id: "help-overlay",
|
|
class: "help-overlay",
|
|
role: "dialog",
|
|
"aria-label": "Help",
|
|
"aria-hidden": "true",
|
|
},
|
|
el(
|
|
"div",
|
|
{ class: "panel help-panel", onClick: (e) => e.stopPropagation() },
|
|
title,
|
|
el("div", { class: "help-rows" }, ...rows),
|
|
el("p", {
|
|
class: "help-foot",
|
|
text: "Press ? or Esc to close. On the play screen, Esc leaves the room.",
|
|
})
|
|
)
|
|
);
|
|
panel.addEventListener("click", () => hide());
|
|
return panel;
|
|
}
|
|
|
|
function show() {
|
|
const node = build();
|
|
if (visible) return;
|
|
node.setAttribute("aria-hidden", "false");
|
|
document.body.append(node);
|
|
visible = true;
|
|
if (onToggle !== null) onToggle(true);
|
|
}
|
|
|
|
function hide() {
|
|
if (!visible) return;
|
|
visible = false;
|
|
if (panel !== null) {
|
|
panel.setAttribute("aria-hidden", "true");
|
|
panel.remove();
|
|
}
|
|
if (onToggle !== null) onToggle(false);
|
|
}
|
|
|
|
function toggle() {
|
|
if (visible) hide();
|
|
else show();
|
|
}
|
|
|
|
return { show, hide, toggle, visible: () => visible };
|
|
}
|