# 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.
# game.ui.pointer
What is under the cursor, asked cheaply.
```lua
local Pointer = require("game.ui.pointer")
local moved, x, y = Pointer.moved("table")
if moved then
local id, index = Pointer.find(x, y, "^gp%-card%-(%d+)$")
if index then focus(tonumber(index)) end
end
```
- `Pointer.moved(key) -> moved, x, y` — has the cursor moved since this key
last asked. Reads the cursor position out of the input state, cached once a
frame and shared between callers. No hit test happens here, so a frame with
a resting cursor costs a few hundredths of a millisecond. The first ask for
a key answers `true`, so a row already under a resting cursor lights on the
frame its screen comes up.
- `Pointer.forget(key)` — make the next `moved(key)` answer `true` anyway.
What sits under a resting cursor changes when the screen under it does: a
new hand is dealt, a card is played, a set of rows is rebuilt. The caller
says so with this.
- `Pointer.idsAt(x, y) -> { string }` — the widget ids under a point,
top-most first. This is the expensive half (about 1.3 ms), so it is asked
on the frames `moved` answers true for and no others.
- `Pointer.find(x, y, pattern) -> id, capture` — the first id under the point
that matches a Lua pattern, plus that pattern's first capture.
- `Pointer.at() -> x, y` — the cursor, without disturbing any key's mark.
On a touchscreen and on a pad the cursor never moves, so `moved` answers
false forever and nothing here costs anything — which is right, because
hover means nothing to a finger or a thumbstick. Taps and pad focus reach the
table and the cards by their own routes.
# screens
Every card GREASEPAINT puts between the player and the ring, as a 1931 circus
handbill: cream stock, a woodcut display face, and a double rule at every edge.
**Every function here is a pure builder.** Hand it a view model and it hands
back a widget tree. Nothing in this module reads input, registers a screen or
writes game state; `game.ui.Screens` owns all of that and this module only knows
what a card should look like.
## Layout
Every number comes from the live frame through `metrics()`, which is
`Core.frame()` plus the column the words stand in. There is no pixel constant:
the type is a fraction of the SHORTER screen edge, the column of words is a
fraction of the frame, and a card whose column is wide enough sets its blocks in
two. The same card lands on a phone held either way, on a laptop and on a
television.
A flex column that centres its children makes them hug their content, so a
wrapping paragraph inside one collapses to a glyph per line. Everything here
therefore sits in a column of a KNOWN width and centres its text with
`textAlign`.
## The ink
A handbill was printed in two inks on cream stock: a warm black, and one red.
The red is spent where the game means it — the name of the game, the tent's own
grin, the words SHE STAYED. Section heads, menu accents and the tent mark's
stripes take velvet; the control table's touch column takes mustard. A card that
reddens its section heads has nothing left to say the grin with.
## The scrim
A card is paper held up in a tent, not a page in a book. The frame behind it
carries a scrim of 0.58–0.66 and a blur, so the beam, the sawdust and whoever is
standing in the ring are still there behind the paper.
## What it provides
| | |
|---|---|
| `metrics()` | The live frame and every size derived from it. |
| `withScreen(w, h, fn)` | Build against a stated frame instead of the live one — how a card is proved to hold at a second shape. |
| `build(phase, opts)` | The card for a Core phase, or nil in the phases that show the ring alone. |
| `title` `howto` `reward` `pause` `won` `lost` | The cards themselves. |
| `itemIds(phase, state)` | The focusable row ids a phase offers, in the order a stick walks them. |
| `hasCard(phase)` | Whether a phase puts a card up at all. |
| `roman(n)` | An act number in the roman the tent prints it in. |
| `patternWords(clownId)` | A clown's telegraphed pattern as a sentence — "guards for 8, then swings for 13 and laughs, then laughs". |
| `fitSize(text, width, ideal, em, tracking)` | The largest size at which a string still fits a width. |
| `item` `itemList` `eyebrow` `rule` `para` `familiesFooter` | The pieces a card is built from. |
| `tentMark` `rosette` `manicule` `maniculeDown` `grinMark` `ringMark` | The woodcut marks, as inline SVG. |
## Rows
`item()` returns a row that is the focus target, the click target and the touch
target at once. Its visible box carries the look; a transparent `button` with the
id `screens-hit-<id>` is laid over it at the same size, because that is the only
widget that senses a pointer. The lit row takes a sodium wash from the left, a
ring, and the pointing hand a handbill puts beside the line it wants read.
## The interval
`reward` builds both intervals off `state.reward`:
- `mode = "take"` — TAKE ONE. Three faces off the pool, and a fourth row, TAKE
NOTHING, that declines them.
- `mode = "leave"` — LEAVE ONE BEHIND, at the act `Core.RULES.LEAVE_ACT`. The
three faces are Ada's own cards, and the fourth row, KEEP THEM ALL, sends all
three back.
Both name who is waiting, from `state.reward.next`: the clown's name, its Nerve,
and `patternWords` of what it is about to do. The faces are the handbill the hand
holds, built by `CardArt.face` at interval size, so the SAWDUST a player is
offered is the SAWDUST they have been playing. The focus is carried OUTSIDE the
stock — a lit border in the gutter and a hand pointing down at it — because a
face that grew or reddened when it was lit would be a different card.
## Who consumes it
`game.ui.Screens`, and nothing else.
# subtitles
The strip every spoken line in GREASEPAINT is printed in.
Nobody in this game is heard without also being read. The sound slice generates
each line and emits `voice.line { speaker, text, durationMs }`; this module holds
the line that is currently up, counts it down, and builds the plate it is printed
on. Ada never speaks — everything printed here is somebody else.
A line survives a failed generation intact: the text is what the strip prints,
and the audio only rides along with it. A line that arrives with no duration is
given `Core.readingTime`'s instead.
## What it provides
| | |
|---|---|
| `show(who, line, durationMs)` | Put a line up. |
| `tick(dt)` | Count it down; true on the frame it goes away. |
| `clear()` | Take it down now — what a phase change does. |
| `generation()` | Changes whenever the content does, so a caller re-renders only then. |
| `tree(phase, override)` | The strip as a widget tree — or **nil** when nobody is speaking. |
## The strip exists only while a line is up
`tree` returns nil when nothing is being said, and the caller takes the screen
down. When a line IS up the strip is a window of exactly its own size, rooted in
an `area` with `interactable = false`, so it floats over whatever card is up and
lets every click through to the row under it.
Both halves of that matter. The strip sits on the layer above every card, and a
window on that layer wider than the strip is what a hit test finds wherever it
looks — which leaves every row of the card underneath unreachable by a mouse and
a finger for as long as anyone is speaking.
## Placement
Where it sits is a fraction of the LIVE frame, through `Core.frame(override)`.
In battle its bottom edge rests at 0.565 of the height — clear of the clown's
plate above it and of the rail of counters below it — and everywhere else it
drops to 0.93, under whatever card is up.
## Who consumes it
`game.ui.Screens`, and nothing else.
# Core
The one table every part of GREASEPAINT reads. It carries the game's
constants — the cards, the four clowns, the barker's lines, the palette and
the fonts — the live state of the night, and the event bus the game talks
over. Every other module requires this and nothing else of each other.
`game.rules.Director` is the **only** writer of `Core.state()`. Everything
else reads the state, listens on the bus, and asks the Director for changes
through `Core.cmd`.
```luau
local Core = require("game.Core")
Core.state().nerve -- read the night
Core.on("clown.hit", function(p) ... end) -- react to it
Core.cmd("playCard", { index = 2 }) -- ask the Director to act
```
## Constants
- `Core.RULES` — Nerve, Breath, hand size, the Laughter ceiling and where the
meter carries to after a laugh, how warm the house can get and what a step
of heat is worth, what a held Grin bites, how many acts a night runs, which
act's interval takes a card away instead of giving one, the starter deck
and the reward pool.
- `Core.CARDS` — the twelve cards, each with `kind` (`gag` / `guard` /
`trick` / `curse`), `cost`, and whatever it does: `dmg`, `block`, `heal`,
`laughter`, `effect` (`weaken` / `reflect` / `burn`), `exhaust` when playing
it takes it out of the act, and the `text` its face prints.
- `Core.CLOWNS` — Pinch, Bello, Slats and Mr. Marrow: `hp`, a looping
`pattern` of intents, and three spoken `lines` (`enter`, `laugh`, `die`).
- `Core.RUN` — the three-clown pool and the finale.
- `Core.BARKER` — the Ringmaster's four narrated lines, and `heat`, the one
he marks each step of the warming house with.
- `Core.PALETTE` / `Core.FONTS` / `Core.ANCHORS` / `Core.SEAT` /
`Core.PHASES` / `Core.EVENTS` / `Core.COMMANDS`.
## State
- `Core.state()` — the live night. Phase, act, turn, Nerve, Breath, Block,
Laughter, the `heat` the house has reached, the `library` (every card the
night owns) and the working piles drawn from it (`deck`, `hand`, `discard`,
`exhaust`), the current `clown`, the `reward` on offer — its `cards`, the
`next` clown it names and its `mode` (`take` or `leave`) — the focused
index, and the `order` the four acts run in.
- `Core.reset()` — replaces the state with a fresh night, in place, so a
reference held across a restart stays valid.
## The bus
- `Core.on(name, fn, tag?) -> unsubscribe` — subscribe. A `tag` makes it
idempotent: registering the same event and tag again replaces the earlier
subscription rather than stacking a second copy, which is what keeps a
reloaded component from handling everything twice.
- `Core.off(name, tag)` — drop a tagged subscription.
- `Core.emit(name, payload)` — fire to every subscriber, in order. A
listener that errors is reported and the rest still run.
- `Core.cmd(name, payload)` — `Core.emit("cmd." .. name, payload)`.
- `Core.listenerCount(name)`, `Core.clearListeners()`.
## Chance
The night's randomness is one stream, seeded per run, so a seed replays a
whole night: the act order, the shuffles, which card the tent paints, which
three cards a reward offers.
- `Core.random()` — the next value in [0, 1).
- `Core.randomIndex(n)` — an integer in [1, n].
- `Core.shuffle(list)` — shuffle in place, return the list.
- `Core.rand(salt)` — a stable value for a salt, independent of the stream.
## The frame
Every screen, plate and card lays out from the live frame, so any shape holds.
- `Core.frame(override?)` — `{ w, h, u, wide }`. `u` is a hundredth of the
short side: the unit every proportion is stated in. `wide` is true from an
aspect of 1.25 up. Pass `{ width, height }` to lay out against a shape the
engine is not running at.
- `Core.rgba(hex, a)` — a palette hex with an alpha, as the `rgba()` string a
widget takes.
- `Core.hairline(n)` — a rule of `n` pixels, never thinner than one whole one.
## Also
- `Core.card(id)` / `Core.clown(id)` — a record, or nil.
- `Core.readingTime(text)` — how long a spoken line should sit on screen, in
milliseconds, when no generated clip has answered for it. Between 1.4 and
9 seconds, whatever the length.