# Focus
Menu focus navigation for the cards of GREASEPAINT. A controller has no
pointer, so every menu carries an ordered ring of focusable ids and exactly one
of them is lit; this module walks that ring.
- `Focus.open(list, prefer)` — open a menu: the ids in stick order, and which starts lit.
- `Focus.axis(v, dt)` — feed the `navigate` axis; one flick is one step, a held stick repeats.
- `Focus.point(id)` — a mouse crossing a row or a finger tapping it moves the focus there.
- `Focus.id()` / `Focus.isFocused(id)` — read it.
- `Focus.close()` — nothing is focused until the next `open`.
- `Focus.release()` — let go of a held direction, so a stick held through a transition does not keep walking.
The ring is whatever the card offers, so an interval with three faces and a
fourth row is a ring of four and needs nothing said about it here: `open` takes
the list `screens.itemIds` returns for the phase.
A held stick repeats on a delay measured in seconds AND in frames. On a machine
running long frames a single flick can be one frame carrying half a second of
`dt`, and a delay measured only in time turns that flick into two steps.
Fed by `game/input/console.inputMap`; consumed by `game.ui.Screens`, which asks
it which row to draw lit.
# core
<!-- zero:scaffolded-readme -->
<!-- Replace the line below with what this module provides and who
consumes it, then delete both comment lines. -->
TODO: describe this module.
# theme
The palette, the type scale and the layout metrics every screen of *First
Light: Henter of the Glew* is measured from.
```lua
local T = require("game.ui.theme")
local L = T.layout() -- read it fresh every frame
local W = T.layout({ width = 1920, height = 1080 }) -- ask about another shape
```
`T.layout()` derives every size from the LIVE `ui.screenSize()`: one scale
factor `s` taken from the short edge, a card width capped by both edges, a
touch target no smaller than a thumb, and `compact` / `dense` for a screen with
more width than height. Nothing here is a number chosen for one frame, which is
what lets the same screens read on a phone held either way, a laptop, and a
television.
| | |
|---|---|
| `T.color` / `T.wash` | night blue, violet, mist, glew amber, ember, bone — solid and translucent |
| `T.font` | `display` (EB Garamond) for the folk-tale voice, `body` (PT Serif) for what a player reads at speed |
| `T.layout(size?)` | the metrics: `s`, `pad`, `railW`, `cardW`, `touch`, `type.*`, `compact`, `dense` |
| `T.label` / `T.svg` / `T.abs` | the node shapes every screen repeats |
| `T.enter` / `T.breathe` | a one-shot entrance and an idle glow |
Consumed by `game.ui.hud`, `game.ui.cards` and `game.ui.Screens`.
# hud
The readout the hill is played under, and the caption band that carries a
spoken line as written text.
```lua
local Hud = require("game.ui.hud")
ui.registerScreen("fl_hud", Hud.tree(vm, L), 20)
ui.registerScreen("fl_caption", Hud.caption(vm, L), 40)
```
Both builders are **pure**: they take the view model `game.ui.Screens` builds
each frame and the live `game.ui.theme` layout, and return a tree. They read no
game state and register nothing.
## What it draws
**The left rail**, because the filmed screen is taller than it is wide and
nothing may span the width. Reading down:
- the **lantern** — a glass phial filling with what has been hented, notched at
bronze / silver / gold, the count under it in the tier's own colour, and the
next mark named;
- the **dawn rail** — the hill from the summit at the top to the foot at the
bottom, the light-line walking down it, the Henter's own place marked beneath,
and the time till first light, which turns ember in the last of the round.
Opposite it sits the **best night** plate, and past `dawn = 0.72` a warm wash
arrives at the top of the frame — the clock made into a picture, costing no
horizontal room.
**The caption band** prints whatever is being spoken. Every voiced line ships
with its written form, so a failed generation, a muted player and a noisy room
all still get the line. It sits above whatever the on-screen touch overlay has
claimed (`vm.touchCeiling`), so a caption never lands under the Hent button.
# game.audio.sfx
Play a one-shot from anywhere on the hill.
```lua
local Sfx = require("game.audio.sfx")
Sfx.play(clipRef) -- flat, non-spatial
Sfx.play(clipRef, { pos = { x, y, z }, volume = 0.7 }) -- out on the hill
Sfx.play(clipRef, { pitch = 1, jitter = 0.12 }) -- a run that is not a machine
Sfx.ui("confirm") -- the menu's own set
```
It spawns a temporary entity carrying an `Audio` component, lets it sound, and
despawns it after `ttl` seconds. Pass `pos` for a spatial source, heard from
the `AudioListener` on the CameraRig. Resolve your clip through a **literal**
identity so the reference is pinned when this world is installed elsewhere.
## Three things every caller gets
**A voice budget.** Below a reserve of free voices a low-priority one-shot is
dropped rather than stealing the narrator's. `priority` 0 is set dressing, 1
is the default, 2 survives a crowded mix, 3 always plays.
**A collapse window.** Ten motes caught in one frame is one sound, not ten
stacked copies 6 dB louder than anything else in the mix. The same clip asked
for again inside `collapse` seconds (35 ms by default) is dropped. Pass a
longer window for a sound that should never double up, or `priority = 3` to
opt out.
**Pitch jitter.** `jitter` spreads repeats of one clip either side of their
pitch, so a run of them reads as a hill rather than a loop.
## Options
`pos`, `volume`, `pitch`, `jitter`, `maxDistance`, `channel`, `ttl`, `name`,
`priority`, `collapse`, `looping`.
`channel` is one of `sfx` / `ui` / `ambient` / `music` / `voice` — the names
the mix knows. A source naming anything else plays at unity, which is a sound
nobody can turn down, so an unknown name falls back to `sfx`.
## The menu's set
`Sfx.ui(name)` plays one of `click` / `move` / `confirm` / `back` / `deny` —
small struck-glass sounds, because the lantern is glass and brass and so is
the menu. Non-spatial, on the `ui` channel, and high enough priority to get
through a crowded mix.
`Sfx.stopAll()` drops every one-shot still sounding, so a hent that started a
frame before the round ended does not carry into the end card.
# cards
Every screen *First Light: Henter of the Glew* puts BETWEEN the player and the
hill.
```lua
local C = require("game.ui.cards")
ui.registerScreen("fl_card", C.title(vm, L, opts), 1000)
```
| Builder | Card | Rows |
|---|---|---|
| `C.title(vm, L, opts)` | the crest, the name, the tagline, the best night | `to_hill`, `to_howto` |
| `C.howto(vm, L, opts)` | glew, the hent, hoggies, the light-line, the tiers, and every way to play | `to_hill`, `to_title` |
| `C.pause(vm, L, opts)` | where the round stands: lantern, time left, best | `resume`, `again`, `to_title` |
| `C.ending(vm, L, opts)` | the stamp, the count, the word, the tiers | `again`, `to_title` |
Every builder is **pure**: it takes the view model, the live `game.ui.theme`
layout, and `opts` — which row holds the focus, what the connected pad's
buttons are called, and the prompt strip to print — and returns a tree.
Nothing here registers, shows, or writes game state; `game.ui.Screens` does all
of that.
## The three families, in the card itself
A row is at least `L.touch` points tall and carries its own `onClick`, so a
pointer click, a finger tap and a pad's `confirm` on the lit row all land on
the same node. Exactly one row wears the **focus ring** — an amber left bar, a
lit border and a coloured glow — which is the controller's cursor. The how-to
card prints the keyboard, controller and touch bindings side by side, and every
card's footer names the keys, the pad buttons and the tap for what it offers.
## Shape
`L.compact` (a screen with more width than height) lays the how-to card's three
rules ACROSS instead of down, and `L.dense` tightens the vertical rhythm, so the
same card fits a 3:4 phone, a 16:9 laptop and a phone held sideways without
clipping or shrinking the type below reading size.