@ferret/ui
HUD and menus as a responsive HTML/CSS overlay that works with cursor/A/B, mouse and touch.
Classes
LevelMenu class
class LevelMenu implements ServiceOpens on the menu action, pauses the world while it is up and reads the cursor keys, Enter (A) and Escape (B) itself (the fixed step does not run while paused), plus a gamepad's d-pad, A, B and Start.
readonly name: "level-menu" | |
readonly model: LevelMenuModel | |
constructor(handlers: LevelMenuHandlers, readonly opts: LevelMenuOptions = {}) | |
init(world: World) | |
press(button: 'up' | 'down' | 'accept' | 'back') | Feed the menu from any source (keys, gamepad, a test). |
fixedUpdate() | Called before the components' fixed update, at the fixed rate. |
update() | Called once per rendered frame. |
refreshScale() | |
dispose() |
packages/ui/src/level-menu.ts:124
LevelMenuModel class
class LevelMenuModelpage: LevelMenuPage | |
index: number | |
isOpen: boolean | |
onChange?: () => void | |
onOpen?: () => void | |
onClose?: () => void | |
constructor(readonly handlers: LevelMenuHandlers, readonly labels: LevelMenuLabels = DEFAULT_LABELS) | |
rows(): LevelMenuRow[] | the rows of the page being shown |
get current(): LevelMenuRow | null | |
open() | |
close() | |
toggle() | the menu action (Escape, Start, the Menu button) while the game runs opens it; while it is open it closes it, as IA_Escape and the widget's key handler do |
move(delta: number) | |
hover(i: number) | |
accept() | |
back() | B and Escape: the whole menu closes (the widget's key handler closes the widget, not one page) |
packages/ui/src/level-menu.ts:30
MenuModel class
class MenuModelA menu you steer with the cursor, A and B only (up/down/accept/back), or the pointer. Submenus stack; back pops them and, from the first menu, closes it. Disabled items are skipped by the cursor.
onChange?: () => void | |
onClose?: () => void | |
get isOpen(): boolean | |
get menu(): MenuDef | null | |
get index(): number | |
get current(): MenuItem | null | |
open(def: MenuDef): void | |
close(): void | |
move(delta: number): void | |
hover(index: number): void | Point at an item (mouse hover). Disabled items cannot be pointed at. |
accept(): void | |
back(): void |
packages/ui/src/model.ts:20
ToastQueue class
class ToastQueueShort messages that show for a while and go ("Picked up: brass key"). At most max show at once, the rest wait.
active: Toast[] | |
onChange?: () => void | |
constructor(readonly max = 3, readonly defaultMs = 3) | |
push(text: string, o: { seconds?: number; kind?: string } = {}): number | |
update(dt: number): void | |
clear(): void |
packages/ui/src/model.ts:82
UIService class
class UIService implements ServiceThe HTML overlay: HUD lines, the use-prompt, toasts, the loading screen and the menu. Everything shown is data first (hud, promptText, loading, toasts, menu), the DOM only draws it, so games and tests can read what the player sees. Cursor, A and B drive the menu (arrows, Enter, Escape; gamepad d-pad, A, B); mouse and touch work on the items directly.
readonly name: "ui" | |
readonly hud: Map<string, string> | |
readonly toasts: ToastQueue | |
readonly menu: MenuModel | |
promptText: string | |
loading: Loading | |
scale: number | |
constructor(readonly opts: UIOptions = {}) | |
init(world: World) | |
showFailure(e: FailedEvent) | The engine's failure screen: what went wrong in a line, then Retry and Back. It is a menu, so the cursor, A and B, the mouse and touch all work, at any screen size. Retry travels again with the same options. |
setPrompt(text: string) | Show the use-prompt for an interactable: "[E] Open door". An empty text hides it. |
setHud(key: string, text: string | null) | |
toast(text: string, o?: { seconds?: number; kind?: string }) | |
openMenu(def: MenuDef) | Open a menu and pause the game while it is up. |
refreshScale() | |
press(button: 'up' | 'down' | 'accept' | 'back') | Feed the menu from any source (keys, gamepad, a test). |
update(dt: number) | Called once per rendered frame. |
fixedUpdate() | Called before the components' fixed update, at the fixed rate. |
dispose() |
packages/ui/src/service.ts:43
Functions
failureText function
function failureText(e: FailedEvent): stringA short sentence for the player, with the reason as a code for support.
packages/ui/src/service.ts:23
Interfaces
FailedEvent interface
interface FailedEventWhat level:failed carries (see LevelService.travel).
id: string | |
reason?: string | |
handled?: boolean | |
opts?: Record<string, unknown> | |
error?: { message?: string; status?: number } |
packages/ui/src/service.ts:20
LevelMenuHandlers interface
interface LevelMenuHandlersplaces: LevelMenuPlace[] | every place, in the order the list shows them; name is what the row says |
onPlace(id: string): void | |
onHall(): void | |
onStart(): void | |
onQuit(): void |
packages/ui/src/level-menu.ts:12
LevelMenuLabels interface
interface LevelMenuLabelstitle: string | |
locations: string | |
hall: string | |
start: string | |
quit: string | |
back: string | |
close: string |
packages/ui/src/level-menu.ts:21
LevelMenuOptions interface
interface LevelMenuOptionsdocument?: Document | |
parent?: HTMLElement | |
menuAction?: string | the button action that opens and closes the menu (default "menu") |
scale?: () => number | the UI scale, set as --ui on the menu's root |
blocked?: () => boolean | return true while something else owns the screen (a failure screen, the sign-in card): the menu does not open then |
labels?: Partial<LevelMenuLabels> |
packages/ui/src/level-menu.ts:98
LevelMenuPlace interface
interface LevelMenuPlaceThe level-select menu of a game with many places: a main page (Locations, two relocations, Quit) and a scrolling Locations page (Back, then one row per place). Steered by the cursor (up, down), A (accept) and B (close) only, or the pointer. The model is plain data and runs headless; LevelMenu is the service that opens it on the menu action, pauses the world and draws it. The game decides what the rows do and how they look (class names ferret-lm*, see LEVEL_MENU_CSS for the structure only).
id: string | |
name: string |
packages/ui/src/level-menu.ts:10
LevelMenuRow interface
interface LevelMenuRowid: string | |
label: string | |
kind: 'locations' | 'hall' | 'start' | 'quit' | 'back' | 'place' | |
danger?: boolean |
packages/ui/src/level-menu.ts:28
MenuDef interface
interface MenuDeftitle: string | |
items: MenuItem[] | |
message?: string |
packages/ui/src/model.ts:14
MenuItem interface
interface MenuItemThe parts of the UI that are plain data, so they run (and are tested) without a DOM.
id: string | |
label: string | |
disabled?: boolean | |
action?: () => void | called on accept; a submenu opens instead if submenu is set |
submenu?: MenuDef | |
value?: () => string | a value shown at the right (a setting) |
packages/ui/src/model.ts:3
Toast interface
interface Toastid: number | |
text: string | |
kind: string | |
left: number |
packages/ui/src/model.ts:79
UIOptions interface
interface UIOptionsdocument?: Document | where the overlay goes; without a document the service keeps its state only (headless tests, servers) |
parent?: HTMLElement | |
scale?: () => number | returns the UI scale (use () => uiScaleFor(platform.screen())); called again on refreshScale() |
useAction?: string | the button action that uses an interactable; its label is shown in the prompt (default "use") |
menuAction?: string | the action that opens the pause menu, when pauseMenu is set (default "menu") |
pauseMenu?: () => MenuDef | opened by the menu action; the game pauses while it is open |
packages/ui/src/service.ts:5
Types
LevelMenuPage type
type LevelMenuPage = 'main' | 'locations'packages/ui/src/level-menu.ts:27
Constants
DEFAULT_LABELS const
const DEFAULT_LABELS: LevelMenuLabelspackages/ui/src/level-menu.ts:23
LEVEL_MENU_CSS const
const LEVEL_MENU_CSS: "\n:where(.ferret-lm){position:fixed;inset:0;display:none;align-items:center;justify-content:center;z-index:20;pointer-events:auto;font:calc(var(--ui,1)*16px)/1.3 system-ui,sans-serif;color:#fff;user-select:none;-webkit-user-select:none}\n:where(.ferret-lm.open){display:flex}\n:where(.ferret-lm__panel){position:relative;box-sizing:border-box;display:flex;flex-direction:column;overflow:hidden;backg …packages/ui/src/level-menu.ts:110
UI_CSS const
const UI_CSS: "\n.ferret-ui{position:fixed;inset:0;pointer-events:none;font:calc(var(--ui,1)*16px)/1.3 system-ui,sans-serif;color:#fff;\n padding:env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);user-select:none;-webkit-user-select:none;z-index:10}\n.ferret-ui *{box-sizing:border-box}\n.ferret-ui .hud{position:absolute;top:calc(var(--ui,1)*12px);left:cal …The overlay's stylesheet. Sizes are in calc(var(--ui) * N px) so the whole UI follows uiScaleFor (console, phone, desktop).
packages/ui/src/style.ts:2