# Fizlet API v0.21.3 > Fizlet™ (fzlt™) is the bricks for DOM animation and games, by Chris Cooley: a game or an animation boils down to logic and style, from a prompt. Every brick has a gzip budget in CI, there are no runtime dependencies, and the page always comes back. Every public export of every brick, generated from the published type declarations (dist/*.d.ts) of v0.21.3 by the TypeScript compiler at site build time. Each brick's page on https://fizlet.com/bricks/ shows the same, with live demos. Fizlet 0.x is a public beta: every brick carries a status flag, and a 0.x minor release may change a BETA brick's API (the CHANGELOG says how). COMPLETE bricks keep theirs. Status flags: - COMPLETE: Done, and always getting smaller and faster. - BETA: Tested and budgeted; not yet carried by a shipped game, or its API is still moving. - EXPERIMENTAL: In the package to try; no promise yet. - ROADMAP: Planned, not shipped: a name, a purpose, a size target. - DEPRECATED: Still in the package, replaced by something named. ## @fzlt/core Status: COMPLETE. Size: 3372 B gzip (budget 4096 B). Page: https://fizlet.com/bricks/kernel.html Import: `import { … } from "@fzlt/core"` The kernel: one loop, bounded work, and the page always comes back. It is the whole default export: one shared requestAnimationFrame for every engine on the page, a fixed 60 Hz step with a catch-up cap, auto-pause, bounded pools, the event ring, seeded random, settle and an fps cap for page interactions, and the ledger that gives the page back on destroy(). Every brick is its own subpath. ### createEngine (function, COMPLETE) Make an engine: a fixed-step loop on the page's shared clock, with its own pools, event ring, seeded random and ledger. Add systems with `add()`, then `start()`; `destroy()` (or aborting `o.signal`) stops it and gives the page back. Signature: `createEngine(o?: EngineOptions): Engine` - o (optional): `EngineOptions` - returns: `Engine` ```ts export declare function createEngine(o?: EngineOptions): { signal: AbortSignal; events: EventRing; ledger: Ledger; random: Random; root: Element | null; /** * The current (or last) frame, as a typed-array view (reading a slot never boxes a number): * `[0]` seconds this frame advances on the shared clock (0 after a pause or a settle), `[1]` ms * the last finished frame's hooks took (read, steps, render). Read-only by convention. */ timing: Float64Array; /** Fixed step length in seconds. */ dt: number; readonly time: number; /** Fixed steps run so far: an integer, so reading it in a hot path never boxes a number. */ readonly steps: number; readonly phase: number; /** Pooled things in use right now, summed over every `engine.pool` (no allocation). */ readonly live: number; readonly running: boolean; /** Why it's paused right now (empty when running). */ readonly paused: string[]; add(s: S): S; /** Safe from inside a hook: the rest of the frame still runs every other system. */ remove(s: System): void; /** A named, bounded pool, listed in stats(). */ pool(name: string, cap: number, create: () => T): Pool; /** Run (again). On a settled engine this is wake() too: the quiet time starts over, so it runs a * full settle window instead of going back to sleep on its first frame. Inert once destroyed. */ start(): void; /** * Something moved this frame: keeps a settling engine awake (no-op without `settle`). With * `sec`, something is scheduled that far ahead (an intro delay, a pause between beats): the * engine stays awake for `sec` of its own time (steps, so a slow frame can't outrun it), and * settle counts from then. A later, shorter hold never cuts a longer one; 0 or NaN is busy(). */ busy(sec?: number): void; /** Wake a settled engine (input does this for you); the quiet time starts again. */ wake(): void; stop(): void; /** Called by the scheduler with the rAF timestamp. */ frame(now: number): void; /** Run one frame's worth of time by hand (tests, headless, a server). A hook's error throws here. * `sec` must be a finite number (0 or less runs a frame with no steps); anything else throws a * RangeError before the frame starts, instead of stalling the clock for good (S6 P2-04). */ advance(sec: number): void; stats(): EngineStats; /** Stop for good: every listener and observer goes (one signal), the page is restored. * Idempotent; afterwards the engine is inert (start, add and advance do nothing). */ destroy(): void; }; ``` ### scheduler (const, COMPLETE) Scheduler facts for tests and the debug HUD. Type: `{ readonly engines: number; readonly requests: number; }` ```ts export declare const scheduler: { readonly engines: number; readonly requests: number; }; ``` ### IDLE, READ, STEP, RENDER (const, COMPLETE) Frame phases. Read geometry only in READ, never in STEP; write the DOM only in RENDER. ```ts export declare const IDLE = 0, READ = 1, STEP = 2, RENDER = 3; ``` ### Engine (type, COMPLETE) The engine `createEngine()` returns: what every system and brick takes. ```ts export type Engine = ReturnType; ``` ### EngineOptions (interface, COMPLETE) Options for `createEngine()`. Every field is optional. ```ts export interface EngineOptions { /** The element the engine lives in; it pauses while this is off screen. */ root?: Element | null; /** Fixed step in seconds (default 1/60). */ step?: number; /** Most steps per frame before dropping time instead of spiralling (default 4). */ maxSteps?: number; /** Seed for `engine.random` (default 1). */ seed?: number; /** Event ring capacity per frame (default 64). */ events?: number; /** Aborting this destroys the engine (Recall included). */ signal?: AbortSignal; /** A hook threw during a scheduled frame. The engine is already stopped (`start()` resumes it); * other engines and the shared clock keep running. Default: `console.error(error)`. */ onError?: (error: unknown, engine: Engine) => void; /** Settle (page interactions): after this many seconds in which no system called `busy()`, the * engine sleeps (paused: "settled", no frames at all) until `wake()`, input or `start()`. Off by default. * Counted in engine time (steps). Something scheduled for later (an intro delay, a beat, an * autoplay timer) holds it awake with `busy(sec)`. */ settle?: number; /** Frame-rate cap (e.g. 30 for an ambient hero): between frames the engine is off the shared clock * and a timer brings it back, so it asks for ~fps requestAnimationFrame calls a second. */ fps?: number; /** Auto-pause rules (all on by default; reducedMotion is opt-in: most games calm their effects). */ pause?: { hidden?: boolean; offscreen?: boolean; editable?: boolean; reducedMotion?: boolean; }; } ``` ### EngineStats (interface, COMPLETE) What `engine.stats()` reports: frame and step counts, frame time, the event ring and pool fill, the ledger's size and the auto-pause rules holding the engine right now. ```ts export interface EngineStats { frames: number; steps: number; dropped: number; time: number; frameMs: { avg: number; max: number; }; events: { size: number; cap: number; dropped: number; }; pools: Record; ledger: number; paused: string[]; } ``` ### System (interface, COMPLETE) A system: game logic or a harness. Every hook is optional; systems run in the order added. ```ts export interface System { /** Once per frame, before the steps: read cached geometry, sample input. */ read?(e: Engine): void; /** Fixed step (dt = engine.dt): the simulation. No DOM reads or writes, no allocation. */ step?(e: Engine, dt: number): void; /** Once per frame, after the steps: write the DOM (change-only), play sounds from the ring. */ render?(e: Engine, alpha: number): void; /** When a running engine pauses (stop() or an auto-pause rule): drop held input, stop sounds. */ pause?(e: Engine): void; } ``` ### Ledger (class, COMPLETE) Records every change the engine makes to the page (styles, attributes, classes, text, added nodes) so `restore()` puts it back exactly. Writes that change nothing touch nothing, and an edit the host page made after ours always wins. Each engine has one: `engine.ledger`. Signature: `new Ledger(): Ledger` - returns: `Ledger` ```ts export declare class Ledger { /** Replace a text node's value. False if the host changed it since our last write. */ text(node: CharacterData, value: string): boolean; /** Set one inline style property (e.g. "transform", "--hp"). */ style(el: El, prop: string, value: string, priority?: string): boolean; /** Put one style property back now (as Recall would) and forget it: a later `style` records the * value then in place as the new original. Hands off if the host changed it since our last write. */ unstyle(el: El, prop: string): boolean; /** Set (string) or remove (null) an attribute. */ attr(el: El, name: string, value: string | null): boolean; /** Add or remove one class. */ cls(el: El, name: string, on: boolean): boolean; /** A node the engine put on the page (canvas, overlay, debris): removed on restore. Once closed * (after engine.destroy()) it is removed as soon as the caller's task ends: the caller inserts it * after this returns, so it can't be refused here. */ add(node: N): N; /** Recall: put back everything we still own, newest first. Host edits win. With `close` (what * `engine.destroy()` passes) every later text, style, attr or cls write is refused (returns false, * touches nothing), so nothing written after Recall outlives it; without it the ledger is reused. */ restore(close?: boolean): void; /** How many things the ledger holds (for stats). */ get size(): number; } ``` ### Pool (class, COMPLETE) A bounded pool. Every item is made up front, so acquiring and releasing never allocates and the pool never grows: when it's full, `acquire()` returns undefined (refuse new work). Signature: `new Pool(cap: number, create: () => T): Pool` - cap: `number` - create: `() => T` - returns: `Pool` ```ts export declare class Pool { readonly cap: number; readonly items: T[]; constructor(cap: number, create: () => T); get size(): number; acquire(): T | undefined; /** Swap-remove by index (order is not kept). Iterate backwards when releasing in a loop. */ releaseAt(i: number): boolean; release(item: T): boolean; clear(): void; } ``` ### EventRing (class, COMPLETE) Fixed-capacity event ring, filled during a frame's steps and read in render (sound adapter, effects, HUD). Harnesses talk through it instead of callbacks. Full = the event is dropped. Signature: `new EventRing(cap?: number): EventRing` - cap (optional): `number` - returns: `EventRing` ```ts export declare class EventRing { readonly cap: number; size: number; dropped: number; constructor(cap?: number); emit(type: string, a?: number, b?: number, x?: number, y?: number, v?: number): boolean; at(i: number): FizEvent | undefined; clear(): void; } ``` ### FizEvent (interface, COMPLETE) One event in the ring. Slots are reused; copy what you need before the next frame. ```ts export interface FizEvent { type: string; a: number; b: number; x: number; y: number; v: number; } ``` ### seeded (function, COMPLETE) A seeded random number generator: `r()` returns [0, 1). The same seed gives the same sequence; read or set `r.state` to snapshot and restore it. Signature: `seeded(seed?: number): Random` - seed (optional): `number` - returns: `Random` ```ts export declare function seeded(seed?: number): Random; ``` ### Random (interface, COMPLETE) Seeded random (the DOM VIRUS core LCG, same sequence). Use it for anything that changes the simulation, so a run can be replayed or synced; `state` reads and restores it for snapshots. The state lives in a Uint32Array so drawing a number never allocates. ```ts export interface Random { (): number; state: number; } ``` ## @fzlt/core/input Status: COMPLETE. Size: 1335 B gzip (budget 1536 B). Page: https://fizlet.com/bricks/input.html Import: `import { … } from "@fzlt/core/input"` One controller for keys, pointer and a touch stick, mapped to named intents. Listeners only record; the engine's read phase latches edges and measures the target, so a step sees a stable snapshot. Keys typed into the host page's fields (or inside [data-fizlet-ignore]) never count. Keys, the pointer, `press()` and `steer()` wake a settled engine. Held state is dropped on window blur, when the engine pauses, on dispose() and on destroy: nothing stays stuck. `hit` (an intent or the pointer): a press is seen by exactly one fixed step, the first step after it, even when that comes frames later (0-step frames while catching up) and never again in the same frame's later steps. Outside a step (read, render), `hit` is true for the one frame whose read latched it. Presses between two steps count once. ### ControlsOptions (interface, COMPLETE) Options for `controls()`: the intents and their keys, the pointer target and the touch stick. ```ts export interface ControlsOptions { /** intent → key codes, e.g. { fire: ["Space"], up: ["KeyW", "ArrowUp"] } (KeyboardEvent.code). */ keys?: Record; /** Where the pointer is measured and the stick lives (default: the engine root, else window). */ target?: HTMLElement; /** Touch: a drag that starts on the left half (or anywhere: "any") drives a virtual stick. */ stick?: "left" | "any" | false; /** Stick radius in CSS px (default 60). */ radius?: number; } ``` ### Intent (type, COMPLETE) An intent by name, or by the number `id(name)` gave. ```ts export type Intent = string | number; ``` ### controls (function, COMPLETE) One controller for keys, pointer and a touch stick on engine `e`, mapped to named intents: `down(intent)`, `hit(intent)` (one step per press), `pointer` and `stick`. It registers its own read-phase system; `dispose()` removes it. Signature: `controls(e: Engine, o?: ControlsOptions): Controls` - e: `Engine` - o (optional): `ControlsOptions` - returns: `Controls` ```ts export declare function controls(e: Engine, o?: ControlsOptions): { /** An intent's number, for a hot path: `const FIRE = c.id("fire")`, then `c.hit(FIRE)` in a step. * A name that is not an intent throws here, at setup (a typo is never a silent `false`). */ id(intent: string): number; /** Held right now (any of its keys, or press()). */ down: (intent: Intent) => boolean; /** Pressed: true in the first step after the press; outside steps, for the frame it latched. */ hit: (intent: Intent) => boolean; /** -1..1 from two intents (keys), plus the stick when it's active. */ axis(neg: Intent, pos: Intent, stickAxis?: 0 | 1): number; pointer: { readonly x: number; readonly y: number; readonly down: boolean; readonly hit: boolean; }; stick: { readonly x: number; readonly y: number; readonly active: boolean; }; /** Press an intent from code (on-screen keys): `press("strike")`. */ press(intent: string, down?: boolean): void; /** Drive the stick from code (an on-screen stick, @fzlt/core/pad): x, y in -1..1; `on = false` lets go. */ steer(x: number, y: number, on?: boolean): void; dispose: () => void; system: System; }; ``` ### Controls (type, COMPLETE) The controller `controls()` returns. ```ts export type Controls = ReturnType; ``` ## @fzlt/core/pad Status: COMPLETE. Size: 2068 B gzip (budget 2120 B). Page: https://fizlet.com/bricks/pad.html Import: `import { … } from "@fzlt/core/pad"` Swappable on-screen controls for touch (and mouse). One move control (an analog stick, a d-pad or move keys) under one thumb and round arcade action keys under the other, all driving `controls` intents through `press()` / `steer()`: game code reads `down`, `hit` and `axis` and never knows which scheme is on screen. Swap with `setScheme()` at any time. Visuals are the Board UI kit's (`.bui-pad`, `.bui-stick`, `.bui-dpad`, `.bui-arcade` + roles): mount the pad inside a `.bui` root. Listeners are delegated on the pad's one element (bound to the engine's signal), so a scheme swap adds none; the element goes through the ledger. ### Scheme (type, COMPLETE) The move control on screen: an analog stick, a one-finger d-pad, or a key per move intent. ```ts export type Scheme = "stick" | "dpad" | "buttons"; ``` ### PadRole (type, COMPLETE) A Board UI kit key role: it sets an action key's colour. ```ts export type PadRole = "fire" | "aim" | "attack" | "advance" | "fallin" | "sys" | "cmd" | "go" | "flag"; ``` ### PadAction (interface, COMPLETE) One round action key: the intent it drives, its caption and accessible name, its role colour. ```ts export interface PadAction { /** The `controls` intent this key drives. */ intent: string; /** Short caption on the key (e.g. "FIRE"); empty for icon-only. */ label: string; /** Accessible name (default: label), e.g. "Hold to fire the cannon (Space)". */ name?: string; /** Inline SVG markup (trusted game code). */ icon?: string; /** Shortcut chip for fine pointers, e.g. "SPACE". */ key?: string; /** Kit role colour (default: "fire" for the first action, "sys" for the rest). */ role?: PadRole; /** Held while the finger is down (default: a tap = one press, one `hit`). */ hold?: boolean; /** Each tap flips the intent: `down(intent)` is the switch; `press(intent, on)` from code flips the key too. */ toggle?: boolean; } ``` ### PadOptions (interface, COMPLETE) Options for `pad()`: the move scheme, the action keys, which thumb gets them and where the pad goes. ```ts export interface PadOptions { /** "stick" (analog, drives the stick axes), "dpad" (one finger, 8-way) or "buttons" (a key per move intent). */ scheme?: Scheme; /** Action keys; the first is the big primary key. */ actions?: PadAction[]; /** Which thumb gets the action keys (default "right"); the move control takes the other. */ side?: "left" | "right"; /** Where the pad goes (default: the engine root). Must be inside a `.bui` root for the kit look. */ host?: HTMLElement; /** Move intents [up, down, left, right] for the d-pad and move keys; "" leaves one out. */ move?: string[]; } ``` ### stickAt (function, COMPLETE) Stick vector from a drag offset in radii: clamped to the unit circle, dead zone rescaled so a light push still creeps (Battlefield's feel). Writes out[0], out[1]; never allocates (the exact `hypot` from @fzlt /core/steer: `Math.hypot` boxes its arguments, K5). Signature: `stickAt(dx: number, dy: number, out: Float64Array): void` - dx: `number` - dy: `number` - out: `Float64Array` - returns: `void` ```ts export declare function stickAt(dx: number, dy: number, out: Float64Array): void; ``` ### dpadAt (function, COMPLETE) D-pad direction mask (up 1, down 2, left 4, right 8; diagonals combine) from a drag offset in radii. Signature: `dpadAt(dx: number, dy: number): number` - dx: `number` - dy: `number` - returns: `number` ```ts export declare function dpadAt(dx: number, dy: number): number; ``` ### pad (function, COMPLETE) On-screen controls for touch (and mouse) that drive the intents of `c` (a `controls()` controller): a move control and action keys, in the Board UI kit's look. Swap the move control with `setScheme()`; `destroy()` (or the engine's) removes the pad. Signature: `pad(e: Engine, c: Controls, o?: PadOptions): Pad` - e: `Engine` - c: `Controls` - o (optional): `PadOptions` - returns: `Pad` ```ts export declare function pad(e: Engine, c: Controls, o?: PadOptions): { /** The pad's root element (`.bui-pad`). */ el: HTMLDivElement; readonly scheme: Scheme; setScheme: (s: Scheme) => void; /** Let go of every finger, remove the listeners and the pad (engine.destroy() does this too). */ destroy(): void; }; ``` ### Pad (type, COMPLETE) The pad `pad()` returns. ```ts export type Pad = ReturnType; ``` ## @fzlt/core/collide Status: COMPLETE. Size: 1265 B gzip (budget 1536 B). Page: https://fizlet.com/bricks/collide.html Import: `import { … } from "@fzlt/core/collide"` A fixed-capacity spatial hash (no allocation per insert or query) and the shape tests games need: circle–circle, circle–rect, swept segment–circle. ### Grid (class, COMPLETE) Spatial hash over a w×h world. Insert ids each step (after clear), then query a circle. Three separate capacities, each saturating visibly (counters, never silent): - entities `cap`: ids 0..cap-1; any other id (or a non-finite x, y, r) is refused, `rejected`; - index `nodes` (default cap × 4): one node per cell a body covers; past it, `dropped`; - query results: the caller's `out` buffer; ids that don't fit set `missed` (this query) and count in `truncated` (queries cut short). Size `out` so `missed` stays 0: collision results must not drop, so a game should treat any of these counters rising as a bug. Signature: `new Grid(cell: number, cap: number, w: number, h: number, nodes?: number): Grid` - cell: `number` - cap: `number` - w: `number` - h: `number` - nodes (optional): `number` - returns: `Grid` ```ts export declare class Grid { cell: number; cols: number; rows: number; /** Entity capacity: valid ids are 0..cap-1. */ readonly cap: number; /** Index capacity: cell entries per step (a body covering 4 cells takes 4). */ readonly nodes: number; /** Index entries refused because the node store was full (raise `nodes`). Cumulative. */ dropped: number; /** Inserts and queries refused: id outside 0..cap-1, or a non-finite (or negative r) circle. Cumulative. */ rejected: number; /** Queries whose result did not fit in `out`. Cumulative. */ truncated: number; /** Ids the last query found but could not write to `out` (0 = complete). */ missed: number; constructor(cell: number, cap: number, w: number, h: number, nodes?: number); /** World size changed (resize is not a hot path). */ resize(w: number, h: number): void; clear(): void; /** The circle being inserted or queried. The work below takes only integers, so a call that * V8 doesn't inline never boxes a number (numbers stay in this typed array). */ /** Add id (0..cap-1) covering the circle (x, y, r). */ insert(id: number, x: number, y: number, r: number): void; /** * Hot-path forms: read body i's x, y, r from typed arrays (arrays and integers never box, even * when a big step function stops V8 inlining these calls). `pad` widens the search radius. */ add(x: Nums, y: Nums, r: Nums, i: number): void; near(x: Nums, y: Nums, r: Nums, i: number, out: Int32Array, pad?: number): number; /** * Ids whose cells touch the circle, each once, into `out`. Returns the count written. If more * were found than `out` holds, `missed` says how many (and `truncated` counts the query). */ query(x: number, y: number, r: number, out: Int32Array): number; } ``` ### circles (const, COMPLETE) Two circles overlap. Signature: `circles(ax: number, ay: number, ar: number, bx: number, by: number, br: number): boolean` - ax: `number` - ay: `number` - ar: `number` - bx: `number` - by: `number` - br: `number` - returns: `boolean` ```ts export declare const circles: (ax: number, ay: number, ar: number, bx: number, by: number, br: number) => boolean; ``` ### overlap (function, COMPLETE) Bodies i and j overlap, reading x, y, r from typed arrays. Prefer this in big step functions: it passes arrays and integers, so V8 never boxes numbers even when the call isn't inlined (number arguments to a non-inlined function allocate). Signature: `overlap(x: Nums, y: Nums, r: Nums, i: number, j: number): boolean` - x: `Nums` - y: `Nums` - r: `Nums` - i: `number` - j: `number` - returns: `boolean` ```ts export declare function overlap(x: Nums, y: Nums, r: Nums, i: number, j: number): boolean; ``` ### circleRect (function, COMPLETE) A circle overlaps a rect (x, y, w, h). Signature: `circleRect(cx: number, cy: number, r: number, x: number, y: number, w: number, h: number): boolean` - cx: `number` - cy: `number` - r: `number` - x: `number` - y: `number` - w: `number` - h: `number` - returns: `boolean` ```ts export declare function circleRect(cx: number, cy: number, r: number, x: number, y: number, w: number, h: number): boolean; ``` ### inRect (const, COMPLETE) A point is inside a rect. Signature: `inRect(px: number, py: number, x: number, y: number, w: number, h: number): boolean` - px: `number` - py: `number` - x: `number` - y: `number` - w: `number` - h: `number` - returns: `boolean` ```ts export declare const inRect: (px: number, py: number, x: number, y: number, w: number, h: number) => boolean; ``` ### segCircle (function, COMPLETE) Swept hit: where along the segment (x0, y0) → (x1, y1) it first touches the circle, as 0..1, or -1 for a miss. Use it for fast shots so they never tunnel through a target. Signature: `segCircle(x0: number, y0: number, x1: number, y1: number, cx: number, cy: number, r: number): number` - x0: `number` - y0: `number` - x1: `number` - y1: `number` - cx: `number` - cy: `number` - r: `number` - returns: `number` ```ts export declare function segCircle(x0: number, y0: number, x1: number, y1: number, cx: number, cy: number, r: number): number; ``` ## @fzlt/core/grid Status: BETA. Size: 1652 B gzip (budget 1664 B). Page: https://fizlet.com/bricks/grid.html Import: `import { … } from "@fzlt/core/grid"` The game board. A cols × rows tile grid in typed arrays (terrain id, move cost, flags), A* paths, line of sight, movement range and seeded terrain. Every search reuses buffers made once by `board()`, so no call allocates. Cell ids are `row * cols + col`. Not collision: @fzlt/core/collide's `Grid` re-buckets moving bodies every step; a `Board` holds the ground they move over. Use the same cell size for both and a body's collide bucket is its cell. The size is checked at setup: `cols` and `rows` are positive integers, `cell` a finite number > 0 (a RangeError names all three). A search (`path`, `flood`, `los`, `hit`) checks its cell ids: an id that is not an integer on the board throws `fizlet/grid: cell … is not on the board`. ### BLOCKED, COVER, OCCUPIED (const, BETA) Flags: BLOCKED stops movement and sight, COVER stops sight (pass it in a mask), OCCUPIED marks a unit standing there (paths avoid it unless it is the goal). Bits 8–128 are yours. ```ts export declare const BLOCKED = 1, COVER = 2, OCCUPIED = 4; ``` ### BoardOptions (interface, BETA) Options for `board()`: the cell size in world units and whether moves go 8-way. ```ts export interface BoardOptions { /** World units per cell (default 1: world = grid units). */ cell?: number; /** 8-way moves (never cutting a blocked corner). Default false: 4-way. */ diagonal?: boolean; } ``` ### Board (class, BETA) The board `board()` makes: typed-array terrain, cost and flags per cell, plus the searches (A* paths, line of sight, movement range) on buffers made once. Make one with `board()`. Signature: `new Board(cols: number, rows: number, opts?: BoardOptions): Board` - cols: `number` - rows: `number` - opts (optional): `BoardOptions` - returns: `Board` ```ts export declare class Board { readonly cols: number; readonly rows: number; readonly cell: number; diagonal: boolean; /** Per cell: your terrain id (0 = open ground), move cost to enter (1–255, default 1), flags. * Plain typed arrays: write them directly (`flags[i] |= OCCUPIED`, `flags.fill(0)` to reset). */ readonly terrain: Uint8Array; readonly cost: Uint8Array; readonly flags: Uint8Array; /** Cells the last search expanded; `partial` is true when the last path ran out of budget. */ expanded: number; partial: boolean; constructor(cols: number, rows: number, opts?: BoardOptions); /** The cell under a world point, clamped to the board (so always a valid id). */ at(x: number, y: number): number; /** Hot-path form: the cell under body i, reading typed arrays (nothing boxes). */ cellOf(x: Float64Array | Float32Array, y: Float64Array | Float32Array, i: number): number; /** World centre of cell i into out[k], out[k + 1]. */ center(i: number, out: Float64Array | Float32Array, k?: number): void; set(i: number, terrain: number, cost?: number, flags?: number): void; /** * Seeded terrain: inside the rect (c0, r0)–(c1, r1) (default the whole board), each open cell * (terrain 0, no flags) becomes `terrain` with chance `density`. One draw per cell in the rect, * whatever is there, so the same seed always lays the same board. Returns cells placed. */ scatter(rand: () => number, density: number, terrain: number, cost?: number, flags?: number, c0?: number, r0?: number, c1?: number, r1?: number): number; /** A random cell with none of `mask`, or -1 after `tries` misses (spawns, pickups, crates). */ pick(rand: () => number, mask?: number, tries?: number): number; /** * A* from `from` to `to` into `out`: the cells to step through, start excluded, goal last. * Returns the step count (0 when from === to, -1 for no path); if it is over out.length only the * first out.length steps are written. `budget` caps the cells expanded (0 = no cap) so a frame * never stalls: on a cap it writes the path to the closest cell found and sets `partial`. * Cells with any `avoid` flag are walls, except that the goal may be OCCUPIED. */ path(from: number, to: number, out: Int32Array, budget?: number, avoid?: number): number; /** * Movement range: every cell reachable from `from` for at most `max` cost (an orthogonal step * into a cost-1 cell costs 1, a diagonal 1.4), nearest first, start included. Returns the count * (capped at out.length). `dist(i)` then reads each cell's cost. */ flood(from: number, max: number, out: Int32Array, avoid?: number): number; /** Cost from the last search's start to cell i (exact after flood), or -1 if it was not reached. */ dist(i: number): number; /** Line of sight between two cells: no cell strictly between them has a `mask` flag. * Symmetric (a sees b exactly when b sees a). Pass BLOCKED | COVER to let cover block it. */ los(a: number, b: number, mask?: number): boolean; /** The first cell strictly between a and b (walking from a) with a `mask` flag, or -1 if clear. * Bresenham over cells: what a shot hits on its way. */ hit(a: number, b: number, mask?: number): number; } ``` ### board (const, BETA) A cols × rows board; every buffer a search needs is made here, once. Signature: `board(cols: number, rows: number, opts?: BoardOptions): Board` - cols: `number` - rows: `number` - opts (optional): `BoardOptions` - returns: `Board` ```ts export declare const board: (cols: number, rows: number, opts?: BoardOptions) => Board; ``` ## @fzlt/core/fx Status: COMPLETE. Size: 2792 B gzip (budget 3072 B). Page: https://fizlet.com/bricks/fx.html Import: `import { … } from "@fzlt/core/fx"` Explosions that land. One fixed pool of particles (struct-of-arrays, no allocation), drawn on a canvas: square debris, flashes, shock rings, spinning square rings, smoke, scorch marks and pop words, plus screen shake. `blast()` is the DOM VIRUS / Battlefield hero recipe. Gravity (`gravity`, px/s²) pulls each kind by its own share. Particle kinds are data (shape, drag, gravity, how they leave an emitter): `emitter(fx, KINDS)` registers the built-in bubble, spark, drip and debris (or your own) and sprays them; a page that never calls it never ships it. Visual only: its own random (seeded from Math.random), never the simulation's seeded random. ### SHAPES (const, COMPLETE) Shapes a kind can draw as (a streak trails its velocity: sparks, drips). Type: `readonly ["square", "disc", "ring", "frame", "word", "smoke", "streak"]` ```ts export declare const SHAPES: readonly ["square", "disc", "ring", "frame", "word", "smoke", "streak"]; ``` ### Kind (interface, COMPLETE) A particle kind, as data. Ranges are [min, max]; speed and size scale with emit's `scale`. ```ts export interface Kind { shape: (typeof SHAPES)[number]; /** Velocity lost per second, as a fraction (1.4 is the blast's smoke and flashes). */ drag: number; /** Share of the pool's gravity (0: none; negative floats up, like a bubble). */ gravity: number; /** Emit direction in radians (-π/2 is up) and the spread around it (2π: all round). */ angle: number; spread: number; speed: readonly [number, number]; size: readonly [number, number]; life: readonly [number, number]; } ``` ### BUBBLE (const, COMPLETE) Built-in particle kind: small squares that drift up and fade. Type: `Kind` ```ts export declare const BUBBLE: Kind; ``` ### SPARK (const, COMPLETE) Built-in particle kind: fast streaks thrown in every direction, falling a little. Type: `Kind` ```ts export declare const SPARK: Kind; ``` ### DRIP (const, COMPLETE) Built-in particle kind: streaks that fall straight down. Type: `Kind` ```ts export declare const DRIP: Kind; ``` ### DEBRIS_KIND (const, COMPLETE) Built-in particle kind: square debris thrown in every direction, falling under full gravity. Type: `Kind` ```ts export declare const DEBRIS_KIND: Kind; ``` ### KINDS (const, COMPLETE) All four by name: `emitter(fx, KINDS)`, or pick some (`emitter(fx, { bubble: BUBBLE })`). Type: `{ bubble: Kind; spark: Kind; drip: Kind; debris: Kind; }` ```ts export declare const KINDS: { bubble: Kind; spark: Kind; drip: Kind; debris: Kind; }; ``` ### FxOptions (interface, COMPLETE) Options for `effects()`: the particle cap, the colour and word tables, scorch marks and gravity. ```ts export interface FxOptions { /** Max live particles (default 420). New ones are refused (and counted in `dropped`) when full. */ cap?: number; /** Colour table, at most 256; particles store an index (default: the Battlefield blast palette). */ colors?: string[]; /** Word table for pop words (index into it), at most 256. */ words?: string[]; /** Max scorch marks kept on the ground (default 36; 0 turns them off). */ scorch?: number; /** Gravity in px/s² down (default 0); each kind takes its own share of it. */ gravity?: number; } ``` ### effects (function, COMPLETE) Overload policy: particles are decorative, so a full pool refuses new ones and counts them in `dropped`; a blast stops its per-particle work as soon as the pool is full. Invalid input (non-finite numbers, a life that would never end) is refused and counted the same way. Signature: `effects(o?: FxOptions): Effects` - o (optional): `FxOptions` - returns: `Effects` ```ts export declare function effects(o?: FxOptions): { colors: string[]; words: string[]; spawn: (k: number, x: number, y: number, vx: number, vy: number, size: number, life: number, c: number, rot?: number, spin?: number, w?: number) => boolean; readonly live: number; readonly shake: number; /** Particles refused so far: pool full, or invalid input. Decorative, so they may drop; counted. */ readonly dropped: number; /** Set gravity in px/s² down (each kind takes its own share; read it back from your own state). */ gravity: number; /** The pool itself, for emitter() and other helpers in this file. Not public API. */ raw: { f: Float32Array[]; kind: Uint8Array; col: Uint8Array; word: Uint8Array; st: Float64Array; K: Float32Array; cap: number; rnd: (a: number, b: number) => number; }; /** Add screen shake (decays on its own; read `shake` when drawing). */ kick(amount: number): void; /** Pop word (index into `words`), drifting up. */ word(x: number, y: number, w: number, c: number, size?: number): void; /** The hero explosion: flash, fireball, shock ring, spinning square ring, debris, smoke, scorch. */ blast(x: number, y: number, power?: number, tint?: number): undefined; /** Advance every particle (call once per frame or per step). */ step(dt: number): void; /** Scorch marks under everything; call before drawing the actors. */ drawGround(ctx: CanvasRenderingContext2D, light?: boolean): void; /** Every live particle, on top of the actors. */ draw(ctx: CanvasRenderingContext2D, light?: boolean): void; clear(): void; }; ``` ### Effects (type, COMPLETE) The particle pool `effects()` returns. ```ts export type Effects = ReturnType; ``` ### emitter (function, COMPLETE) Named particle kinds on a pool: `const { ids, emit } = emitter(fx, KINDS)`, then `emit(ids.bubble, x, y, 12)`. Its own export, so a page that never sprays kinds never ships it. At most 26 kinds per pool (ids 6-31); a second emitter on the same pool adds after the first. Signature: `emitter(fx: Effects, kinds: Record): { … }` - fx: `Effects` - kinds: `Record` - returns: `{ … }` ```ts export declare function emitter(fx: Effects, kinds: Record): { ids: Record; /** * `n` particles of kind `k` from (x, y), as that kind's data says: direction, spread, speed, size * and life (speed and size times `scale`). Colours are picked from `c`...`c + cn - 1`, and a word * kind's words from `w`...`w + wn - 1` (indexes into the pool's `words`). Bounded: it stops at * capacity and counts the rest in `dropped`. Writes the arrays directly: nothing boxes. */ emit(k: number, x: number, y: number, n: number, c?: number, cn?: number, scale?: number, w?: number, wn?: number): undefined; }; ``` ## @fzlt/core/camera Status: BETA. Size: 1400 B gzip (budget 1536 B). Page: https://fizlet.com/bricks/camera.html Import: `import { … } from "@fzlt/core/camera"` The page is the camera. Pan and zoom a DOM stage (any element) with one `transform` written through the engine's ledger, so Recall gives it back exactly. Moves ease and settle (the engine sleeps when they end); with reduced motion they jump. `contain` (default on) keeps the stage covering its own box, so no edge shows and the page never scrolls sideways. Coordinates: world = the stage's own untransformed CSS px (0,0 is its top-left corner); screen = the same box after the camera (what the visitor sees, in the stage's untransformed layout box). ### CameraOptions (interface, BETA) Options for `camera()`: easing rate, zoom limits, containment and reduced-motion handling. ```ts export interface CameraOptions { /** Easing rate per second (default 7; higher is snappier). A move settles in about 6 / ease s. */ ease?: number; /** Zoom limits (default 1 and 4). */ min?: number; max?: number; /** Keep the stage covering its own box and clip its parent (default true). */ contain?: boolean; /** Ease moves (default: yes, unless the visitor prefers reduced motion; then every move jumps). */ motion?: boolean; } ``` ### camera (function, BETA) A camera on stage `el`: pan and zoom it with one `transform` written through the engine's ledger (so Recall gives it back). Moves ease and settle; `to()`, `pan()` and `home()` drive it. Signature: `camera(engine: Engine, el: HTMLElement | SVGElement, o?: CameraOptions): Camera` - engine: `Engine` - el: `HTMLElement | SVGElement` - o (optional): `CameraOptions` - returns: `Camera` ```ts export declare function camera(engine: Engine, el: HTMLElement | SVGElement, o?: CameraOptions): { /** Show world point (x, y) at screen point (px, py) (default: the same point, a zoom about it). */ to(x: number, y: number, zoom?: number, px?: number, py?: number): void; /** Stage size in px (untransformed), as last measured. */ readonly width: number; readonly height: number; /** Drag by a screen delta (px): follows at once, no easing. */ pan(dx: number, dy: number): void; /** Back to the page as it is (zoom 1, no offset), eased. */ home(): void; /** Screen (stage box px) to world, into `out` (no allocation). */ toWorld(x: number, y: number, out: T): T; readonly zoom: number; /** Still easing toward the target. */ readonly moving: boolean; read(): void; step(e: Engine, dt: number): void; render(): void; }; ``` ### Camera (type, BETA) The camera `camera()` returns. ```ts export type Camera = ReturnType; ``` ### zoomAt (function, BETA) Zoom about world point (x, y): it stays where it is on screen. The same as `cam.to(x, y, zoom)`. Signature: `zoomAt(cam: Camera, x: number, y: number, zoom: number): void` - cam: `Camera` - x: `number` - y: `number` - zoom: `number` - returns: `void` ```ts export declare function zoomAt(cam: Camera, x: number, y: number, zoom: number): void; ``` ### flyTo (function, BETA) Centre world point (x, y) on the stage at `zoom`. Signature: `flyTo(cam: Camera, x: number, y: number, zoom?: number): void` - cam: `Camera` - x: `number` - y: `number` - zoom (optional): `number` - returns: `void` ```ts export declare function flyTo(cam: Camera, x: number, y: number, zoom?: number): void; ``` ### toScreen (function, BETA) World to screen (stage box px), into `out`: the inverse of `cam.toWorld`. Signature: `toScreen(cam: Camera, x: number, y: number, out: T): T` - cam: `Camera` - x: `number` - y: `number` - out: `T` - returns: `T` ```ts export declare function toScreen(cam: Camera, x: number, y: number, out: T): T; ``` ### fit (function, BETA) Fit a world box (x, y, w, h) in the stage, with `pad` (fraction, default 0.1) around it. Signature: `fit(cam: Camera, x: number, y: number, w: number, h: number, pad?: number): void` - cam: `Camera` - x: `number` - y: `number` - w: `number` - h: `number` - pad (optional): `number` - returns: `void` ```ts export declare function fit(cam: Camera, x: number, y: number, w: number, h: number, pad?: number): void; ``` ## @fzlt/core/sprite Status: COMPLETE. Size: 1437 B gzip (budget 1536 B). Page: https://fizlet.com/bricks/sprite.html Import: `import { … } from "@fzlt/core/sprite"` The character layer's drawing primitive. Pixel-art sprites written as data (rows of characters + a colour map whose values can be palette roles), baked once per (sprite, frame, scale, palette) into a bounded cache, then blitted with one drawImage. `spriteSVG()` / `spriteURL()` give the same pixels as an SVG for characters that live in the page instead of on a canvas; they stand alone, so a page that only places SVG characters never ships the canvas half. ### Paint (type, COMPLETE) A colour for a character: a hex, or a role name (`team`, `enemy`, `hazard`…) a palette fills. ```ts export type Paint = string; ``` ### SpriteDef (interface, COMPLETE) One sprite as data: its frames as rows of characters, the colour or role of each character, its pivot and an optional outline. ```ts export interface SpriteDef { /** Rows of characters, one per pixel (one frame), or a list of those (an animation). */ frames?: string[] | string[][]; /** Character → colour or role. Any character not listed is empty (use `.` by convention). */ colors?: Record; /** Pivot in sprite pixels (default: the centre). Draw places it at (x, y) and rotates about it. */ ax?: number; ay?: number; /** A 1px rim around the silhouette (colour or role): readable on light and dark pages. */ outline?: Paint; /** Vector sprite instead of pixels: its size, frame count and a painter, baked like the rest. */ w?: number; h?: number; n?: number; paint?: (c: CanvasRenderingContext2D, frame: number, color: (p: Paint) => string) => void; } ``` ### SpriteOptions (interface, COMPLETE) Options for `sprites()`: the palettes and the size of the baked-tile cache. ```ts export interface SpriteOptions { /** * Palettes: role → hex. Index 0 is the default and fills roles the others leave out. The key `*` * paints every pixel one colour (a hit flash). Push more at runtime (max 32); `clear()` after * changing one in place. */ palettes?: Record[]; /** Max baked tiles kept (default 256). The oldest is dropped when a new one is baked. */ cap?: number; /** Tile factory (default: OffscreenCanvas, else a detached canvas). */ canvas?: (w: number, h: number) => OffscreenCanvas | HTMLCanvasElement; } ``` ### spriteSVG (function, COMPLETE) One sprite as an SVG string (crisp at any size), for characters placed in the page. Frames wrap. Signature: `spriteSVG(d: SpriteDef, frame?: number, palettes?: Palettes, palette?: number): string` - d: `SpriteDef` - frame (optional): `number` - palettes (optional): `Palettes` - palette (optional): `number` - returns: `string` ```ts export declare function spriteSVG(d: SpriteDef, frame?: number, palettes?: Palettes, palette?: number): string; ``` ### spriteURL (const, COMPLETE) `spriteSVG()` as a data URL, for an `` or a CSS background. Signature: `spriteURL(d: SpriteDef, frame?: number, palettes?: Palettes, palette?: number): string` - d: `SpriteDef` - frame (optional): `number` - palettes (optional): `Palettes` - palette (optional): `number` - returns: `string` ```ts export declare const spriteURL: (d: SpriteDef, frame?: number, palettes?: Palettes, palette?: number) => string; ``` ### sprites (function, COMPLETE) Frames ≤ 64, palettes ≤ 32 and baked scales 1–8 per sprite: the cache key is one small integer. Signature: `sprites(defs: Record, o?: SpriteOptions): Sprites` - defs: `Record` - o (optional): `SpriteOptions` - returns: `Sprites` ```ts export declare function sprites(defs: Record, o?: SpriteOptions): { ids: Record; /** Width and height of each sprite in sprite pixels (outline not included), by id. */ w: Float64Array; h: Float64Array; palettes: Record[]; tile: (id: number | K, frame?: number, scale?: number, palette?: number) => Tile; /** * Draw sprite `id` (number from `ids`, or its name) with its pivot at (x, y), turned by `angle` * radians, `scale` pixels per sprite pixel. Bakes on the first call; after that, no allocation. * An id that names no sprite throws a RangeError (as `tile` does). */ draw(ctx: CanvasRenderingContext2D, id: number | K, frame: number, x: number, y: number, angle?: number, scale?: number, palette?: number): void; /** Baked tiles kept now. */ readonly size: number; /** Tiles baked so far (cache misses). Flat in a steady scene. */ readonly baked: number; /** Drop every baked tile (after changing a palette in place). */ clear(): void; }; ``` ### Sprites (type, COMPLETE) The sprite set `sprites()` returns: `draw()` blits a baked frame. ```ts export type Sprites = ReturnType; ``` ## @fzlt/core/sound Status: COMPLETE. Size: 1062 B gzip (budget 1536 B). Page: https://fizlet.com/bricks/sound.html Import: `import { … } from "@fzlt/core/sound"` The sound adapter. After each frame it reads the engine's event ring and plays the mapped cues, so collisions and game events make sound without knowing about audio. The default voice is a tiny synth (oscillators, no assets); pass `play` to use your own sounds. Silent until the first trusted gesture, while paused, and when switched off. ### Cue (type, COMPLETE) A built-in sound cue the synth can play. ```ts export type Cue = "fire" | "hit" | "boom" | "hurt" | "power" | "ready" | "win" | "end" | "click"; ``` ### CueName (type, COMPLETE) A cue name: a built-in Cue, or one of your own `notes`. ```ts export type CueName = Cue | (string & {}); ``` ### Note (type, COMPLETE) One synth voice: pitch from → to (Hz) over `sec`, after `delay`, its wave and volume. ```ts export type Note = [from: number, to: number, sec: number, delay: number, wave: OscillatorType, vol: number]; ``` ### SoundOptions (interface, COMPLETE) Options for `sound()`: which event types play which cue, on or off, volume, or your own player. ```ts export interface SoundOptions { /** event type → cue, e.g. { shot: "fire", wreck: "boom" }. */ cues: Record; /** Your own cues for the built-in synth: name → voices (a name here wins over a built-in). */ notes?: Record; /** Start switched on (default true). */ on?: boolean; /** Master volume 0..1 (default 0.12). */ volume?: number; /** Your own player instead of the synth. */ play?: (cue: CueName, intensity: number) => void; } ``` ### sound (function, COMPLETE) The sound adapter for engine `e`: after each frame it plays the cue mapped to each event in the ring. Silent until the first trusted gesture and while paused; set `on` to mute, `dispose()` to remove it. Signature: `sound(e: Engine, o: SoundOptions): Sound` - e: `Engine` - o: `SoundOptions` - returns: `Sound` ```ts export declare function sound(e: Engine, o: SoundOptions): { play: (cue: CueName, intensity?: number, at?: number) => void; dispose: () => void; on: boolean; /** The audio clock in seconds (it stands still while suspended or muted), -1 before the first gesture. */ readonly time: number; }; ``` ### Sound (type, COMPLETE) The sound adapter `sound()` returns. ```ts export type Sound = ReturnType; ``` ## @fzlt/core/embed Status: COMPLETE. Size: 2040 B gzip (budget 2560 B). Page: https://fizlet.com/bricks/embed.html Import: `import { … } from "@fzlt/core/embed"` Drop a game onto any page with one small config. palette - hex colours for the Board UI roles (anything else is ignored: no CSS injection) easterEgg - false: open now. true / {...}: stay dormant (tiny listeners only) until a trigger (key sequence, gesture on a selector, click on a selector), then lazy-load the game. toggle - a floating key to reopen / close once unlocked. Escape closes and restores the page. Lifted from Swarm Protocol's loader: keys typed in fields never count, only a position and a timestamp are kept for the sequence (never the typed text), and destroy() removes everything. ### EasterEgg (interface, COMPLETE) What wakes a dormant embed: a key sequence (`"konami"` or your own keys), a gesture or a click on a selector, each limited to desktop, mobile or all; `toggle` adds the on-page toggle key. ```ts export interface EasterEgg { sequence?: "konami" | string[] | false; gesture?: { on: string; type: "hold-tap" | "double-tap" | "long-press"; } | false; click?: string | false; scope?: { sequence?: Scope; gesture?: Scope; click?: Scope; }; toggle?: boolean; } ``` ### EmbedConfig (interface, COMPLETE) The one config an embed takes: its name, the toggle label, the Board UI palette and the easter egg. ```ts export interface EmbedConfig { /** Short id: data--trigger / data--ignore, the toggle label. */ name: string; /** Label on the toggle key (default: name). */ label?: string; palette?: Palette; easterEgg?: boolean | EasterEgg; /** With no easter egg: open right away (default) or wait closed behind the toggle key. */ open?: boolean; } ``` ### Mounted (interface, COMPLETE) A mounted game: `destroy()` removes it and gives the page back. ```ts export interface Mounted { destroy(): void; } ``` ### Mount (type, COMPLETE) Your game's mount function: draws the game into `host` and returns it as `Mounted`. ```ts export type Mount = (host: HTMLElement, config: EmbedConfig) => Mounted; ``` ### embed (function, COMPLETE) Drop a game onto the page: `load` is called (and awaited) only when the game opens, at once or when the easter egg fires. Returns the controls: `open()`, `close()`, `unlock()`, `opened`, `unlocked` and `destroy()`. Signature: `embed(load: () => Promise | Mount, cfg: EmbedConfig): { … }` - load: `() => Promise | Mount` - cfg: `EmbedConfig` - returns: `{ … }` ```ts export declare function embed(load: () => Promise | Mount, cfg: EmbedConfig): { open: () => Promise; close: () => void; unlock: () => void; readonly opened: boolean; readonly unlocked: boolean; /** Remove every listener, the toggle and the game (the game restores the page). */ destroy(): void; }; ``` ## @fzlt/core/slot Status: COMPLETE. Size: 1849 B gzip (budget 2048 B). Page: https://fizlet.com/bricks/slot.html Import: `import { … } from "@fzlt/core/slot"` Mount a Fizlet scene into an Astro page engine slot: the element carrying class="ae-slot" data-slot-target="#" role="img" aria-label="…". Both shapes the engine has rendered are found: the figure itself (`
`), or a box inside one (`
…
`, where the caption sits outside the role="img" box and stays: it is not over the scene). - Lazy: nothing loads or runs until the slot comes near the viewport, and then only once the main thread is idle (or `idle` ms have passed), so the page's own work goes first. - Safe first tap: a tap on the slot before the scene is up loads it at once and is handed to the scene as plain numbers (`first`), never as a stale PointerEvent whose pointer is gone. - Reduced motion: the slot keeps its placeholder and fallback; the scene never loads. If the setting turns on while a scene runs, the scene goes and the fallback comes back. - Accessible: role="img" + aria-label stay unless the scene says it renders real accessible content (`accessible: true`): then role="img" becomes role="figure" (a
's own role, so the label stays on a role that takes one) in place, and Recall puts "img" back in the same spot, byte for byte. Until then the stage is aria-hidden. - Contained: the scene gets its own stage inside the slot (contain: strict). Every change the slot makes goes through a Ledger, so destroy() gives the page back exactly. - Off screen, off the page's frame: the stage is content-visibility: auto, so a scene scrolled away is skipped for style, layout, paint and layers. Without it a 3D scene's composited layers stayed in every repaint elsewhere on the page (Chromium layerizes the whole document). ### SlotScene (interface, COMPLETE) What a slot's mount function returns: the scene, with `destroy()` to remove it. ```ts export interface SlotScene { /** Remove the scene (its engine's destroy() restores what it changed). */ destroy(): void; /** True when the scene renders real accessible content: the slot's role="img" becomes "figure". */ accessible?: boolean; } ``` ### SlotTap (interface, COMPLETE) A tap on the slot before the scene was up: where (px from the slot's top left) and when. ```ts export interface SlotTap { x: number; y: number; /** `performance.now()` when the finger went down. */ at: number; } ``` ### SlotMount (type, COMPLETE) Build the scene inside `stage` (sized to the slot). `slot` is the figure, read-only. `first` is the tap that started the load, if one did: act on it as an intent (aim, fire), not as an event. ```ts export type SlotMount = (stage: HTMLElement, slot: HTMLElement, first: SlotTap | null) => SlotScene; ``` ### SlotOptions (interface, COMPLETE) Options for `slot()`: how near the viewport the slot must come before loading, and the idle wait. ```ts export interface SlotOptions { /** How close to the viewport the slot must come before the scene loads (default "200px"). */ margin?: string; /** Longest wait (ms) for an idle main thread once the slot is near (default 1000; 0: no wait). */ idle?: number; } ``` ### findSlot (const, COMPLETE) The slot for "#" (or null): any `.ae-slot` carrying the target, figure or box. Signature: `findSlot(target: string, doc?: Document): HTMLElement | null` - target: `string` - doc (optional): `Document` - returns: `HTMLElement | null` ```ts export declare const findSlot: (target: string, doc?: Document) => HTMLElement | null; ``` ### slot (function, COMPLETE) Mount a scene into an Astro page engine slot (`target`: its `data-slot-target` or the element). `load` runs once the slot nears the viewport and the main thread is idle (or on the first tap); with reduced motion it never loads. Returns `el`, `mounted` and `destroy()`. Signature: `slot(target: string | HTMLElement, load: () => Promise | SlotMount, opts?: SlotOptions): { … }` - target: `string | HTMLElement` - load: `() => Promise | SlotMount` - opts (optional): `SlotOptions` - returns: `{ … }` ```ts export declare function slot(target: string | HTMLElement, load: () => Promise | SlotMount, opts?: SlotOptions): { /** The slot figure, or null when no slot matches the target. */ el: HTMLElement | null; readonly mounted: boolean; /** Remove the scene, the stage and every listener; the slot is back as the engine drew it. */ destroy(): void; }; ``` ## @fzlt/core/hud Status: BETA. Size: 1306 B gzip (budget 1536 B). Page: https://fizlet.com/bricks/hud.html Import: `import { … } from "@fzlt/core/hud"` Change-only HUD bindings that never allocate in the steady state. A binding reads a number (or a flag) every render, compares it as a number after rounding to what the readout shows, and formats and writes only when that changed. Feeds and banners react to the event ring (`engine.events`), never to polling. Every write goes through `engine.ledger`, so `destroy()` gives the page back. Render phase only: no layout reads, nothing in the step. No visuals of its own: it binds to the game's markup (Board UI or anything else). ### Fmt (type, BETA) How a number shows: `int` 12 · `clock` 3:05 (seconds) · `rank` #2 · `pct` 75% (0..1 in), or your own. ```ts export type Fmt = "int" | "clock" | "rank" | "pct" | ((v: number) => string); ``` ### Src (type, BETA) Where a number comes from: a getter, or a slot in a typed array (`[state, 3]`). A getter V8 doesn't inline boxes a fractional double on every call (16 bytes); a slot never does, so it is the safe choice for running fractions (clock, hp, heat). Whole numbers and booleans never box. ```ts export type Src = (() => number) | readonly [ArrayLike, number]; ``` ### hud (function, BETA) Change-only HUD bindings on `engine`, finding readouts in `root` (default `document`): `num`, `meter`, `flag`, `feed` and `banner`. Values are read every render and written only when what shows changes; every write goes through the ledger. Signature: `hud(engine: Engine, root?: ParentNode): Hud` - engine: `Engine` - root (optional): `ParentNode` - returns: `Hud` ```ts export declare function hud(engine: Engine, root?: ParentNode): { /** A number readout: written only when its shown value changes. */ num: (t: Target, get: Src, fmt?: Fmt) => /*elided*/ any; /** A bar: `width` in whole percent (0..1 in), `aria-valuenow` on a progressbar, class `low` at or under `low`. */ meter: (t: Target, get: Src, low?: number) => /*elided*/ any; /** A class that follows a condition. */ flag: (t: Target, cls: string, get: () => boolean) => /*elided*/ any; /** A list of the last `rows` events of a type, newest first (`play` runs on the newest row). */ feed: (list: Target, rows: number, on: string, line: (a: number, b: number) => string, play?: (row: HTMLElement) => void) => /*elided*/ any; /** Title and sub from an event; `play` (re)starts the game's own animation. */ banner: (t: Target, on: string, text: (a: number, b: number) => readonly [string, string?], play?: (el: HTMLElement) => void) => /*elided*/ any; /** The render system (already added to the engine; `engine.remove(system)` unbinds everything). */ system: System; }; ``` ### Hud (type, BETA) The HUD `hud()` returns. ```ts export type Hud = ReturnType; ``` ## @fzlt/core/match Status: BETA. Size: 2617 B gzip (budget 2816 B). Page: https://fizlet.com/bricks/match.html Import: `import { … } from "@fzlt/core/match"` The match flow every game shares, intro → count → play → paused → end, on the Board UI kit's overlay (grey dim, a sticker that slams, at most two sticker keys). One overlay is built once; each phase only flips attributes, styles and text through the ledger, so destroy() gives the page back exactly. Every transition is an event on the ring (`match:play`, …), so the HUD, sound and coach layers react without knowing about this one. Pass `null` as the root to run the phase machine headless (tests, a server). ### Phase (type, BETA) Where a match is: the intro, the countdown, play, paused or the end. ```ts export type Phase = "intro" | "count" | "play" | "paused" | "end"; ``` ### Sticker (type, BETA) A sticker: kicker (small, blinking) and word (big). ```ts export type Sticker = [kicker: string, word: string]; ``` ### MatchKey (type, BETA) The overlay's sticker keys, by what they do. ```ts export type MatchKey = "start" | "resume" | "surrender" | "again" | "next"; ``` ### MatchLabels (interface, BETA) Every word on the match overlay: the stickers per phase, the countdown kicker and the key labels. ```ts export interface MatchLabels extends Record { intro: Sticker; /** The kicker over the 3-2-1. */ count: string; paused: Sticker; won: Sticker; lost: Sticker; /** Lost by surrender (the white flag). */ flag: Sticker; } ``` ### MatchResult (interface, BETA) How a match ended: won or lost, why, and the clock. ```ts export interface MatchResult { won: boolean; /** "time" (the clock ran out), "flag" (surrender) or what the game passed to finish(). */ why: string; /** The clock when it ended. */ clock: number; } ``` ### MatchOptions (interface, BETA) Options for `match()`: the countdown, the match length, labels and key icons, the pause keys. ```ts export interface MatchOptions { /** Countdown beats of one second before play (default 3; 0 skips it). */ count?: number; /** Match length in seconds; 0 or left out = no time limit (the clock counts up). */ seconds?: number; labels?: Partial; /** SVG markup per key (64 × 64), e.g. the game's skull on KEEP FIGHTING. */ icons?: Partial>; /** Pause / resume keys (KeyboardEvent.code; default Escape and P). [] turns them off. */ keys?: string[]; /** A new round starts (start, rematch): reset the game. Runs in the click (user activation). */ begin?(): void; /** The match ended. Return a sticker to replace the default words (e.g. ["INSERT COIN", "RED WINS"]). */ end?(r: MatchResult): Sticker | void; /** The clock ran out: did the player win? (default: no) */ timeUp?(): boolean; /** The kit's launch burst (`launch` from @fzlt/core/ui) for PRESS START and SURRENDER. */ launch?(key: HTMLElement, action: () => void, o: { reduced?: boolean; }): void; /** Each phase change, once it is painted (in the call that made it, or in the render of the frame * a step made it in): a host that owns more than the overlay (@fzlt/core/arcade's skirmish preset) * follows the phase here. Not called for the first intro. */ on?(phase: Phase): void; /** Phases that show no overlay (the phase machine and its events still run), e.g. ["paused"]. */ hide?: Phase[]; } ``` ### match (function, BETA) The shared match flow on engine `e`, intro → count → play → paused → end, on the Board UI overlay in `root`. Every transition is an event on the ring (`match:play`, …). Drive it with `start()`, `pause()`, `finish()`, `restart()`; read `phase` and `clock`. Signature: `match(e: Engine, root: HTMLElement | null, o?: MatchOptions): Match` - e: `Engine` - root: `HTMLElement | null` - o (optional): `MatchOptions` - returns: `Match` ```ts export declare function match(e: Engine, root: HTMLElement | null, o?: MatchOptions): { readonly phase: Phase; /** Seconds left (timed match) or played so far (no limit). Counts only in play. */ readonly clock: number; /** The overlay element (null headless): add your own content, style it by `[data-phase]`. */ el: HTMLElement | null; /** PRESS START: from the intro (or the result), begin a round. */ start(): void; /** Pause (true), resume (false) or toggle (no argument). Only during play. */ pause(on?: boolean): void; /** End the match now (a win, a loss, "flag" for surrender). */ finish(w: boolean, y?: string): void; /** A new round from any phase: begin(), the clock reset, then the countdown. */ restart(): void; /** Back to the start screen (NEW GAME). */ intro(): void; system: System; }; ``` ### Match (type, BETA) The match flow `match()` returns. ```ts export type Match = ReturnType; ``` ## @fzlt/core/board-page Status: COMPLETE. Size: 4103 B gzip (budget 4160 B). Page: https://fizlet.com/bricks/board-page.html Import: `import { … } from "@fzlt/core/board-page"` The host page as the board. Its text glyphs become terrain and food, its buttons become targets. Geometry is measured only in the read phase, on a budget (Range reads capped per refresh; 12.5 Hz while the page moves, 2 Hz once it is still), kept in typed arrays and indexed with a collide Grid, so `near` / `hit` in a step never read layout or allocate. `consume` writes a literal 0/1 into the page's own Text node through the ledger (no wrappers, no clones); from a step it is queued and written in render. Recall gives every glyph back, and a newer edit by the host page wins. Coordinates are viewport CSS px (getBoundingClientRect). Glyph indices hold for a frame: a rescan (read phase, after the page changed) renumbers them. Targets only (`text: false`) with `still: Infinity`: a scene that reads page elements (headings, buttons) re-measures only when the page moves or changes, so its still frames read nothing. ### BoardPageOptions (interface, COMPLETE) Options for `boardPage()`: which part of the page is the board, which elements are text and targets, and the measuring budget. ```ts export interface BoardPageOptions { /** The part of the page that is the board (default document.body). The board lives in its document * and window (an iframe's page works too). */ root?: Element; /** Text containers (default headings, p, li, blockquote, [data-fizlet-text]); 160 at most. false: no glyphs, targets only. */ text?: string | false; /** Targets (default button, [role=button], a.button, a.btn, [data-fizlet-target]). */ targets?: string; /** More to leave alone, on top of fields, editable, code, live regions, dialogs, [data-fizlet-ignore]. */ exclude?: string; /** Glyphs indexed at most (default 8000). */ maxGlyphs?: number; /** Targets at most (default 32). */ maxTargets?: number; /** Letters only (default false: letters and digits are glyphs). */ letters?: boolean; /** Range reads per refresh at most (default 900); the rest are measured on the next refresh. */ reads?: number; /** Seconds between re-measures while the page is still (default 0.5; Infinity: only after a scroll, resize or change). */ still?: number; /** Hex colour for consumed glyphs (a CSS Custom Highlight, where supported). Off by default. */ color?: string; } ``` ### BoardTarget (interface, COMPLETE) One target on the board (a button or link): its element and its viewport rect, measured in read. ```ts export interface BoardTarget { readonly el: Element; /** Viewport rect, measured in read; w = h = 0 while hidden or off screen. */ x: number; y: number; w: number; h: number; /** The game's to use: starts at 100 and survives a rescan. */ hp: number; } ``` ### boardPage (function, COMPLETE) The host page as the board for engine `e`: its glyphs become terrain and food, its buttons targets. Measured only in the read phase, on a budget; `near()` and `hit()` in a step never read layout or allocate. `consume()` changes a glyph through the ledger. Signature: `boardPage(e: Engine, o?: BoardPageOptions): BoardPage` - e: `Engine` - o (optional): `BoardPageOptions` - returns: `BoardPage` ```ts export declare function boardPage(e: Engine, o?: BoardPageOptions): { glyphs: { /** Glyphs indexed, spent ones included (w = 0 while not on screen). */ readonly count: number; x: Float32Array; y: Float32Array; w: Float32Array; h: Float32Array; /** 1 once consumed. */ spent: Uint8Array; }; /** Page targets (rects measured in read). */ targets: readonly BoardTarget[]; /** Unspent on-screen glyphs touching the circle, into `out`. Returns how many. */ near(px: number, py: number, pr: number, out: Int32Array): number; /** `near` for hot paths: the circle is p[i], p[i + 1] (centre) and p[i + 2] (radius), so no * number is passed (a number passed to a call V8 doesn't inline boxes: one allocation). */ nearAt: (p: Float64Array, i: number, out: Int32Array) => number; /** First unspent glyph the segment touches (2 px pad), or -1; how far along it (0..1) is `t`. */ hit(x0: number, y0: number, x1: number, y1: number): number; /** `hit` for hot paths: the segment is p[i .. i + 3] (x0, y0, x1, y1); how far along it the * glyph is goes in p[i + 4] (and `t`). No number is passed, so nothing boxes. */ hitAt(p: Float64Array, i: number): number; readonly t: number; /** Rescans so far: glyph indices from before a rescan mean nothing after it. */ readonly scans: number; /** Range reads made by the last refresh (never more than `reads`). */ readonly reads: number; /** * Consume glyph i: a 0/1 ("binary": the board's random draw; "0" / "1": the game's) or a no-break * space ("erase") in the page's own text, through the ledger; from a step it is written in * render. False if spent or not ours anymore. */ consume(i: number, as?: "binary" | "erase" | "0" | "1"): boolean; /** Glyph i as it was before the game changed it (the letter a bite ate). Not for a step: a string. */ char: (i: number) => string; /** The element that holds glyph i's text (for a game's own read-phase checks, e.g. what covers it). */ parent: (i: number) => HTMLElement | null; /** Rescan and re-measure now; from a step, at the next read. */ refresh(): void; system: System; }; ``` ### BoardPage (type, COMPLETE) The board `boardPage()` returns. ```ts export type BoardPage = ReturnType; ``` ## @fzlt/core/presence Status: BETA. Size: 2197 B gzip (budget 2240 B). Page: https://fizlet.com/bricks/presence.html In-house / experimental: Used in our games. Not a supported multiplayer service. Build your own room server: https://fizlet.com/guides/build-your-own-room-server/ Import: `import { … } from "@fzlt/core/presence"` Lockstep input exchange for multiplayer, behind a tiny transport adapter. Every peer runs the same fixed-step simulation from the same seed. Each sim frame needs every player's input for that frame; your own input is sent `delay` frames ahead, so on a good link the game never waits. A missing input stalls the sim (never guesses), so peers can't drift apart. Inputs live in fixed rings of typed arrays: a confirmed per-frame input log a rollback layer can replay. State hashes are exchanged every `hashEvery` frames to catch a desync early. ### INPUT, HASH, WELCOME, LEAVE, RESUME, AWAY, START, LOBBY (const, BETA) Wire kinds. Every message is 12 bytes: kind, slot, players, mode, u32 frame (or seed), u32 value (or token). From the room: WELCOME, LEAVE, RESUME (a seat is back: resend), AWAY (a seat dropped and may come back), LOBBY (before the start: your seat, room size, seated); START goes to the room from the host (the protocol: /guides/build-your-own-room-server/). ```ts export declare const INPUT = 1, HASH = 2, WELCOME = 3, LEAVE = 4, RESUME = 5, AWAY = 6, START = 7, LOBBY = 8; ``` ### Transport (interface, BETA) Moves 12-byte messages to the room (or the other peers) and back. ```ts export interface Transport { /** Send one message. The buffer is reused: copy it before returning. */ send(data: Uint8Array): void; /** Deliver every incoming message to `fn` (messages that arrived earlier are flushed to it). */ listen(fn: ((data: Uint8Array) => void) | null): void; close(): void; } ``` ### PresenceOptions (interface, BETA) Options for `presence()`: the transport, this peer's seat, the number of players, the input delay and the ring sizes. ```ts export interface PresenceOptions { transport: Transport; /** This peer's seat, 0..players-1 (the room says which). */ slot: number; players: number; /** Frames of input delay (default 2: 33 ms at 60 Hz). */ delay?: number; /** Input frames kept per player (default 64; at least 2 × delay + 2). */ cap?: number; /** This player's input for the frame being scheduled, as bits (read latched intents here). */ input(): number; /** The simulation for one frame: same inputs + same seed → same state on every peer. */ step(e: Engine, inputs: Uint32Array, frame: number): void; /** A hash of the sim state (see `hash32`); compared across peers to catch a desync. */ hash?(): number; /** Frames between hash checks (default 30). */ hashEvery?: number; } ``` ### presence (function, BETA) Lockstep multiplayer on engine `e`: each sim frame waits for every player's input (sent `delay` frames ahead), so peers never drift; state hashes are compared to catch a desync. Read `frame`, `stalls`, `desync` and `lost`; `destroy()` stops it. Signature: `presence(e: Engine, o: PresenceOptions): Presence` - e: `Engine` - o: `PresenceOptions` - returns: `Presence` ```ts export declare function presence(e: Engine, o: PresenceOptions): { system: System; /** Sim frames run so far (every peer passes the same frames, maybe at different times). */ readonly frame: number; /** Engine steps spent waiting for someone's input. */ readonly stalls: number; /** Messages refused (out of the window, bad seat, a second input or hash for one frame). */ readonly dropped: number; /** First frame whose state hash differed from a peer's, or -1. */ readonly desync: number; /** Bitmask of seats the room said left (their frames never arrive: the sim waits). */ readonly lost: number; /** Bitmask of seats reconnecting (dropped, may come back; this one too, while `join`'s link retries). */ readonly away: number; /** The confirmed input of `slot` for `frame`, or -1 if it isn't known (yet, or any more). */ confirmed(slot: number, frame: number): number; destroy: () => void; }; ``` ### Presence (type, BETA) The lockstep session `presence()` returns. ```ts export type Presence = ReturnType; ``` ### hash32 (function, BETA) FNV-1a over bytes: hash a byte view of your state's typed arrays (make the view once). Signature: `hash32(bytes: Uint8Array, h?: number): number` - bytes: `Uint8Array` - h (optional): `number` - returns: `number` ```ts export declare function hash32(bytes: Uint8Array, h?: number): number; ``` ### memory (function, BETA) In-memory hub: `peers` transports that broadcast to each other (like the room relay) after `latency` ± `jitter` ticks. Call `tick()` once per frame. Bounded: `cap` messages in flight. Signature: `memory(o: { peers: number; latency?: number; jitter?: number; seed?: number; cap?: number; }): { … }` - o: `{ peers: number; latency?: number; jitter?: number; seed?: number; cap?: number; }` - returns: `{ … }` ```ts export declare function memory(o: { peers: number; latency?: number; jitter?: number; seed?: number; cap?: number; }): { ends: Transport[]; /** Advance the link clock one tick and deliver what's due (later messages may overtake). */ tick(): void; readonly queued: number; readonly dropped: number; }; ``` ### websocket (function, BETA) Wrap an open-or-opening WebSocket as a transport. Messages before `listen` are kept (≤ 256). `send` takes presence's 12-byte messages (the room refuses any other size). `eat` (join's) sees each message first and keeps it from the listener when it returns true. Signature: `websocket(ws: WebSocket, eat?: (d: Uint8Array) => unknown): Transport` - ws: `WebSocket` - eat (optional): `(d: Uint8Array) => unknown` - returns: `Transport` ```ts export declare function websocket(ws: WebSocket, eat?: (d: Uint8Array) => unknown): Transport; ``` ### JoinOptions (interface, BETA) Options for `join()`. ```ts export interface JoinOptions { /** Reconnect tries after a drop, 250 ms apart and doubling (default 6: about 16 s; 0: off). Meanwhile * `away` has this seat's bit (the others see AWAY); when they run out, `lost` (LEAVE). */ retry?: number; /** Called on each LOBBY from the room before the start (on every join and leave): this player's * `seat`, how many are `seated`, the room `size`. Seat 0 is the host: its `start(seats, mode)` asks * the room to begin once `seats` are taken (0: now) with the seats it has; `mode` (0..255) reaches * every peer. Other seats' starts are ignored by the room. */ lobby?(start: (seats?: number, mode?: number) => void, seat: number, seated: number, size: number): void; } ``` ### join (function, BETA) Join a room (a server speaking the `fizlet-rooms` subprotocol): resolves when the room starts with your seat, the player count, the shared seed and the host's `mode`. `protocols` go to the WebSocket handshake: the room reads its access token there (`["fizlet-rooms", "invite." + code]`), so the code stays out of the URL and the logs. A dropped link (1006, or a server restart: 1011, 1012, 1014) reconnects on its own (`o.retry`) with the seat's resume token, kept in memory only (never in a URL); the room's own closes (idle, full, seat gone, the caps) end it. Signature: `join(url: string, signal?: AbortSignal, protocols?: string | string[], o?: JoinOptions): Promise<{ transport: Transport; slot: number; players: number; seed: number; mode: number; }>` - url: `string` - signal (optional): `AbortSignal` - protocols (optional): `string | string[]` - o (optional): `JoinOptions` - returns: `Promise<{ transport: Transport; slot: number; players: number; seed: number; mode: number; }>` ```ts export declare function join(url: string, signal?: AbortSignal, protocols?: string | string[], o?: JoinOptions): Promise<{ transport: Transport; slot: number; players: number; seed: number; mode: number; }>; ``` Import: `import { … } from "@fzlt/core/presence/check"` The determinism check as one function any game can call from a test. It runs `players` copies of your sim as lockstep clients over the in-memory link (each with only its own input), hashes the state after every frame, and reports the first frame that differs. ### SyncGame (interface, BETA) Your sim as `syncCheck()` runs it: its input for the next frame, one step from every player's inputs, and a byte view of its whole state. ```ts export interface SyncGame { /** This player's input bits for the next scheduled frame (only this client knows them). */ input(): number; /** One sim frame from every player's inputs. Use `e.random`, never Math.random or the clock. */ step(e: Engine, inputs: Uint32Array, frame: number): void; /** A byte view of the whole sim state (typed arrays; make the view once). */ state: Uint8Array; } ``` ### SyncOptions (interface, BETA) Options for `syncCheck()`: players, frames, seed, input delay and the simulated link. ```ts export interface SyncOptions { players?: number; frames?: number; seed?: number; delay?: number; latency?: number; jitter?: number; } ``` ### SyncResult (interface, BETA) What `syncCheck()` found: whether every client agreed, how far they got and the first frame that differed. ```ts export interface SyncResult { /** Every client reached `frames` with the same state hash on every frame. */ ok: boolean; /** Frames every client completed (≤ frames). */ frames: number; /** First frame whose state differed between clients, or -1. */ diverged: number; /** Engine steps spent waiting for inputs, summed over clients. */ stalls: number; } ``` ### syncCheck (function, BETA) Check a sim is deterministic: run `players` copies as lockstep clients over the in-memory link, hash the state every frame and report the first frame that differs (`diverged`, -1 if none). Signature: `syncCheck(setup: (e: Engine, slot: number, players: number) => SyncGame, o?: SyncOptions): SyncResult` - setup: `(e: Engine, slot: number, players: number) => SyncGame` - o (optional): `SyncOptions` - returns: `SyncResult` ```ts export declare function syncCheck(setup: (e: Engine, slot: number, players: number) => SyncGame, o?: SyncOptions): SyncResult; ``` ## @fzlt/core/ui Status: COMPLETE. Size: 2907 B gzip (budget 3072 B). Page: https://fizlet.com/bricks/ui.html Import: `import { … } from "@fzlt/core/ui"` The tiny behaviour half of board-ui.css. No dependencies, no globals, every listener bound to the caller's AbortSignal. Spec: board-ui/BOARD-UI.md. ### Palette (type, COMPLETE) The only per-game input: a palette. Any key left out keeps the kit default. ```ts export type Palette = Partial<{ bg: string; surface: string; raised: string; card: string; ink: string; muted: string; line: string; rim: string; sys: string; cmd: string; fire: string; aim: string; attack: string; advance: string; fallin: string; flag: string; go: string; teamA: string; teamB: string; hurt: string; critical: string; /** Text / thin-line twins for the light theme (defaults to the face colour). */ sysInk: string; cmdInk: string; fireInk: string; }>; ``` ### mount (function, COMPLETE) Mount a board inside a shadow root on `host`, isolated both ways: the kit can't touch the page and the page's CSS (a global `button {}` rule, a reset) can't touch the board. Pass the kit CSS text (e.g. `import css from "board-ui/board-ui.css?raw"`). Returns the `.bui` root to build in. Stylesheet ownership: the kit owns one constructed sheet per CSS text (keyed by the text), so games composed independently with different CSS each get their own, and boards with the same text share one. Treat it as read-only; it is appended to the shadow root's adoptedStyleSheets, never replacing sheets you adopted there yourself. Signature: `mount(host: HTMLElement, css: string, opts?: { theme?: "dark" | "light"; side?: 0 | 1; palette?: Palette; }): HTMLDivElement` - host: `HTMLElement` - css: `string` - opts (optional): `{ theme?: "dark" | "light"; side?: 0 | 1; palette?: Palette; }` - returns: `HTMLDivElement` ```ts export declare function mount(host: HTMLElement, css: string, opts?: { theme?: "dark" | "light"; side?: 0 | 1; palette?: Palette; }): HTMLDivElement; ``` ### applyPalette (function, COMPLETE) Paint a board: sets the palette's CSS variables on the .bui root. Invalid colours are ignored (an embed config can never inject CSS). Returns a function that restores the old values. Signature: `applyPalette(root: HTMLElement, palette: Palette): () => void` - root: `HTMLElement` - palette: `Palette` - returns: `() => void` ```ts export declare function applyPalette(root: HTMLElement, palette: Palette): () => void; ``` ### readouts (function, COMPLETE) Readouts (BOARD-UI.md section 18): a `.bui-readouts` group of `[label, tone?]` items, appended to `parent`. Returns the value elements in order, for @fzlt /core/hud (or your own change-only writes). Text is set with textContent. The caller owns the group: add it through the engine's ledger. Signature: `readouts(parent: HTMLElement, items: [label: string, tone?: "team" | "enemy"][], onMap?: boolean): { group: HTMLDivElement; values: HTMLElement[]; }` - parent: `HTMLElement` - items: `[label: string, tone?: "team" | "enemy"][]` - onMap (optional): `boolean` - returns: `{ group: HTMLDivElement; values: HTMLElement[]; }` ```ts export declare function readouts(parent: HTMLElement, items: [label: string, tone?: "team" | "enemy"][], onMap?: boolean): { group: HTMLDivElement; values: HTMLElement[]; }; ``` ### press (function, COMPLETE) A press that fires once per touch, mouse or keyboard activation, and never loses to a second finger already on the stick. Touch fires on touchstart (no 300ms wait); the click that may follow an uncancellable touch is skipped. Run your state change and repaint synchronously in fn: a frame between release and the new pressed state makes the key bounce. Signature: `press(el: HTMLElement, fn: () => void, signal: AbortSignal): void` - el: `HTMLElement` - fn: `() => void` - signal: `AbortSignal` - returns: `void` ```ts export declare function press(el: HTMLElement, fn: () => void, signal: AbortSignal): void; ``` ### pressCue (function, COMPLETE) The press cue for page keys (`.bui-key`, links and buttons) under `root`. Mouse and touch press through CSS `:active`; this adds the rest: keyboard Enter (and Space on a button) shows the pressed face (`.down`) until the key is released or loses focus, and one passive touchstart listener lets iOS Safari apply `:active` at all. Activation stays native (click, navigation). Signature: `pressCue(root: HTMLElement | Document, signal?: AbortSignal): void` - root: `HTMLElement | Document` - signal (optional): `AbortSignal` - returns: `void` ```ts export declare function pressCue(root: HTMLElement | Document, signal?: AbortSignal): void; ``` ### PERF_KEY (const, COMPLETE) localStorage key of the Settings "Performance overlay" switch (off by default). Type: `"dom-virus:perf"` ```ts export declare const PERF_KEY = "dom-virus:perf"; ``` ### setting (function, COMPLETE) A Settings switch: a key that is `aria-pressed` (the kit's locked state) while on, remembered in localStorage under `key` as "1" / "0". Storage that throws (private mode, blocked cookies, a sandboxed frame) is ignored: the switch still works, it just forgets. `change` runs once with the stored value and on every flip; `set(on)` flips it from code (a keyboard shortcut). Example: `setting(btn, PERF_KEY, (on) => overlay.show(on), signal)` with `@fzlt/core/perf`. Signature: `setting(button: HTMLElement, key: string, change: (on: boolean) => void, signal: AbortSignal, fallback?: boolean): { readonly on: boolean; set: (next: boolean, save?: boolean) => void; }` - button: `HTMLElement` - key: `string` - change: `(on: boolean) => void` - signal: `AbortSignal` - fallback (optional): `boolean` - returns: `{ readonly on: boolean; set: (next: boolean, save?: boolean) => void; }` ```ts export declare function setting(button: HTMLElement, key: string, change: (on: boolean) => void, signal: AbortSignal, fallback?: boolean): { readonly on: boolean; set: (next: boolean, save?: boolean) => void; }; ``` ### slam (function, COMPLETE) Restart an overlay sticker's slam and press (on every entry, and on return to a hidden tab). Signature: `slam(sticker: HTMLElement | null): void` - sticker: `HTMLElement | null` - returns: `void` ```ts export declare function slam(sticker: HTMLElement | null): void; ``` ### toast (function, COMPLETE) Show the comms toast for a while (it never stacks: a new line replaces the old). Signature: `toast(el: HTMLElement, text: string, ms?: number): void` - el: `HTMLElement` - text: `string` - ms (optional): `number` - returns: `void` ```ts export declare function toast(el: HTMLElement, text: string, ms?: number): void; ``` ### launch (function, COMPLETE) The launch burst: the key presses into its base and flips to its inverse, then shatters into spinning chunks with a flash, a round shock ring and square debris that falls away (the hub hero's explosion). The action runs synchronously in the click (mobile full screen keeps its user activation); the burst lives on , so the layout the action causes can't move it. Never cancel a burst from the action itself (see BOARD-UI.md, "reset cancels the burst"). Signature: `launch(button: HTMLElement, action: () => void, opts?: { reduced?: boolean; tones?: string[]; done?: () => void; }): void` - button: `HTMLElement` - action: `() => void` - opts (optional): `{ reduced?: boolean; tones?: string[]; done?: () => void; }` - returns: `void` ```ts export declare function launch(button: HTMLElement, action: () => void, opts?: { reduced?: boolean; tones?: string[]; done?: () => void; }): void; ``` ## @fzlt/core/perf Status: COMPLETE. Size: 2019 B gzip (budget 2022 B). Page: https://fizlet.com/bricks/perf.html Import: `import { … } from "@fzlt/core/perf"` A live performance overlay for players, off until they switch it on. Frames per second, a rolling frame-time bar chart (one bar per frame, coloured against the budget), dropped frames, the engine's work per frame (read, steps, render), live pooled entities and, where the browser exposes it, the JS heap. It reads the engine's own frames on the shared clock (`engine.timing`, `engine.live`): no clock or requestAnimationFrame of its own, and it never calls `engine.busy()`, so a settling engine still settles and the chart rests with it. Its own cost stays out of what it measures: frame times go into a typed-array ring (nothing is allocated per frame), the numbers are written twice a second through the ledger (change-only, strings cached), the chart is one canvas, and the root is `contain: layout paint style` and `pointer-events: none`. Hidden, it only records (a few typed-array writes a frame). Not the dev-only `@fzlt/core/debug`, which patches Element prototypes: this one ships to players. ### Corner (type, COMPLETE) A corner of the host: top left, top right, bottom left or bottom right. ```ts export type Corner = "tl" | "tr" | "bl" | "br"; ``` ### PerfOptions (interface, COMPLETE) Options for `perf()`: where the overlay goes, the toggle key and the frame budget. ```ts export interface PerfOptions { /** Where the overlay goes (default the engine root, else `document.body`, where it is fixed). */ host?: HTMLElement | null; /** Corner of the host (default "tr"). */ corner?: Corner; /** `KeyboardEvent.code` that toggles it (default "F9"); null for none. */ key?: string | null; /** Shown from the start (default false: players switch it on in Settings). */ on?: boolean; /** The frame budget in ms (default 1000 / 60): a frame over 1.5 budgets counts as dropped. */ budget?: number; /** Called when it is shown or hidden (the key or `show()`): keep a Settings switch in step. */ change?: (on: boolean) => void; } ``` ### BARS (const, COMPLETE) Bars in the chart: one per frame, 2 s at 60 fps. Type: `120` ```ts export declare const BARS = 120; ``` ### WINDOW (const, COMPLETE) How often the numbers update (ms of frame time). Type: `500` ```ts export declare const WINDOW = 500; ``` ### FrameRing (class, COMPLETE) The last `cap` frame times (ms) in a typed array: `push` never allocates. Signature: `new FrameRing(cap: number): FrameRing` - cap: `number` - returns: `FrameRing` ```ts export declare class FrameRing { readonly cap: number; readonly ms: Float32Array; /** The next slot written. */ head: number; size: number; constructor(cap: number); push(ms: number): void; /** Slot of the k-th oldest frame held. */ slot(k: number): number; } ``` ### fps (const, COMPLETE) Frames per second from a frame count and the ms they took (the readout inlines the same sum). Signature: `fps(frames: number, ms: number): number` - frames: `number` - ms: `number` - returns: `number` ```ts export declare const fps: (frames: number, ms: number) => number; ``` ### missed (const, COMPLETE) Frames a long frame swallowed: 0 under 1.5 budgets, else the extra budgets it took (rounded). Signature: `missed(ms: number, budget: number): number` - ms: `number` - budget: `number` - returns: `number` ```ts export declare const missed: (ms: number, budget: number) => number; ``` ### perf (function, COMPLETE) A performance overlay for `engine`, off until shown (F9 by default): fps, a frame-time chart against the budget, dropped frames, the engine's work per frame, pooled entities and the JS heap. `show()` toggles it; `destroy()` removes it. Signature: `perf(engine: Engine, o?: PerfOptions): { … }` - engine: `Engine` - o (optional): `PerfOptions` - returns: `{ … }` ```ts export declare function perf(engine: Engine, o?: PerfOptions): { el: HTMLElement; ring: FrameRing; /** Show (true), hide (false) or flip (no argument). */ show: (next?: boolean) => void; readonly on: boolean; /** Frames recorded (0-length frames after a pause are not frames). */ readonly frames: number; /** Dropped frames so far. */ readonly dropped: number; /** Remove the overlay, its key and its system; the engine keeps running. */ destroy: () => void; }; ``` ## @fzlt/core/rules Status: COMPLETE. Size: 1318 B gzip (budget 1536 B). Page: https://fizlet.com/bricks/rules.html Import: `import { … } from "@fzlt/core/rules"` The rules every arcade game repeats, as three small systems on the kernel: - `waves`: a wave clock, prepare → assault → rebuild, with a spawn budget per wave; - `drops`: pickups that fall, wait to be collected and expire; - `score`: points, a combo that decays, and levels from XP thresholds. Each is DOM-free, steps without allocating (state lives in typed arrays) and says what happened on the event ring (`wave:start`, `wave:clear`, `drop:spawn`, `drop:land`, `drop:collect`, `drop:expire`, `score`, `combo:end`, `level`), so the HUD, sound, fx and coach layers react without reaching into the game. Defaults are the DOM VIRUS core's (WaveDirector, SupplyDrops) and Null Invaders' numbers, so a port keeps its feel. The game still decides where and what to spawn. Call `collect`, `add` and `miss` from a step, like the rest of the game: their events go on that frame's ring. ### WavePhase (type, COMPLETE) Where the wave clock is: prepare (before a wave), assault (spawning) or rebuild (between waves). ```ts export type WavePhase = "prepare" | "assault" | "rebuild"; ``` ### WaveOptions (interface, COMPLETE) Options for `waves()`: how many are alive now, the cap, the phase lengths and each wave's size, limit and spawn gap. ```ts export interface WaveOptions { /** Alive now. A wave clears when it has spawned everything and this reaches 0. */ alive(): number; /** Most alive at once, whatever the wave (default 32). */ cap?: number; /** Seconds before wave 1 (default 4) and between waves (default 8). */ prepare?: number; rebuild?: number; /** Enemies in wave n (default min(40, 5 + 5n)). */ size?(wave: number): number; /** Most alive at once in wave n, before `cap` (default 8 + 4n). */ limit?(wave: number): number; /** Seconds between two spawns in wave n (default max(0.7, 2.1 − 0.12n)). */ gap?(wave: number): number; } ``` ### waves (function, COMPLETE) A wave clock on engine `e`, prepare → assault → rebuild, with a spawn budget per wave. It says when to spawn; the game decides where and what. Events: `wave:start`, `wave:clear`. Signature: `waves(e: Engine, o: WaveOptions): { … }` - e: `Engine` - o: `WaveOptions` - returns: `{ … }` ```ts export declare function waves(e: Engine, o: WaveOptions): { system: System; readonly wave: number; readonly phase: WavePhase; /** Enemies this wave still has to spawn. */ readonly left: number; /** Seconds until the next wave starts (prepare, rebuild). */ readonly countdown: number; /** Most alive at once in this wave. */ readonly limit: number; /** Spawn one now? Assault, enemies left, the gap has passed and the board is under the limit. */ ready(): boolean; /** One spawned: count it and wait the gap. */ spawned(): void; reset: () => undefined; }; ``` ### DropOptions (interface, COMPLETE) Options for `drops()`: the pickup kinds, the cap, the timer for timed drops, their life and fall. ```ts export interface DropOptions { kinds: readonly K[]; /** Most on the board at once (default 6). */ cap?: number; /** Seconds between timed drops; 0 or left out: only `spawn()` drops. */ every?: number; /** Where the next timed drop lands: write x, y into `out`. Return false to skip this one. */ place?(out: Float64Array): boolean | void; /** Kind of the n-th timed drop, as an index into `kinds` (default: in turn). */ pick?(n: number): number; /** Seconds a drop lasts (default 14), how far it falls (default 60 px) and how fast (default 90 px/s). */ life?: number; fall?: number; speed?: number; } ``` ### drops (function, COMPLETE) Pickups on engine `e` that fall, wait to be collected and expire, from typed arrays. Call `collect()` from a step. Events: `drop:spawn`, `drop:land`, `drop:collect`, `drop:expire`. Signature: `drops(e: Engine, o: DropOptions): { … }` - e: `Engine` - o: `DropOptions` - returns: `{ … }` ```ts export declare function drops(e: Engine, o: DropOptions): { system: System; /** Positions and kinds of the drops on the board, for drawing: index 0 to `count` − 1. */ x: Float64Array; y: Float64Array; kind: Uint8Array; /** Seconds since each drop spawned (fall shadow, blink: `life` − age is the time left). */ age: Float64Array; /** Where each drop lands: (x[i], landY[i]). Moves with `scroll`. */ landY: Float64Array; /** Seconds a drop lasts, as configured. */ life: number; readonly count: number; /** Has drop i landed (only landed drops can be collected)? */ landed: (i: number) => boolean; spawn: (px: number, py: number, k?: number) => boolean; /** The kind of the first landed drop within r of (px, py), removed; or null. */ collect(px: number, py: number, r: number): K | null; /** The page scrolled: move every drop with it. */ scroll(dx: number, dy: number): void; reset(): void; }; ``` ### ScoreOptions (interface, COMPLETE) Options for `score()`: the combo window and steps (or no combo) and the XP for each level. ```ts export interface ScoreOptions { /** A streak lasts `window` seconds after each hit (default 3); the multiplier is * min(max, 1 + floor(streak / step)) (defaults 3 and 5). false: no combo. */ combo?: { window?: number; step?: number; max?: number; } | false; /** XP to reach each level after level 0, rising (e.g. [8, 24, 50] gives levels 0 to 3). */ levels?: readonly number[]; } ``` ### score (function, COMPLETE) Points, a combo that decays and levels from XP thresholds, on engine `e`. Call `add()` and `miss()` from a step. Events: `score`, `combo:end`, `level`. Signature: `score(e: Engine, o?: ScoreOptions): { … }` - e: `Engine` - o (optional): `ScoreOptions` - returns: `{ … }` ```ts export declare function score(e: Engine, o?: ScoreOptions): { system: System; readonly points: number; readonly streak: number; readonly xp: number; readonly level: number; /** The multiplier the next hit gets. */ readonly combo: number; /** XP still needed for the next level, or -1 at the top. */ readonly next: number; /** A hit: `points` × the combo, plus `xp`. Returns the points awarded. Event `score` (a: points, b: multiplier, at x, y). */ add(points: number, xp?: number, px?: number, py?: number): number; /** A miss: the streak ends now. */ miss(): void; reset(): void; }; ``` ## @fzlt/core/steer Status: BETA. Size: 1148 B gzip (budget 1280 B). Page: https://fizlet.com/bricks/steer.html Import: `import { … } from "@fzlt/core/steer"` How things move toward, away from and around a point, as DOM-free steering on typed arrays. Every mover has a position (`x`, `y`), a heading (`a`, radians) that turns at most `turn` rad/s, and moves `speed × scale` px/s along it. Behaviours: - `seek`: drive at a point, stop inside `stop`; - `flee`: drive straight away from a point until `far`; - `arrive`: seek with an arrival band: full pace far off, slower inside `slow`, stop inside `near`; `arriveGoal` is the same toward the goal the game wrote in `gx[i]`, `gy[i]` (a target that moves every frame: no double crosses a call, so nothing is boxed); - `wander`: pick a random goal in `area` every `every + random × jitter` s and seek it; - `patrol`: seek waypoints in turn, looping. Each writes the distance moved this call to `moved[i]` (0 when standing). The heading turn and the per-axis wall check (`blocked`, so a mover slides along walls) are DOM Battlefield Blitz's `drive`, and the defaults are its tank numbers, so a port keeps its feel. Nothing allocates: call them from a step, one mover at a time. Pass `engine.random` as `random` so seeded runs replay. Options are checked at setup: a bad number throws a RangeError with its name instead of turning into NaN mid-game. ### SteerOptions (interface, BETA) Options for `steer()`: how many movers, their pace, turn rate and arrival bands, the wander area and timing, the wall check and the random source. ```ts export interface SteerOptions { /** Movers, a positive integer (fixed: the arrays never grow). */ cap: number; /** Pixels per second at full pace (default 150) and a multiplier for the board's scale (default 1). */ speed?: number; scale?: number; /** Most the heading turns, radians per second (default 7). */ turn?: number; /** Seek, wander and patrol stop inside this many pixels of the goal (default 16). */ stop?: number; /** Arrive: stop inside `near` (14), slow down inside `slow` (140): pace `fast` (1.05) then `calm` (0.8). */ near?: number; slow?: number; fast?: number; calm?: number; /** Flee stops once this far away (default 240). */ far?: number; /** Wander: a new goal every `every` (2) + random × `jitter` (2) seconds, inside `area` [left, top, width, height]. */ every?: number; jitter?: number; area?: readonly [number, number, number, number]; /** Is (at[0], at[1]) a wall for mover i? Checked per axis, so movers slide along walls. The spot * comes in a typed array, not as two numbers: doubles passed to a callback are boxed. */ blocked?(i: number, at: Float64Array): boolean; /** 0..1 (default Math.random; pass engine.random to replay). */ random?(): number; } ``` ### turn (const, BETA) Turn `from` toward `to` by at most `max` radians, the short way round. Signature: `turn(from: number, to: number, max: number): number` - from: `number` - to: `number` - max: `number` - returns: `number` ```ts export declare const turn: (from: number, to: number, max: number) => number; ``` ### hypot (const, BETA) Math.hypot of two numbers, without the builtin call: V8 boxes Math.hypot's arguments, a heap number per call in a hot loop. Same steps as V8's own (Infinity wins over NaN, scale by the larger, sum, sqrt), so it returns the same bits (tested against Math.hypot). Signature: `hypot(dx: number, dy: number): number` - dx: `number` - dy: `number` - returns: `number` ```ts export declare const hypot: (dx: number, dy: number) => number; ``` ### steer (function, BETA) Steering for `o.cap` movers on typed arrays: `seek`, `flee`, `arrive`, `wander` and `patrol`, each with a turn-limited heading and a per-axis wall check. DOM-free and allocation-free: call them from a step, one mover at a time. Signature: `steer(o: SteerOptions): Steer` - o: `SteerOptions` - returns: `Steer` ```ts export declare function steer(o: SteerOptions): { x: Float64Array; y: Float64Array; a: Float64Array; /** The goal: wander's current one (and seconds until it picks the next), or one the game writes for `arriveGoal`. */ gx: Float64Array; gy: Float64Array; think: Float64Array; /** Patrol: the waypoint each mover is heading for. */ leg: Uint32Array; cap: number; /** Put mover i at (px, py) facing `heading`; wander picks a goal at once, patrol starts at 0. */ place(i: number, px: number, py: number, heading?: number): void; /** Pixels each mover went on its last call (0 when it stood). */ moved: Float64Array; /** Drive mover i along (ax, ay) (a stick, length ≤ 1) for dt seconds. */ drive(i: number, ax: number, ay: number, dt: number): void; /** Toward (tx, ty) at `pace` (dt is scaled by it, so the turn slows too), until inside `stop`. */ seek(i: number, tx: number, ty: number, dt: number, pace?: number): void; flee(i: number, fx: number, fy: number, dt: number, pace?: number): undefined; arrive(i: number, tx: number, ty: number, dt: number): void; /** * `arrive` toward (gx[i], gy[i]): write the goal there, then call it. For a goal computed every * frame (a slot behind a leader): passed as two numbers, V8 boxes each one whenever it doesn't * inline the call (3 young GCs per 20k steps in Blitz's platoons); a typed array never boxes. */ arriveGoal(i: number, dt: number): void; wander(i: number, dt: number, pace?: number): void; /** Waypoints as [x0, y0, x1, y1, …] (a typed array is best); the next one once inside `stop`. * No waypoint (fewer than two numbers): it stands still. */ patrol(i: number, points: ArrayLike, dt: number, pace?: number): undefined; }; ``` ### Steer (type, BETA) The movers `steer()` returns. ```ts export type Steer = ReturnType; ``` ## @fzlt/core/coach Status: BETA. Size: 1220 B gzip (budget 1536 B). Page: https://fizlet.com/bricks/coach.html Import: `import { … } from "@fzlt/core/coach"` The hints and tutorial layer the board games share, in three small parts: - `flag`: a once-only switch kept in localStorage ("tips off", "seen the tour"). Every read and write is wrapped in try/catch, so a page with storage blocked (private mode, sandboxed iframe, quota) just reads "not set" and plays on; - `tour`: DOM-free tutorial steps. It counts time on the current step, moves on when the game says the step is done (`coach:step` on the event ring), then cycles tips every `every` seconds. `show()` says which card to draw only when it changed, so the DOM is written once per card; - `card`: the Board UI kit card that shows it (count, title, text, an End button) and rings the control it is about. Its look is DOM Battlefield's coach card, inline on the kit tokens. Defaults are DOM Battlefield Blitz's practice coach (10 steps, then a tip every 6 s), so a port keeps its pace. `tour` steps without allocating; call it from a step like the rest of the game. ### flag (function, BETA) A localStorage switch: `on` when `key` holds `value` (default "1"). Never throws. Signature: `flag(key: string, value?: string): { readonly on: boolean; /** Set it (the player turned tips off, finished the tour…). */ mark(): void; clear(): void; }` - key: `string` - value (optional): `string` - returns: `{ readonly on: boolean; /** Set it (the player turned tips off, finished the tour…). */ mark(): void; clear(): void; }` ```ts export declare function flag(key: string, value?: string): { readonly on: boolean; /** Set it (the player turned tips off, finished the tour…). */ mark(): void; clear(): void; }; ``` ### TourOptions (interface, BETA) Options for `tour()`: how many tutorial steps, then how many tips and how often. ```ts export interface TourOptions { /** Tutorial steps (default 10). */ steps?: number; /** Tips shown in turn after the last step (default 7), one every `every` seconds (default 6). */ tips?: number; every?: number; } ``` ### tour (function, BETA) DOM-free tutorial steps on engine `e`: it counts time on the current step, moves on when the game says the step is done (`coach:step` on the event ring), then cycles tips every `every` seconds. Signature: `tour(e: Engine, o?: TourOptions): Tour` - e: `Engine` - o (optional): `TourOptions` - returns: `Tour` ```ts export declare function tour(e: Engine, o?: TourOptions): { /** Seconds on this step (after the last: since it ended). A typed array: read time[0]. */ time: Float64Array; /** The step the player is on; `steps` once the tutorial is over. */ readonly step: number; readonly done: boolean; steps: number; /** Count dt on this step. Call once a step, before you test whether it is done. */ tick(dt: number): void; /** The current step is done: on to the next (time back to 0, `coach:step` on the ring). False after the last. */ pass(): boolean; /** The card to draw, only when it changed since the last call: a step (0…steps − 1), a tip * (steps + 0…tips − 1), or -1 for no change. */ show(): number; /** Back to step 0 (a new practice run). */ reset(): void; }; ``` ### Tour (type, BETA) The tutorial `tour()` returns. ```ts export type Tour = ReturnType; ``` ### CardOptions (interface, BETA) Options for `card()`: the End button's label and action, and an abort signal. ```ts export interface CardOptions { /** The End button's label (default "END PRACTICE") and what it does (hidden if none). */ end?: string; onEnd?(): void; signal?: AbortSignal; } ``` ### card (function, BETA) The coach card, added to `host` (hidden until `show`). Signature: `card(host: HTMLElement, o?: CardOptions): { … }` - host: `HTMLElement` - o (optional): `CardOptions` - returns: `{ … }` ```ts export declare function card(host: HTMLElement, o?: CardOptions): { el: HTMLDivElement; /** Show a card (writes only what changed); `target` gets the dashed ring. */ show(c: string, t: string, body: string, target?: HTMLElement | null): void; hide(): void; remove(): void; }; ``` ## @fzlt/core/timeline Status: BETA. Size: 2137 B gzip (budget 2304 B). Page: https://fizlet.com/bricks/timeline.html Import: `import { … } from "@fzlt/core/timeline"` Scripted DOM choreography on the kernel clock. Keyframe tracks animate CSS properties (transform, translate, scale, rotate, opacity, clip-path, filter, custom properties: any value whose numbers change and whose words stay) or plain numbers, with easings, delays, staggers, sequencing and overlap, labels, repeats and yoyo. Time moves in the fixed step (no allocation, no DOM); writes happen in render through the ledger, change-only, so Recall restores every property touched. Done or paused, it asks for no frames: the engine settles. With reduced motion, play() lands on the end state in one step (or build a gentler variant on `tl.reduced`). ### Ease (type, BETA) An easing: progress 0..1 in, eased progress out (it may overshoot). ```ts export type Ease = (p: number) => number; ``` ### EASES (const, BETA) The named easings: cubic in, out and in-out; back overshoots a little and settles. Also by name: "steps(n)" (n equal jumps: a ticking counter, a typed-out line). Type: `Record` ```ts export declare const EASES: Record; ``` ### Value (type, BETA) A number, a CSS value ("translate(0px,8px)", "inset(0 50% 0 0)") or keyframes, evenly spaced. Keyframes share one shape: the same words, only the numbers change. A single value starts where the property's last track ended (or is set at once if it has none). ```ts export type Value = number | string | readonly (number | string)[]; ``` ### TrackOptions (interface, BETA) Options for one track: where it starts (`at`), its duration, delay, easing and stagger. ```ts export interface TrackOptions { /** Seconds (default 0.4). */ dur?: number; /** Seconds after `at`; negative overlaps the track before (default 0). */ delay?: number; /** A name in EASES, "steps(n)" or a function (default "out"). */ ease?: string | Ease; /** Seconds between one target's start and the next. */ stagger?: number; /** Where it starts: seconds, "<" (with the track before) or a label (default: the end so far). */ at?: number | string; } ``` ### TimelineOptions (interface, BETA) Options for `timeline()`: repeats, yoyo, speed and reduced-motion handling. ```ts export interface TimelineOptions { /** Extra plays after the first (default 0; Infinity loops). */ repeat?: number; /** Every other play runs backwards. */ yoyo?: boolean; /** Speed (default 1). */ rate?: number; /** Animate (default: yes, unless the visitor prefers reduced motion; then play() jumps). */ motion?: boolean; /** `a` on this timeline's events (default 0). */ id?: number; } ``` ### timeline (function, BETA) A timeline on `engine` (paused until `play()`). Events on the ring, `a` = id: "timeline:start", "timeline:end", and each label's own name (b = its index) as the playhead passes it. Signature: `timeline(engine: Engine, o?: TimelineOptions): Timeline` - engine: `Engine` - o (optional): `TimelineOptions` - returns: `Timeline` ```ts export declare function timeline(engine: Engine, o?: TimelineOptions): { /** Add tracks: each property of `values` on each target (an element, a list of them, or any * object or typed array for plain numbers: `tl.to(arr, { 0: [0, 1] })`). Chainable. */ to(targets: object | ArrayLike, values: Record, t?: TrackOptions): /*elided*/ any; /** Name a moment (default: the end so far) for `at`; its name goes on the ring as it passes. */ label(name: string, t?: number | string): /*elided*/ any; /** Play forward (from the start if it is at the end). */ play: () => /*elided*/ any; /** Play backward (from the end if it is at the start). */ reverse: () => /*elided*/ any; /** Stop where it is. The engine calls this as its System pause hook (off screen, hidden tab, * stop()): then it only suspends, and plays on when the engine resumes unless you paused it. */ pause(by?: unknown): /*elided*/ any; /** Move the playhead to `t` seconds; it keeps playing or stays paused. */ seek(t: number): /*elided*/ any; /** Land on the end in the next step ("timeline:end" fires). */ finish: () => /*elided*/ any; /** Pause and give every property back as it was before the timeline wrote it: styles through * the ledger (a newer host edit wins), plain numbers to their value when the track was added. */ revert(): /*elided*/ any; /** Resolves the next time the timeline ends, or when its engine is destroyed (at once if it was). */ done: () => Promise; /** The playhead in seconds (over every play). */ readonly time: number; /** Every play together, in seconds (Infinity when it loops forever). */ readonly duration: number; readonly playing: boolean; /** The visitor prefers reduced motion (or `motion: false`): build the gentler variant on it. */ readonly reduced: boolean; step(e: Engine, dt: number): void; render(): void; }; ``` ### Timeline (type, BETA) The timeline `timeline()` returns: add tracks with `to()`, then play it. ```ts export type Timeline = ReturnType; ``` ## @fzlt/core/touch Status: BETA. Size: 310 B gzip (budget 512 B). Page: https://fizlet.com/bricks/touch.html Import: `import { … } from "@fzlt/core/touch"` Taps and swipes on an animation or a game never zoom or pan the page. One delegated listener set on the document (capture, not passive) cancels, on the surfaces that match `sel`, what CSS `touch-action` can't: iOS Safari's `gesturestart` / `gesturechange` (its pinch, which ignores `user-scalable=no`) and any multi-touch `touchstart` / `touchmove` (a pinch anywhere else). One finger is never touched, so taps, drags and the page's own scroll stay. The CSS half lives in the Board UI kit (board-ui.css): `touch-action: manipulation` on the page (no double-tap zoom) and `touch-action: none` / `pan-y` plus `overscroll-behavior: none` on the same surfaces. Pass `null` to lock the whole page (a full-screen board; content pages keep pinch-zoom for WCAG 1.4.4). Returns `release()`: the page gets its gestures back. ### SURFACES (const, BETA) The surfaces Fizlet draws on: slot stages, game hosts, the touch pad, draggable panels, and anything marked `data-touch-lock`. Type: `"[data-fizlet-slot],[data-fizlet-host],[data-touch-lock],.bui-pad,.bui-grip"` ```ts export declare const SURFACES = "[data-fizlet-slot],[data-fizlet-host],[data-touch-lock],.bui-pad,.bui-grip"; ``` ### lockTouch (function, BETA) Lock pinch on the surfaces matching `sel` (`null`: the whole page). Returns `release()`. Signature: `lockTouch(doc?: Document, sel?: string | null): () => void` - doc (optional): `Document` - sel (optional): `string | null` - returns: `() => void` ```ts export declare function lockTouch(doc?: Document, sel?: string | null): () => void; ``` ## @fzlt/core/css-clock Status: BETA. Size: 5368 B gzip (budget 5566 B). Page: https://fizlet.com/bricks/css-clock.html Import: `import { … } from "@fzlt/core/css-clock"` A page's own CSS @keyframes, on the kernel clock. The look stays in the stylesheet; Fizlet owns the time. Each animation under `root` is paused and reads its time from one custom property that a timeline moves through the ledger: .led { animation: blink 1s steps(2) infinite paused; animation-delay: var(--fz-t, 0s); } .bug { animation: walk 6s linear infinite paused; animation-delay: calc(var(--fz-t, 0s) - 3s); } - One shared requestAnimationFrame with every other engine on the page; off screen or in a hidden tab it asks for no frames at all. - Settles: after `loops` plays of `span` seconds it rests where it started (every animation whose duration divides `span` is back on its first frame) and the engine sleeps. A pointer over the root, focus inside it or coming back into view plays it again. - Reduced motion: play() lands on the rest frame at once (the still poster). - Recall: destroy() (or aborting `signal`) gives the root's style back as it was. ### CssClockOptions (interface, BETA) Options for `cssClock()`: the loop length, how many plays before it rests, the custom property, the frame cap, reduced motion and an abort signal. ```ts export interface CssClockOptions { /** Seconds in one play: the loop length (the longest animation, or a common multiple). Default 6. */ span?: number; /** Plays before it rests and the engine settles (default 3). */ loops?: number; /** The custom property the animations read (default "--fz-t"). */ prop?: string; /** Frame-rate cap, e.g. 30 for steps() blinks (default: none, 60 Hz). */ fps?: number; /** Animate (default: yes, unless the visitor prefers reduced motion). */ motion?: boolean; /** Aborting it destroys the clock (Recall included). */ signal?: AbortSignal; } ``` ### cssClock (function, BETA) Run the CSS @keyframes under `root` on the kernel clock: each paused animation reads its time from one custom property a timeline moves. It settles after `loops` plays and wakes on pointer, focus or coming back into view; `destroy()` gives the root's style back. Signature: `cssClock(root: HTMLElement, o?: CssClockOptions): CssClock` - root: `HTMLElement` - o (optional): `CssClockOptions` - returns: `CssClock` ```ts export declare function cssClock(root: HTMLElement, o?: CssClockOptions): { engine: { signal: AbortSignal; events: import("./kernel/index.js").EventRing; ledger: import("./kernel/index.js").Ledger; random: import("./kernel/index.js").Random; root: Element | null; timing: Float64Array; dt: number; readonly time: number; readonly steps: number; readonly phase: number; readonly live: number; readonly running: boolean; readonly paused: string[]; add(s: S): S; remove(s: import("./kernel/index.js").System): void; pool(name: string, cap: number, create: () => T): import("./kernel/index.js").Pool; start(): void; busy(sec?: number): void; wake(): void; stop(): void; frame(now: number): void; advance(sec: number): void; stats(): import("./kernel/index.js").EngineStats; destroy(): void; }; tl: { to(targets: object | ArrayLike, values: Record, t?: import("./timeline.js").TrackOptions): /*elided*/ any; label(name: string, t?: number | string): /*elided*/ any; play: () => /*elided*/ any; reverse: () => /*elided*/ any; pause(by?: unknown): /*elided*/ any; seek(t: number): /*elided*/ any; finish: () => /*elided*/ any; revert(): /*elided*/ any; done: () => Promise; readonly time: number; readonly duration: number; readonly playing: boolean; readonly reduced: boolean; step(e: import("./kernel/index.js").Engine, dt: number): void; render(): void; }; /** Play from the start (unless held). */ play: () => void; /** Hold or release (a pause button): held, it keeps its frame and asks for nothing. */ hold(on: boolean): void; /** Stop for good and give the page back (Recall). */ destroy: () => void; }; ``` ### CssClock (type, BETA) The clock `cssClock()` returns. ```ts export type CssClock = ReturnType; ``` ## @fzlt/core/layout Status: EXPERIMENTAL. Size: 1747 B gzip (budget 2048 B). Page: https://fizlet.com/bricks/layout.html Import: `import { … } from "@fzlt/core/layout"` Force-directed layout on typed arrays, with a Barnes-Hut pass, for diagrams and graphs that settle. Nodes repel (a quadtree, rebuilt each step, sums far groups as one mass: O(n log n)), links pull like springs toward their length, and a weak pull holds the graph around (`cx`, `cy`). One tick per fixed step, so a run replays exactly. - Settle: `alpha` cools from 1 toward 0 over `ticks` steps; when it is spent, or nothing moved more than `still` px in a step, the layout rests and stops calling `busy()`: the engine sleeps. `heat()`, `pin()` and `link()` warm it up and wake the engine. - Reduced motion: the whole run happens in one step, so the graph lands where it settles. - Zero allocation per step; fixed capacity (`cap` nodes, `links` links: add and link return -1 when full). - DOM-free: draw it anywhere (@fzlt/core/view-canvas), frame it with @fzlt/core/camera (`bounds`). Distances use steer's exact `hypot` (Math.hypot boxes its arguments). ### LayoutOptions (interface, EXPERIMENTAL) Options for `layout`: capacity, forces, cooling and motion. ```ts export interface LayoutOptions { /** Nodes (fixed: the arrays never grow). */ cap: number; /** Links (default 2 × cap). */ links?: number; /** Repulsion between nodes, px² per tick (default 30). */ charge?: number; /** A link's rest length in px (default 40; `link` takes its own). */ distance?: number; /** Pull toward the centre, per tick (default 0.04). */ gravity?: number; /** The centre gravity pulls toward, x (default 0). */ cx?: number; /** The centre gravity pulls toward, y (default 0). */ cy?: number; /** Barnes-Hut accuracy: a cell of size s at distance d counts as one mass when s / d < theta (default 0.9). */ theta?: number; /** Velocity kept per tick (default 0.6). */ keep?: number; /** Steps a full run cools over (default 300: five seconds at 60 Hz). */ ticks?: number; /** At rest once no node moves more than this many px in a step (default 0.02). */ still?: number; /** Animate (default: yes, unless the visitor prefers reduced motion; then each run lands at once). */ motion?: boolean; } ``` ### layout (function, EXPERIMENTAL) A force-directed layout on typed arrays (`x`, `y`): Barnes-Hut repulsion, links, centre gravity. It cools over `ticks` steps, then rests and stops calling `busy()` so the engine settles. DOM-free. Signature: `layout(engine: Engine, o: LayoutOptions): Layout` - engine: `Engine` - o: `LayoutOptions` - returns: `Layout` ```ts export declare function layout(engine: Engine, o: LayoutOptions): { x: Float64Array; y: Float64Array; vx: Float64Array; vy: Float64Array; /** Nodes and links added so far. */ readonly size: number; readonly links: number; /** How warm the run is: 1 just heated, 0 at rest. */ readonly alpha: number; /** At rest: no ticks, no busy(); the engine can settle. */ readonly resting: boolean; /** Add a node at (x, y) (default: a sunflower spiral round the centre); its index, or -1 when full. */ add(ax?: number, ay?: number): number; /** Link nodes a and b at rest length `len` (default `distance`); its index, or -1 when full. */ link(a: number, b: number, len?: number): number; /** Hold node i at (x, y) (a drag): the rest flows round it. */ pin(i: number, ax: number, ay: number): void; /** Let node i go. */ free(i: number): void; /** Warm the run up to at least `alpha` (default 1) and wake the engine. */ heat(alpha?: number): void; /** The node nearest (ax, ay) within `r` px, or -1. */ nearest(ax: number, ay: number, r?: number): number; /** The box round every node, [left, top, right, bottom], into `out` (no allocation). */ bounds(out: T): T; step(e: Engine): void; }; ``` ### Layout (type, EXPERIMENTAL) What `layout` returns: `x`, `y`, `add`, `link`, `pin`, `free`, `heat`, `nearest`, `bounds`. ```ts export type Layout = ReturnType; ``` ## @fzlt/core/view-canvas Status: EXPERIMENTAL. Size: 936 B gzip (budget 1229 B). Page: https://fizlet.com/bricks/view-canvas.html Import: `import { … } from "@fzlt/core/view-canvas"` One overlay canvas per root, sized for the screen's pixel density and redrawn only where something changed. The canvas covers the root's padding box (`pointer-events: none`, so the root keeps its input), its backing store is the CSS size × devicePixelRatio (capped by `maxDpr`), and drawing is in CSS px. - Dirty redraw: `dirty([x, y, w, h])` grows one box (no arguments: the whole canvas); in the render phase that box, snapped out to whole device pixels, is cleared, clipped and handed to `draw`. Nothing dirty, nothing drawn: a still view costs no canvas work at all. - Settle: it never keeps the engine awake; `dirty()` wakes a settled engine for one redraw. - Layout reads only in the read phase, and only after a resize or a zoom (a ResizeObserver). - Recall: the canvas goes and the root's style comes back exactly (the engine's ledger). It has no motion of its own: reduced motion belongs to whatever moves the drawing. ### ViewCanvasOptions (interface, EXPERIMENTAL) Options for `viewCanvas`: the `draw` callback, the density cap and an accessible name. ```ts export interface ViewCanvasOptions { /** Paint. The canvas is already cleared and clipped to `box` ([x, y, w, h], CSS px): draw the * scene (anything outside the box is clipped away, so drawing it all is fine). */ draw(ctx: CanvasRenderingContext2D, box: Float64Array): void; /** The most pixel density drawn (default 2: sharp on a phone, a quarter of the pixels at 4×). */ maxDpr?: number; /** An accessible name: the canvas becomes role="img" (default: aria-hidden, decoration). */ label?: string; } ``` ### viewCanvas (function, EXPERIMENTAL) One overlay canvas on `root`, sized for the pixel density (capped by `maxDpr`) and redrawn only inside the dirty box: `dirty([x, y, w, h])` (CSS px), or `dirty()` for all of it. Never keeps the engine awake; Recall removes the canvas and gives the root's style back. Signature: `viewCanvas(engine: Engine, root: HTMLElement, o: ViewCanvasOptions): ViewCanvas` - engine: `Engine` - root: `HTMLElement` - o: `ViewCanvasOptions` - returns: `ViewCanvas` ```ts export declare function viewCanvas(engine: Engine, root: HTMLElement, o: ViewCanvasOptions): { canvas: HTMLCanvasElement; ctx: CanvasRenderingContext2D; /** The canvas size in CSS px, as last measured. */ readonly width: number; readonly height: number; /** The device pixels per CSS px it draws at. */ readonly dpr: number; /** Redraw box [x, y, w, h] (CSS px) on the next render; no box, the whole canvas. The box comes * in an array, not as four numbers: doubles passed to a call are boxed. Wakes a settled engine. */ dirty(b?: { readonly [i: number]: number; }): void; read(): void; render(): void; }; ``` ### ViewCanvas (type, EXPERIMENTAL) What `viewCanvas` returns: `canvas`, `ctx`, `width`, `height`, `dpr` and `dirty(box?)`. ```ts export type ViewCanvas = ReturnType; ``` ## @fzlt/core/arcade Status: COMPLETE. Size: 24147 B gzip (budget 24150 B). Page: https://fizlet.com/bricks/arcade.html Import: `import { … } from "@fzlt/core/arcade"` The shell every page arcade game repeats, so a game is only its rules and its art. One engine per run over the host page (`@fzlt/core/board-page`: its words are terrain, its buttons are targets), a fixed overlay with a canvas and a scoreboard in the Board UI kit (title, sound, pause, fold and exit keys, readouts, an optional meter, action keys, a comms toast), keys and pointer (`@fzlt/core/input`), the touch pad on coarse pointers (`@fzlt/core/pad`), sound from the event ring (`@fzlt/core/sound`), sparks and stacking score popups, factory health painted into the buttons, and a controller a hub drives: start, pause, resume, stop, destroy, getSnapshot. While a run is live, a click on the page fires instead of following a link; the scoreboard, fields and `[data-virus-ignore]` / `[data-fizlet-ignore]` keep their clicks. Recall (Escape, the exit key, stop) destroys the engine, so every letter and button comes back exactly; a run that ends keeps the scoreboard with the result until Recall. The game supplies `create(ctx)`: its own systems on `ctx.engine`, reading `ctx.ctl` (written in the read phase) and drawing in render. The scoreboard and controls are a HUD preset, chosen per game with `hud` (BOARD-UI.md §19): `arcadeHud()` is today's card and the default (no `hud` renders it byte for byte); `skirmishHud({...})` is Battlefield Blitz's top bar with a clock, rank and armor bar, a feed, a banner, aim toggles and counted keys, and its flow (countdown, pause / surrender, result) on `@fzlt/core/match`: then match owns the phase, the shell asks it to pause, resume or end and follows it (`HudView.ask`, `HudRun.go`). Each preset is its own export, so a game ships only the one it names. ### skirmishHud (function, COMPLETE) The `skirmish` preset (Battlefield Blitz): `hud: skirmishHud({ readouts: [{ label: "Kills", tone: "team" }, { label: "Time", kind: "clock" }, { label: "Armor", kind: "bar", low: 0.3 }] })`. `hud(out)` fills the readouts in order, then any count slots. Signature: `skirmishHud(o: SkirmishOptions): ArcadeHud` - o: `SkirmishOptions` - returns: `ArcadeHud` ```ts export declare function skirmishHud(o: SkirmishOptions): ArcadeHud; ``` ### SkirmishOptions (interface, COMPLETE) Options for the `skirmish` HUD preset: its readouts, header and nav keys, feed, banner, overlays, result, aim toggles and key counts. ```ts export interface SkirmishOptions { /** Readouts, in the order `hud(out)` fills them. */ readouts: SkirmishReadout[]; /** Header keys, in order (default full screen, sound, pause, exit; `fold` hides the title and the * text keys). Full screen shows only where the page may use it. */ header?: ("fullscreen" | "sound" | "pause" | "fold" | "exit")[]; /** Text keys after the actions, as on the card (default none): `next` (Next gate, G), `restart`, * `auto` (Auto fire: needs `fire`). */ nav?: ("next" | "restart" | "auto")[]; /** A feed of the last `rows` (default 4) events of type `on`, newest first. */ feed?: { rows?: number; on: string; line: (a: number, b: number) => string; }; /** A centred banner (title, sub) on each event of type `on`. */ banner?: { on: string; text: (a: number, b: number) => readonly [string, string?]; }; /** Overlays (default all four; @fzlt/core/match runs the flow either way). `countdown` holds the start * for 3, 2, 1 on the flow's clock; without `pause` or `result` that phase shows no overlay; without * `surrender` the pause card has no SURRENDER key. */ overlays?: ("pause" | "surrender" | "countdown" | "result")[]; /** The result card: `stats()` keys to list, and the one whose best is kept on this device. */ result?: { stats: string[]; best?: string; }; /** Aim toggles: the intents `aim-auto` and `aim-lock` (`ctx.input.down("aim-lock")`). */ aim?: ("auto" | "lock")[]; /** Counted keys: action id → the `hud(out)` slot of its count, a badge on its keys. */ counts?: Record; } ``` ### SkirmishReadout (interface, COMPLETE) One readout on the skirmish bar: its label, how the value shows and its tone. ```ts export interface SkirmishReadout { label: string; /** How the value shows: `num` (default) 12 · `clock` 3:05 from seconds · `rank` #2 · `pct` 75% * from 0..1 · `bar` a meter from 0..1 (armor, health). */ kind?: "num" | "clock" | "rank" | "pct" | "bar"; tone?: "team" | "enemy"; /** A bar: class `low` at or under this (0..1). */ low?: number; } ``` ### MODE, PX, PY, PON, KX, KY, FIRE, VW, VH, CALM, TARGET_HP (const, COMPLETE) Controls and view, written once per frame in the read phase (a step reads them): mode (0 keys or stick, 1 pointer), pointer x, y and on, move x, y (-1..1), fire, viewport width and height, and reduced motion (1/0). The game writes TARGET_HP: the full health of a target, for its paint. ```ts export declare const MODE = 0, PX = 1, PY = 2, PON = 3, KX = 4, KY = 5, FIRE = 6, VW = 7, VH = 8, CALM = 9, TARGET_HP = 10; ``` ### ArcadeAction (interface, COMPLETE) One action key: the intent it drives, its keyboard code and its scoreboard text. ```ts export interface ArcadeAction { /** The controls intent: `ctx.input.hit(id)` in a step. */ id: string; /** KeyboardEvent.code, e.g. "KeyB". */ key: string; /** Scoreboard key text, e.g. "B · Cover". */ label: string; } ``` ### ArcadeStatus (type, COMPLETE) Where a run is: idle, running, paused, won, game over or destroyed. ```ts export type ArcadeStatus = "idle" | "running" | "paused" | "won" | "gameover" | "destroyed"; ``` ### ArcadeOptions (interface, COMPLETE) Options for `arcade()`: the board and its selectors, theme, seed, reduced motion, sound and controls, and the `onState` / `onEvent` / `onError` hooks. ```ts export interface ArcadeOptions { /** The board: a selector or an element (default document.body). */ root?: string | Element; textSelector?: string; targetSelector?: string; excludeSelector?: string; /** "system" (default) follows the visitor's colour scheme. */ theme?: "dark" | "light" | "system"; seed?: number; reducedMotion?: boolean; /** Start with sound on (default true; it waits for a gesture to unlock). */ sound?: boolean; /** "keyboard": the mouse never steers. */ controls?: "mouse" | "keyboard"; onState?: (state: ArcadeSnapshot) => void; onEvent?: (event: { type: string; game: string; [key: string]: unknown; }) => void; onError?: (error: unknown) => void; } ``` ### ArcadeSnapshot (type, COMPLETE) What the controller reports about a run: its status, time and frame cost, plus the game's own state. ```ts export type ArcadeSnapshot = Partial & { id: string; status: ArcadeStatus; elapsed: number; frameMs: number; maxFrameMs: number; notice: string; /** Letters indexed, on screen and spent, and the range reads the last measurement made. */ board?: { indexed: number; visible: number; spent: number; measurements: number; }; /** Sparks alive (at most 100). */ particles?: number; }; ``` ### ArcadeController (interface, COMPLETE) The controller `arcade()` returns: start, pause, resume, stop and destroy a run, and read its snapshot. ```ts export interface ArcadeController { start(): ArcadeSnapshot; pause(): void; resume(): void; stop(): void; destroy(): void; getSnapshot(): ArcadeSnapshot; } ``` ### ArcadeContext (interface, COMPLETE) What `create` gets: the run's engine and board, the controls, and the shell's presentation. ```ts export interface ArcadeContext { engine: Engine; board: BoardPage; input: Controls; /** MODE … TARGET_HP (see the constants). */ ctl: Float64Array; reduced(): boolean; /** A popup at (x, y): `+value` when `text` is null (stacked with nearby ones), else the text. * Tone 0 score, 1 accent (upgrade), 2 hurt. Call it from render (react). */ popup(x: number, y: number, value: number, text: string | null, tone?: 0 | 1 | 2): void; /** n sparks from (x, y) in colour `c` (an index into `colors`); none under reduced motion. */ spark(x: number, y: number, n: number, c: number): void; /** A line in the comms toast. */ say(text: string, ms?: number): void; /** onEvent for the hub (a plain object: call it from render, not a step). */ emit(type: string, data?: Record): void; /** In-page art: a shadow root in an overlay inside `el` (an absolutely positioned host that * follows the page's layout, scrolling and transforms natively). A static `el` becomes * relative. Through the ledger: Recall removes it and puts the style back. Call from create * or reset (it reads computed style), never from a step. Null for replaced elements. */ attach(el: Element): ShadowRoot | null; } ``` ### ArcadeGame (interface, COMPLETE) One run of your game, as the shell drives it: reset, draw, the HUD and meter, page scroll, reactions to events, stats, and whether the run is over. ```ts export interface ArcadeGame { /** A fresh run (the viewport is in ctl[VW], ctl[VH]). */ reset(): void; /** 0 playing, 1 lost, 2 won: read every render; the run ends the frame it turns non-zero. */ over(): number; /** The page scrolled by (dx, dy): move page-space things (read phase). */ scroll?(dx: number, dy: number): void; /** Each event on the ring this frame (render): popups, sparks, the toast, onEvent. */ react?(ev: FizEvent): void; /** Draw on the canvas (render), after the target beacons, before sparks and popups. */ draw(ctx: CanvasRenderingContext2D, dt: number): void; /** The readout values, in `readouts` order, then the meter's change key (render, change-only). */ hud(out: Float64Array): void; /** The meter's left text, right text and fill 0..1, when its change key moved. */ meter?(): readonly [string, string, number]; /** The snapshot's game fields (not a hot path). */ stats(): S; } ``` ### ArcadeDef (interface, COMPLETE) Your game for `arcade()`: its id, title, accent, hint, CSS, actions, readouts, sound cues and `create()`, which makes a run. ```ts export interface ArcadeDef { id: string; title: string; /** The game's accent (hex): title, meter, popups, target paint at full health. */ color: string; hint: string; touchHint?: string; /** The Board UI kit CSS text (the game bakes in the sections it uses). */ css: string; /** Hold Space / the pointer to fire; a Fire key on the touch pad and an Auto fire key. */ fire?: boolean; actions?: ArcadeAction[]; /** Touch move control (default "stick"; "dpad" for grid games). */ pad?: "stick" | "dpad"; /** Keys (or the stick) only: the mouse never steers. */ keyboard?: boolean; /** The card's readouts: label, tone, how the value shows, and whether it hides when the board is * folded. A preset with its own readouts (skirmish) leaves this out. */ readouts?: [label: string, tone?: "team" | "enemy" | undefined, fmt?: ((v: number) => string) | undefined, fold?: boolean | undefined][]; meter?: { label: string; }; /** Spark colours, by index. */ colors: string[]; /** Event type → sound cue. */ cues: Record; /** Paint targets with their health (default true). */ paint?: boolean; /** The scoreboard and controls: a HUD preset (default `arcadeHud()`, today's card). */ hud?: ArcadeHud; create(ctx: ArcadeContext): ArcadeGame; } ``` ### ArcadeHud (interface, COMPLETE) A HUD preset: its CSS (after the game's kit CSS) and a mount, once per run. ```ts export interface ArcadeHud { css: string; /** Extra controls intents the HUD drives (e.g. aim toggles): intent → key codes. */ keys?: Record; mount(run: HudRun): HudView; } ``` ### HudRun (interface, COMPLETE) What the shell gives a HUD when a run starts. ```ts export interface HudRun { /** The kit root (`.bui`), the canvas already in it. */ ui: HTMLElement; def: ArcadeDef; engine: Engine; input: Controls; /** A coarse pointer: build the touch pad. */ coarse: boolean; /** The game, once `create` returned: read it in `write`, never in `mount`. */ game: ArcadeGame; /** Sound (a scoreboard key flips `on`). */ sfx: { on: boolean; }; /** Aborts at Recall: the overlay's life. */ signal: AbortSignal; status(): ArcadeStatus; /** Moves the run, for a HUD that owns its flow (`ask`), once its phase changed: pause, resume, * the end (won, lost), a rematch (restart) or Recall (exit). */ go(to: ArcadeFlow | "restart" | "exit"): void; } ``` ### ArcadeFlow (type, COMPLETE) A change in a run's flow the HUD is told about: paused, resumed, won or lost. ```ts export type ArcadeFlow = "pause" | "resume" | "won" | "lost"; ``` ### HudView (interface, COMPLETE) A mounted HUD. Keys are `button[data-a]` (sound, pause, exit, restart, gate, auto, fold, or the HUD's own) and `button[data-act]` (an action): the shell runs them, then calls `key`. ```ts export interface HudView { /** The comms toast. */ msg: HTMLElement; /** Every render and once at start: change-only writes, nothing allocated in the steady state. */ write(): void; /** The status changed: the pause key, overlays. */ sync(): void; /** A scoreboard key ran: repaint its face (`auto`: auto fire is on). */ key(a: string, b: HTMLElement, auto: boolean): void; /** At start, in place of starting the engine: a countdown that calls `go` when it is done. */ begin?(go: () => void): void; /** Before the engine goes at the end of a run (its ledger takes back what went through it). */ hold?(): void; /** The run ended and the page is back: show the result. */ end(won: boolean, notice: string, stats: Record): void; /** The HUD owns the phase (skirmish: `@fzlt/core/match`): the shell's pause, resume and end are asked of * it, and it answers with `HudRun.go` (one owner; the shell keeps the keys and delegates). */ ask?(to: ArcadeFlow): void; } ``` ### ArcadeHudOptions (interface, COMPLETE) Options for the default HUD: which header keys and which nav keys, in order. ```ts export interface ArcadeHudOptions { /** Header keys, in order (default all four). */ header?: ("sound" | "pause" | "fold" | "exit")[]; /** The nav row after the action keys (default all three; `auto` needs `fire`). */ nav?: ("next" | "restart" | "auto")[]; } ``` ### arcadeHud (function, COMPLETE) The `arcade` preset, the default: one card with the title, sound / pause / fold / exit keys, the readouts, one meter, the nav row (actions, Next gate, Restart, Auto fire) and the toast; phones get the pad and the card folded at the top. `arcadeHud()` renders exactly what leaving `hud` out renders. Values: `hud(out)` fills the readouts in order, then the meter's change key. Signature: `arcadeHud(o?: ArcadeHudOptions): ArcadeHud` - o (optional): `ArcadeHudOptions` - returns: `ArcadeHud` ```ts export declare function arcadeHud(o?: ArcadeHudOptions): ArcadeHud; ``` ### arcade (function, COMPLETE) The shell every page arcade game repeats: one engine per run over the host page (its words are terrain, its buttons targets), a Board UI overlay with a canvas and scoreboard, input, sound and the run's flow. `def` is only the game's rules and art. Signature: `arcade(def: ArcadeDef, options?: ArcadeOptions): ArcadeController` - def: `ArcadeDef` - options (optional): `ArcadeOptions` - returns: `ArcadeController` ```ts export declare function arcade(def: ArcadeDef, options?: ArcadeOptions): ArcadeController; ``` ## @fzlt/core/debug Status: COMPLETE. Size: 486 B gzip (budget 1536 B). Page: https://fizlet.com/bricks/debug.html Import: `import { … } from "@fzlt/core/debug"` Development-only checks. `guardLayout(engine)` counts every layout read (rects, offset/client/scroll sizes, computed style) made while the engine is in its STEP phase, the contract's "no layout reads in the tick". Never ship this in a production build. ### guardLayout (function, COMPLETE) Count layout reads made during a step; the number shows as `engine.violations`. Signature: `guardLayout(engine: Engine): () => boolean` - engine: `Engine` - returns: `() => boolean` ```ts export declare function guardLayout(engine: Engine): () => boolean; ```