beats
The four moments the camera of *First Light: Goura* takes itself away from Tawpy for. `FloorCam.component` runs them; this module holds their shape.
game.staging.beats
The four moments the camera of First Light: Goura takes itself away from Tawpy
for. FloorCam.component runs them; this module holds their shape.
local Beats = require("game.staging.beats")
cutscene.play(Beats.opening(ctx, narratorLine)) -- the opening
local pose = Beats.seen(t, ctx) -- t runs 0 → 1
The opening — a cutscene
The opening is a real cutscene: a shot list, letterbox bands, a fade up out of
the night's own colour, and the narrator's line as a subtitle. It ends by giving
the game back, however it ended — played out, skipped, or stopped.
Three shots, continuous:
- The crown. A station under the canopy, looking up the lane at the head of the roost tree, rising and pushing slowly in. It sits under the canopy on purpose: the shot has to read at every light the run can open on, and a station out above the canopy is a legible silhouette at dawn and a dark mass at night. Under it there are always trunks, shafts and canopy.
- Down the lane. The camera falls out of the canopy and retreats the whole length of the lane, past her, while the aim comes off the crown and settles on the floor ahead. Every look anchor sits further up the lane than the camera does — that one rule is what keeps the aim from swinging across the frame, and it is the difference between a descent and a whip. She arrives in the last second of it, tiny, at the bottom of the lane.
- The settle. The camera comes down onto exactly the pose the chase camera takes over, with her in frame the whole way, so the hand-off is a continuation and not a cut.
Beats.replayShots is the short form — the settle alone, 2.6 s, no line. The
first night gets the whole opening; every retry after it gets this, because
eleven seconds between two attempts is a wall rather than a beat.
Escape is deliberately not a skip key here (SKIP_KEYS), and
staging.inputMap leaves it off too: Escape is the pause control, and a key
that both ends the opening and opens the pause card lands the player on a menu
they did not ask for.
The three pose beats
Beats.seen, Beats.won and Beats.lost are functions of a normalised clock
and a ctx of live readings. They return { px, py, pz, ax, ay, az, fov }.
| Beat | What it does |
|---|---|
seen | 1.7 s. Drops to the floor behind her and looks past her INTO the sweep, so the shot is her small in the near frame and the eagle's shadow coming on beyond her. Then pushes in twice, a beat apart: the heartbeat. |
won | 7.0 s. Leaves her at the foot of the tree and rises with the flock's boom, out to where the crown stands against the sun that has just cleared it. |
lost | 5.4 s. Does not look away. Climbs and widens until she is a small dark thing on a floor the light has filled. |
seen hands the camera straight back; the two end beats hold their last frame
under the end card until the phase moves on.
No state
Nothing here reads the world — every beat is handed its ctx — so the module
holds nothing a hot reload can lose, and any beat can be evaluated without
playing it. That is how the filmstrips were taken:
local eye, at, fov = cutscene.evaluate(Beats.openingShots(ctx), 5.8)
then capture from that station. It is much faster than watching the shot forty
times, and it does not disturb anyone else playing in the world.
What it needs from the other slices
ctx.crown is where the roost tree's head is. FloorCam reads it from
FL.get("roost") when the floor slice publishes one, and otherwise from its own
crownHeight / crownBeyond fields against FL.LANE. A published
roost.crown() would be the better contract — the opening and the win are
both aimed at a landmark the floor slice owns and moves.
Scoped to this part · feeds back into the world's score.