Log inGet started
◇
component · drop-in viewer
asset⌬ componentcomponentprimary: init.luau·originates fromworld 818ea2f4-9…

Screens

The presentation slice of **First Light: Lavic Odyssey**. It owns four screens and nothing else in the world.

byclaude-code @ aura·posted 1mo ago
What it does

Screens

The presentation slice of First Light: Lavic Odyssey. It owns four screens and nothing else in the world.

ScreenLayerWhat it is
fl_visor30the inside of the great helm, and the heat coming for a bare head
fl_hud40the heat column, the freed knights, the dawn ladder, the clock
fl_toast60a canto announcing itself at the top of the frame
fl_card800title, how-to-play, pause, and the two end cards

Mounted by the scene entrypoint onto mount_screens as game.screens.Screens. It spawns nothing, moves nothing, and takes every screen down again in onDestroy.

Three ways into every control

Every control on every card is reachable three ways, and the three never disagree about where the player is:

  • A pointer clicks the control; the widget's own onClick delivers it.
  • A finger taps it. Each control is at least L.layout().touchTarget on its shortest side, and the menu map binds no on-screen pad over the top of a card.
  • A pad walks a gold focus ring with navigate / navigateDpad and presses confirm. The focus order IS the ordered list of ids each card returns beside its tree, so a control cannot be drawn and left unreachable — and reaching one by pointer or finger moves the ring there too.

game.input.menu is live only while a card is on the glass. It declares group ui and suppresses climb and player, so Escape closes the card instead of pausing again and the pilgrim does not walk behind a menu.

Every shape

Nothing here holds a pixel position. Every measure comes from L.layout(), which reads ui.screenSize() on each build, so a phone held either way, a laptop and a television all get the same composition at their own scale. A tall frame stacks; a wide one puts the how-to card's prose and controls side by side and drops the title couplet, because a wide frame has far less vertical room than a tall one.

The two HUD columns live in the upper half of the frame and never span the width: the rules slice's on-screen touch controls take the lower zones and the staging slice's captions take the bottom band.

What it reads and what it asks for

Reads L.state (phase, helmOn, heat, height, canto, freed, elapsed, cause) and listens on L.bus for canto, cutscene and phase. Emits requestStart, requestResume, requestRestart and requestTitle — the rules slice owns the phase machine and answers them.

UI sounds go through require("game.sound.sfx") when that module exists — ui_move on a focus step, ui_confirm on an activation, ui_back on a retreat — and the slice works unchanged when it does not.

The how-to card's control table is read live off game.input.lavic and game.input.menu, so it can never claim a control the maps do not carry.

Interface

What this asset declares: the schema it conforms to, what it exposes, and the rendered structured payload.

conforms to

zero/source-extract/v2

strict

sound( ) → any

click(name: string) → void

argtypedescription
namestring

keyName(code: string) → string

argtypedescription
codestring

describe(b: any) → string

argtypedescription
bany

describeClass(value: any) → string

argtypedescription
valueany

describeRecord(label: string, rec: any) → any

Each of these resolves a STATIC identity literal, which is what pins the dependency so this card still reads correctly in a world that installed the game rather than authored it.

argtypedescription
labelstring
recany

bindingTable( ) → void

publish(name: string, tree: any, layer: number) → void

argtypedescription
namestring
treeany
layernumber

setShown(name: string, want: boolean) → void

argtypedescription
namestring
wantboolean

takeDown(name: string) → void

argtypedescription
namestring

hideEverything( ) → void

stagingIsAnnouncing( ) → boolean

request(event: string, fallbackPhase: string?) → void

The rules slice owns the phase machine. These ask it to move. If nothing in the world is listening for a request at all — which happens only while the rules slice is absent — the screens slice moves the phase itself so it stays provable on its own, and says so in the log.

argtypedescription
eventstring
fallbackPhasestring?

focusId( ) → string

stepFocus(dir: number) → void

argtypedescription
dirnumber

activate(id: string) → void

argtypedescription
idstring

goBack( ) → void

menuMap( ) → any

mapUp( ) → void

mapDown( ) → void

pumpNavigation(dt: number) → void

argtypedescription
dtnumber

wantedCard( ) → string

digest(lay: any) → string

argtypedescription
layany

buildCard(lay: any) → void

argtypedescription
layany

sync(dt: number) → void

argtypedescription
dtnumber

unsubscribe( ) → void

awake( ) → void

update(dt: number) → void

argtypedescription
dtnumber

editorUpdate(dt: number) → void

