Staging
The beats and the voice of First Light: Lucina.
Mounted once, on mount_staging. It puts nothing in the lamp room and owns no
geometry: it listens to the night and answers with a shot and a line.
The beats
| Beat | What starts it | What it is |
|---|---|---|
opening | the phase reaches opening (the first plate of a run) | Up at the dead lens while the Keeper says what has happened, then a crane back through the room and a settle onto the plate. |
firstLight | the phase reaches fixing, or dawnCrest fires | The camera turns to the north window as dawn crests, then comes down the plate with the sweep that fixes the glass. |
relight | the phase reaches won | Up to the lens, the lens takes the light (Lens.lit 0 → 1 over ~2 s), then back off so the beam has room. |
lost | the phase reaches lost | Off the plate and up, turning to a dawn that is coming whatever happened in here, and out to black. |
One beat at a time. An ending (relight, lost) displaces whatever is on
screen; anything else waits its turn, and a beat already waiting is not queued
twice.
The opening is the one beat the night waits on: however it ends — played out or
skipped — staging emits uiRequest("skip") so the phase machine moves on.
Skipping
Every beat is skippable, from all three families at once, through
staging.inputMap:
| Family | Control |
|---|---|
| Keyboard & mouse | Space, Escape, Enter, or a click |
| Controller | the lower face button, or Start |
| Touch | the Skip button, large, in the thumb corner |
That map is live only while a beat is running, and it stands down the player
and ui groups while it is — so the night's controls and the card controls
leave the screen along with the gameplay camera, and come back with it.
A skipped beat keeps its decisions and drops its remaining lines. The cutscene runtime fires every cue between here and the closing fade, which is what makes the lens end lit whether or not the ending was watched; a line cue checks the clock actually reached it before speaking, so what has not been said yet is not said over the game the player has just been handed back. The line mid-delivery stops with the shot it belonged to.
Every shape of screen
No station in a beat is a fixed camera position. Each is a direction from what
the shot is about and a distance solved from the live ui.screenSize() against
the narrower of the frame's two half-angles — so a tall frame stands back for a
wide subject and comes closer for a tall one, by itself. game.staging.beats
holds that solve, and clamps every station into the lamp room, widening the lens
by exactly as much as the clamp took away. Measured at 3:4, at 16:9 and at a
19.5:9 phone, every station lands inside the room and every subject stays framed.
The voice, and the reading of it
One speaker: the Keeper, over the speaking tube from the watch room below. Her
whole script is game.staging.lines, in text, and game/staging/voice/ holds
one .soundClip per line. Whichever arrives, the words reach the player:
- Inside a beat, the line is a cutscene subtitle — drawn inside the frame,
clear of the letterbox bands, sized for the frame it is read in. The cutscene
is the one renderer while it runs, so
Core.state.subtitleis leftnilrather than putting a second copy on screen. - Outside a beat, over live gameplay, the line goes into
Core.state.subtitlefor the screens slice's strip, and staging draws a strip of its own only for as long as the words are painted nowhere else. That is read off the live screens rather than assumed from which slices exist, so the line is on screen exactly once whoever is drawing it.
A line whose recording never landed is still said: the subtitle stands for its reading length and the beat is exactly as long as it would have been.
The voice plays on a non-spatial Audio under the mount, on the voice
mixer channel, so the sound slice's mix can put the Keeper over everything else.
What it listens to, and what it says
Reads, on the game.core bus:
phase · levelStart · dawnCrest · doseWarning · plateFixed · say ·
uiRequest("skip")
Emits:
beat(name)— a beat took the camera;beat("")— it gave it back. A slice drawing a card or a HUD should hold it while this is not empty: the cutscene camera and the game's screens are drawn into the same frame, and the engine does not take content UI off a cutscene (dropping theuilayer would take the subtitle with it).uiRequest("skip")— the opening is over, move the night on.sfx(name)—dawn_crestas first light crests,relightas the lens ignites,night_endsas the room goes.
Writes Core.state.subtitle, and drives Lens.lit on mount_hero when the
hero slice has mounted it.
Public
| Field | What it holds |
|---|---|
beat | the beat running right now, empty while the camera belongs to the game |
lensLit | how far the lens has been driven toward lit, 0..1 |
voiceVolume | playback level of the Keeper's voice |
manualBeats | set to hold the phases from starting their own beats, leaving playBeat the only way in. Stated as an opt-out: a public field carries across a hot reload and serialises with the entity, so a value that drifts should leave the beats running rather than silently staging nothing. |
| Method | What it does |
|---|---|
playBeat(name) | play one now, whatever has already played |
skip() | skip whatever is on screen, the way a player's Skip control does |
sayLine(id) | say one of the Keeper's lines: the recording, and the words |
report() | the beat, the queue, the line, the lens, and how many lines are voiced |
local staging = entity.find("mount_staging").component.get("Staging")
staging:playBeat("relight")
staging:report() --> { beat = "relight", linesVoiced = 10, linesTotal = 10, … }
Scoped to this part · feeds back into the world's score.