FERRETGame engine over Babylon.js
Ferret

@ferret/ui

HUD and menus as a responsive HTML/CSS overlay that works with cursor/A/B, mouse and touch.

20 exports · source in packages/ui/src

Classes

LevelMenu class

class LevelMenu implements Service

Opens 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 LevelMenuModel
page: 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

ToastQueue class

class ToastQueue

Short 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 Service

The 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): string

A short sentence for the player, with the reason as a code for support.

packages/ui/src/service.ts:23

Interfaces

FailedEvent interface

interface FailedEvent

What 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 LevelMenuHandlers
places: 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 LevelMenuLabels
title: string
locations: string
hall: string
start: string
quit: string
back: string
close: string

packages/ui/src/level-menu.ts:21

LevelMenuOptions interface

interface LevelMenuOptions
document?: Document
parent?: HTMLElement
menuAction?: stringthe button action that opens and closes the menu (default "menu")
scale?: () => numberthe UI scale, set as --ui on the menu's root
blocked?: () => booleanreturn 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 LevelMenuPlace

The 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 LevelMenuRow
id: string
label: string
kind: 'locations' | 'hall' | 'start' | 'quit' | 'back' | 'place'
danger?: boolean

packages/ui/src/level-menu.ts:28

Toast interface

interface Toast
id: number
text: string
kind: string
left: number

packages/ui/src/model.ts:79

UIOptions interface

interface UIOptions
document?: Documentwhere the overlay goes; without a document the service keeps its state only (headless tests, servers)
parent?: HTMLElement
scale?: () => numberreturns the UI scale (use () => uiScaleFor(platform.screen())); called again on refreshScale()
useAction?: stringthe button action that uses an interactable; its label is shown in the prompt (default "use")
menuAction?: stringthe action that opens the pause menu, when pauseMenu is set (default "menu")
pauseMenu?: () => MenuDefopened 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: LevelMenuLabels

packages/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