mcui
The design system every authored screen of *First Light: Sabaean Madcaps* is drawn out of.
mcui
The design system every authored screen of First Light: Sabaean Madcaps is drawn out of.
local U = require("game.screens.mcui")
Everything visual that more than one screen needs lives here: the palette in its hex form, the type scale, the Sabaean ornament, the plates, the focus ring, the device glyphs and the game's own marks. Nothing here registers a screen or reads game state — it builds nodes and returns them.
Measure
Every size comes through U.u(fraction), a fraction of the short screen
axis, so one tree composes at 1080×1440, at 1920×1080 and on a phone held either
way. The only pixel constants in the slice are the physical floors — U.TMIN
(a reader's eye) and U.TAP = 48 (a reader's thumb) — because those are real
sizes and do not shrink with the screen.
U.size()— the shape to lay out against.U.previewSize— set it, build a tree, read the numbers back, clear it. That is how a set of screens authored in a 3:4 frame is proved to hold on a 16:9 one without a second engine to look at.U.portrait(),U.cardWidth(),U.typeSize(scale).
The short axis of 1920×1080 is the same 1080 as the short axis of 1080×1440, so a card is exactly as tall on a television as on a phone while the television has a third less height to put it in. Height is the axis that fails; the wide forms give it back by shrinking the marks and turning the how-to card into two columns.
Colour and type
U.C is MC.PALETTE plus the greys a UI needs that a world does not — real
steps in lightness, because depth on a near-black screen comes from a step and a
lit top edge, not from a black shadow.
U.FAMILY reconciles MC.FONTS (which names the faces by their ASSET name)
with the family names the renderer matches on (font.observe()), which are not
always the same string. U.DISPLAY / U.BODY / U.LABEL / U.MONO are the
four resolved stacks, each ending in a browser-safe fallback.
Atoms
- Text —
U.text,U.para(wraps; a paragraph always ships inside a full-width box of its own),U.eyebrow,U.railLabel(the same cap line at the width a HUD rail has). - Ornament —
U.glyphBand(eight musnad-style marks, cut with a compass and a straight edge, set as a band the way a Sabaean mason ran a dedication along a course),U.rule,U.diamond. - Marks —
U.templeMark(the seven terraces of Almaqah, cut fromMC.TEMPLE, with the flights turning on alternating faces,litfor the altar catching andtaperfor somebody else's fire),U.censerGlyph(height, flame),U.brazierGlyph,U.dawnGlyph,U.priestGlyph(smug, …),U.pauseGlyph,U.chevron. - Surfaces —
U.plate,U.hudPlate,U.parch(parchment: what a temple keeps is a written record),U.scrim. - Controls —
U.item(the one shape every card line uses; carries the focus ring, states hover and press away from it, and is a real tap target),U.cap(str, kind, onLight). - Motion —
U.entrance(delay, fromY),U.breathe(duration, low). U.mix(a, b, t)— two#rrggbbstrings and how far between them, for a mark that wants a graded set of stones.
U.live
The little bit of screen state that has to outlive a hot reload. A component's
module-level locals belong to the chunk that declared them, and reloading a
component reconciles the live instance in place without running awake again —
so a bus handler registered there goes on writing into the previous chunk's
locals. A table exported from a module keeps its identity across a reload, which
makes this the one place that survives. Screens.component keeps its bus
handles, its live schemes and the spoken line here, and also publishes
live.card and live.focusId so a test can assert on the ring rather than
squint at a picture of it.
Scoped to this part · feeds back into the world's score.