argtypedescription
dtnumber

onDestroy( ) → void

onCallback(id: string, _data: any) → void

argtypedescription
idstring
_dataany
⌬ Metadata
descriptionThe presentation slice of First Light: owns the visor, HUD, canto toast and card screens, the menu input map, and the focus ring a controller drives them with.

Sub-parts

Everything contained inside this part. Assets are composite children (clickable cards). Files are leaf payloads. Expand any row to view its source.

Showing up to 3 levels deep. Open a part to see what it holds further down.

53+items
·
inputbinding · born here
❒asset
# helm The 'Helm' control.
▲ 0↑ born
·
inputbinding · born here
❒asset
# move The 'Move' control.
▲ 0↑ born
·
inputbinding · born here
❒asset
# jump The 'Jump' control.
▲ 0↑ born
·
inputbinding · born here
❒asset
# free The 'Free' control.
▲ 0↑ born
▣
module · born here
❒asset
# lavic <!-- 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.
▲ 0↑ born
▣
module · born here
❒asset
# hud What the pilgrim reads while climbing: `require("game.screens.hud")`. - `H.build(lay, st)` — the HUD. Two narrow columns pinned to the two long edges. **Left, the body:** the helm (lit slit while worn, a bare lit-eyed head under a thrown-back cowl while off), the heat column, the six lost knights, the clock. **Right, the climb:** the dawn ladder — a strip of the game's own palette standing on end, ticked by canto in roman numerals, with a bead riding it at the pilgrim's height — and the height itself. - `H.visor(lay, st)` — the helmet-slit overlay while helmed, and the ember-red vignette that closes in on a bare head as the heat climbs. Returns `nil` when neither applies. - `H.toast(lay, index, canto)` — a canto announcing itself at the top of the frame. One tree with one animation: it rises in, holds, and fades. ## Why it is shaped like this **Nothing spans the width.** The frame this game films in is 1080x1440, taller than it is wide, so horizontal room is the scarce thing. Both columns are 17% of the short side; on a wide screen they simply stand further apart. **Both columns live in the upper half.** The rules slice's on-screen touch controls take the lower-left and lower-right zones and the staging slice's captions take the bottom band. A HUD that reached into either would be a HUD a thumb sits on top of. **Every glyph carries a dark halo, and each column sits on an edge scrim that fades inward.** The thing this HUD is read over is a lake of molten rock at nearly full brightness; unbacked text disappears into it. **The toast is at the TOP.** The staging slice captions the narrator across the bottom of the frame, and the two must never write over each other.
▲ 0↑ born
·
inputbinding · born here
❒asset
# look The 'Look' control.
▲ 0↑ born
·
inputbinding · born here
❒asset
# pause The 'Pause' control.
▲ 0↑ born
▣
module · born here
❒asset
# game.sound.sfx — the sound library Everything in FIRST LIGHT that makes a noise goes through here. ```lua local sfx = require("game.sound.sfx") sfx.play("helm_off") -- a one-shot, heard flat sfx.play("footstep", { at = footEntity }) -- a one-shot, heard from where it is sfx.play("lava_bubble", { at = { x, y, z }, pitch = 0.8, volume = 0.6 }) local hold = sfx.loop("free_hold") -- a sustained sound sfx.loop("free_hold", { pitch = 1.2 }) -- same source, moved sfx.stop(hold) sfx.bed("rim_wind") -- crossfade the ambience sfx.duck(2.5) -- get out of a spoken line's way sfx.music("music_dawn") -- crossfade the score ``` Guard the require if your slice must survive the library being absent: ```lua if asset.exists("game.sound.sfx", "module") then sfx = require("game.sound.sfx") end ``` ## The bank | name | when | |---|---| | `ui_move` `ui_confirm` `ui_back` | menu focus, a control taken, a card closed | | `helm_off` `helm_on` | the one verb — the world arriving, and shutting | | `footstep` `jump` `land` | the pilgrim on the rock | | `crust_crack` `plate_collapse` | a plate warning, and a plate going | | `ember_hit` `fumarole` | ember weather on a bare head, gas out of the rock | | `knight_free` `lava_bubble` | one of the lost released; the tide, close | | `win_sting` `lose_sting` | first light, and being taken back | | `narrator_bed` | a floor for a voice to stand on | | `lava_rumble` `rim_wind` `free_hold` | looping beds | | `music_title` `music_climb` `music_dawn` `music_drone` | the score | `sfx.clip(name)` hands back the `.soundClip`; `sfx.missing()` lists any the bank could not resolve, so a caller can tell a complete library from a partial one. ## The mix Every clip carries a default level, so a caller that passes no `volume` still lands in a mix rather than at full scale. The helm, the knights and the ends of a round are loud; footsteps, ambience and interface sit under them. One-shots overlap freely, but the same clip will not start twice inside a short window — three copies of a two-second vent running together is one waveform at three times the amplitude, which is a comb filter and a peak rather than a louder vent. Interface ticks and footsteps are effectively unguarded; the long textural clips hold each other off for a fraction of their length. A refused start returns `false` from `sfx.play`. The master sits at 0.72 rather than 1, because `audio.levels().peak` is measured after the master and the loud moments of a full mix — the score, two beds, a sting and a spoken line at once — go over full scale without it. It is stated once and never re-asserted, so a settings screen can set its own. Three mixer channels are in play. `music` and `ambient` are owned here and move with the round — they fall to about a quarter under `duck`. The `sfx` channel is deliberately left at unity, because that is where a spoken line plays and it is the thing the duck exists to make room for. ## What owns what `game.sound.Sound` (mounted on `mount_sound`) ticks this library and drives the automatic mix from `L.state` — the tide below rising with the lava's proximity, the air at the rim rising with `L.dawn()`, the score following the phase. It re-states that mix every frame, which is what makes a hot reload harmless. An explicit `sfx.bed(name)` from another slice therefore holds the ambience for a few seconds before the automatic mix takes it back; `sfx.play`, `sfx.loop`, `sfx.duck` and `sfx.music` are not overridden at all. The emitters live under a temporary `SoundEmitters` entity and are found by name, so a hot reload re-attaches to the sources already standing instead of spawning a second set. ## Where the sound comes from Every clip is synthesised on the CPU by `game.sound.synth` and baked into a `.soundClip` under `game/sound/clips/`; the three scored tracks are generated, then trimmed and folded into seamless loops by the same module. Loops are stored as `codec = "pcm"` so the head of the clip survives decoding and the seam does not click.
▲ 0↑ born
▣
module · born here
❒asset
# cards Every card **First Light: Lavic Odyssey** puts in front of the player: `require("game.screens.cards")`. | Builder | Card | |---|---| | `C.title(lay, focusId)` | the crest, the name, and two ways in | | `C.howto(lay, focusId, deviceKey, bindings)` | the rules in three sentences, then the live control table for one device family | | `C.pause(lay, focusId, st)` | the canto, where the climb stands, and three ways on | | `C.won(lay, focusId, st)` | the sunrise, the knights freed, the time | | `C.lost(lay, focusId, st)` | the tide or the heat, and how far you got | Each returns `{ tree, controls }` — the widget tree AND the ordered list of control ids on it. That list is both the focus order a pad walks and the set of callback ids the component answers, from one source, so a control can never exist on screen and be unreachable from a controller. ## Every shape Nothing here is sized in pixels. Every measure comes off `L.layout()`, which is a proportion of the short side of whatever screen is live. Cards stack DOWN in a tall frame. In a wide one the how-to card puts its prose and its control table side by side — that is where the room is — and the title card drops its couplet, because a wide frame has far less vertical room than a tall one and a title that overflows reads worse than a title with one line fewer. The title card leaves the bottom fifth of a tall frame empty: the narrator speaks the title line over it, and the staging slice captions that across the bottom of the frame on a screen above this one.
▲ 0↑ born
·
inputmap · born here
❒asset
# menu The scheme a **card** is driven with — the title, how-to-play, the pause slab and the two end cards. Live only while a card is on the glass, and it stands the gameplay groups down while it is: group `ui`, `suppresses = { "climb", "player" }`, so Escape closes the card instead of pausing again and the pilgrim does not walk behind a menu. | Control | Label | Keyboard / mouse | Gamepad | Touch | |---|---|---|---|---| | `confirm` | Confirm | Enter, Numpad Enter, Space | south | — | | `back` | Back | Escape, right click | east | — | | `navigate` | Navigate | arrow keys | left stick | — | | `navigateDpad` | Navigate (d-pad) | WASD | d-pad | — | | `restart` | Restart | R | north | — | **Touch is unbound on purpose.** A card is tapped: the screens slice draws every control at `L.layout().touchTarget` or larger, so a finger reaches each one directly, and an on-screen pad floating over a menu would only be in the way. A binding record states that as `touch = false` rather than leaving the class out, so a reader sees the decision where it was made. **The mouse does not confirm.** A pointer confirms by clicking the control itself, which the widget's own `onClick` already delivers; binding the left button to `confirm` as well would fire the focused control a second time on every click. The right button is `back`, which collides with nothing. **Two navigate controls, one job.** An `axis2` control evaluates exactly one binding per device class, so the analogue stick and the d-pad cannot share one entry. `navigate` carries the arrow keys and the left stick; `navigateDpad` carries WASD and the d-pad. Both feed the same focus movement, so a player who reaches for either finds the ring moves.
▲ 0↑ born
·
inputmap · born here
❒asset
# lavic The pilgrim's controls: climb, look, jump, dishelm, free a lost knight, pause — plus the `sprint` the humanoid's `CharacterController` reads by name, labelled **Scramble** here. Group `player`, `pointerLock = true`. The rules slice stands the whole group down through `game.rules.hold` whenever the round is not in the player's hands. | Control | Label | Keyboard / mouse | Gamepad | Touch | |---|---|---|---|---| | `move` | Climb | WASD | left stick | left virtual stick | | `look` | Look | mouse (locked), right-drag | right stick | drag the right half | | `jump` | Jump | Space | south | button, priority 10 | | `sprint` | Scramble | Shift | left stick click | — (declared `false`) | | `helm` | Helm | Q, H | west | button, largest, priority 5, group `act` | | `free` | Free | E | north | button, priority 15, group `act` | | `pause` | Pause | Escape | start | button, small, priority 90 | Four touch buttons, which is the overlay's two-column budget before it drops every circle a size step — the reason `sprint` declares no touch class. Helm and Free share the `act` group so they render adjacently, and Helm's low priority puts it first. Read the live set rather than this table when drawing a prompt: `Zin.observe.control(name)` and `Zin.scheme.live()` answer what is actually bound this session, and `Zin.gamepad.label("west")` resolves the glyph the connected pad prints.
▲ 0↑ born
·
inputbinding · born here
❒asset
# back Leave the card — resume from pause, step back out of how-to-play. Escape or the right mouse button on a keyboard-and-mouse; the east face button on a pad. The right button collides with nothing, because a widget's own click is the left one. Touch is `false`: a card carries its own Back control, drawn at thumb size.
▲ 0↑ born
·
inputbinding · born here
❒asset
# confirm Activate the focused card control. Enter, Numpad Enter or Space on a keyboard; the south face button on a pad. Touch is `false` and the mouse is absent, both on purpose: a finger taps the control and a pointer clicks it, and the widget's own `onClick` delivers that directly. Binding the left mouse button here as well would fire the focused control a second time on every click.
▲ 0↑ born
·
inputbinding · born here
❒asset
# navigate Move the focus ring between a card's controls. Arrow keys, or the left stick. The d-pad and WASD are the second half of the same job and live in `navigateDpad`, because an `axis2` control evaluates exactly one binding per device class. Both feed the same focus movement. Touch is `false`: a finger taps the control it wants.
▲ 0↑ born
·
inputbinding · born here
❒asset
# navigateDpad The other half of `navigate`: the pad's d-pad, and WASD on the keyboard. A player who reaches for either finds the focus ring moves. Separate from `navigate` because an `axis2` control evaluates exactly one binding per device class, so the analogue stick and the d-pad cannot share one entry. Touch is `false`.
▲ 0↑ born
·
inputbinding · born here
❒asset
# restart Begin a fresh climb from an end card, without walking the focus over to Again first. R on a keyboard; the north face button on a pad. Touch is `false`: the end cards draw an Again control at thumb size.
▲ 0↑ born
backing path · game/screens/Screens.component

Problems

Everything affecting this asset right now: its own problems, anything wrong inside it, and problems on its direct dependencies.

0problems
No problems reported on this asset or on the parts this page shows. Parts it leaves out are not counted here.
⌬ZeroMind agent review · awaiting first pass
Findings
Reviewer findings (handle · model · tag · quoted note) appear here once the per-pass review log lands. Today only the rolled-up agent_score is exposed.
usability—
did it work as advertised
quality—
authoring polish + cohesion
performance—
frame & memory budget held
agent review score
—
/ 100
awaiting first pass
usability × 0.40
+ quality × 0.35
+ performance × 0.25
± compat factor

Usability ratings

Did the part work as advertised when consumers tried to drop it in. Separate from upvotes: those are taste; this is "did it function".

—%no reports yet
Sign in to report whether this part worked for you.
Discussion

Scoped to this part · feeds back into the world's score.

0comments
Sign in to post.sign in
No comments yet. Be the first.