# 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.
# menu
Card navigation for RED DOT RISING.
Live only while a card is up — the title, the how-to-play card, the pause
card, WIN and MORNING SHIFT. It belongs to the `ui` group and stands down
both the builtin `player` scheme and the game's own `rdr` scheme, so
nothing walks, looks or fires while a menu is open, and the on-screen
touch controls leave the screen with them.
| Control | Keyboard / mouse | Gamepad | Touch |
|---|---|---|---|
| `navigate` | Arrow keys | D-pad | tap the row |
| `navigateStick` | W A S D | Left stick | tap the row |
| `confirm` | Enter, Space | South (A / Cross) | tap the row |
| `back` | Escape, Backspace | East (B / Circle) | the BACK row |
| `unpause` | P | Start | the pause pip |
Touch is unbound on every control on purpose: a finger drives these cards
by tapping the row it wants, which is that row's own `onClick`. A virtual
stick and four buttons floating over a menu would be four controls with
nothing to do.
`navigate` and `navigateStick` read the same axis so a pad walks a menu
with either hand; the `ui` slice feeds whichever is pushed harder into
`Focus.axis`, so holding both does not walk two rows at once.
# Staging
RED DOT RISING's director, mounted on `mount_staging` by the scene entrypoint.
It stages three moments, each over the player's own CameraRig and each handed
back when it ends:
| moment | when | what happens |
|---|---|---|
| the opening | the game first leaves the title card | the NEKO-9 sign in the rain, over the parapet, down the lift shaft, into the keeper's eyes; the phase goes `intro` then `play` |
| the lift beat | `lift.start` on the bus | two seconds across the shaft as the platform wakes, then straight back |
| the ending | `game.won` / `game.lost` | a slow move onto the loft bed at sunrise, or the house lights washing the keeper's own view white |
Every beat is skippable from a keyboard (Space, Enter, Escape), from a pad
(South, Start) and from a touchscreen (the on-screen SKIP button), through
`game/staging/staging.inputMap`. While a beat runs that map stands the
`player` and `rdr` control groups down, so the keeper cannot walk, look, fire
the pointer or whistle, and the game's own touch buttons leave the screen.
## Fields
| Field | Default | Notes |
|---|---|---|
| `autoIntro` | `true` | Play the opening when the game first leaves the title card. Off leaves it to an explicit `game.staging.staging` `playIntro()` call. |
## What it announces
Each beat brackets itself on `Core.bus` with `beat` events — `intro.start` /
`intro.end`, `lift.start` / `lift.end`, `end.start` / `end.end` — so the HUD
can step out of the way and the sound can time a line. The closing event
carries a `reason`: `completed`, `skipped`, `displaced` or `stopped`.
The behaviour lives in `game.staging.staging`; this component is the mount and
the lifecycle. The camera paths are data in `game.staging.shots`.
# cards
The full-screen cards: the title, how-to-play, pause, WIN and MORNING
SHIFT. Each builder is a pure function of (metrics, state), so the
component can rebuild a card on any change and get the same tree.
Every row carries an `id` of the form `row-<action>`. That one name is what
the Focus module walks, what `onCallback` receives when a finger or a mouse
hits the row, and what a pointer crossing the row re-points the focus to —
so a pad, a keyboard, a mouse and a finger all arrive at the same action.
The entrance is driven, not declared: `setEntrance(t)` takes the seconds
since the card opened and every block fades up and settles on a stagger of
that number.
# Mix
Everything RED DOT RISING is heard through.
`Mix` owns one deck of long-lived `Audio` sources under the sound mount, the
channel levels they are balanced at, and the verbs the rest of the game calls
to make a noise.
```lua
local Mix = require("game.sound.Mix")
Mix.ui("confirm") -- a menu sound: "move" | "confirm" | "back"
Mix.sting("win") -- an ending: "win" | "lose"
Mix.say("brief") -- Hoshi over the intercom; returns the line's seconds
```
## The deck
`Mix.attach(root)` builds six sources under `root` and `Mix.detach()` takes
them away again; the `Sound` component calls both, so nothing else needs to.
| source | plays | channel |
|---|---|---|
| `snd_bed_rain` | rain on the skylight, looping | `ambient` |
| `snd_bed_neon` | the building's mains hum, looping | `ambient` |
| `snd_music` | the night piece, looping | `music` |
| `snd_ui` | menu one-shots | `sfx` |
| `snd_sting` | the win and lose endings | `sfx` |
| `snd_voice` | Hoshi's lines | `voice` |
Every source is 2D: the bed, the score and the intercom are the building
itself, heard the same wherever the keeper is standing. Spatial sound belongs
to the thing making it — a cat, a plate, the pointer.
## The room changes as the tower is climbed
`Mix.setFloor(n)` tilts the bed per floor: the Lobby is deep inside the
building, so the rain arrives through glass and the mains hum is close; the
Rooftop Loft is out in the weather. `Mix.setDawn(t)` thins the score and
brings the rain forward as the night runs out. The `Sound` component drives
both from `Core.state`.
## Ducking
`Mix.duck(true)` steps the bed and the score back; `Mix.say` and `Mix.sting`
do it themselves for as long as the clip lasts. `Mix.tick(dt)` walks the gain
there and back and is called once a frame by the `Sound` component — no timer,
no task, so a hot reload leaves nothing running.
## The voice
`Mix.LINES` holds Hoshi's script: the words and the measured length of each
generated clip. `Mix.say(id)` plays the clip AND emits `voice.line` on
`Core.bus` carrying `{ id, text, seconds }`, so the ui draws the words whether
or not the audio is there. A line with no clip is a quiet line, never a lost
one.
## Reading it back
`Mix.observe()` reports the deck, the bed and score state, the floor, the
dawn, the duck gain, and what the mixer actually hears.
# theme
The look of every screen in RED DOT RISING, in one place: the palette, the
type scale, the motion helpers, and `metrics()`.
`metrics()` reads `ui.screenSize()` afresh on every call and hands back the
numbers a tree sizes itself from — a scale `k` taken from the SHORT axis of
the live screen, a `pad`, a `gap`, a card width, and `fs(n)` / `px(n)` to
put a design number through `k`. Nothing in the game is a pixel position
chosen for one frame; that is what lets the same trees lay out at 810x1080,
at 1920x1080 and on a phone held either way.
The palette is derived from `Core.C` so the HUD and the room agree on what
red is. `M.entrance(t, order, dy)` and `M.enter(style, ...)` drive a card's
entrance from Luau a frame at a time, because a one-shot CSS `animation`
plays once per element ID and does not replay on a re-push of the same
tree — and a card's tree is rebuilt every time the lit row moves.
`M.pulse` stays declarative, because an infinite animation is the case
that repeats.
local T = require("game.ui.theme")
local m = T.metrics()
{ type = "label", props = { text = "DAWN" }, style = T.kicker(m, T.CYAN, 11) }
# hud
The four corners of the playing screen, as four separate anchored screens:
the cats top-left, the dawn meter and the tower top-right, the crosshair
dead centre, Hoshi's captions along the bottom.
Four screens rather than one, because an anchored root occupies only its
own content: nothing here covers the viewport, so the pointer reaches the
game and the laser's left button is never eaten by a transparent HUD box.
It is also why each part re-renders when ITS data changes rather than all
four whenever any of it does.
An anchor stretches its child across the space it was given rather than
hugging it, so every part puts a full-width rail inside the anchor and
pushes its content to the edge the anchor names.
# core
<!-- 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.
# controls
What the player is holding, and what it says on it.
The control table lives here as data, once, so the how-to-play card, the
row hints on every menu and the pause card all read the same row and none
of them can drift from the others. The device family is asked of the input
layer rather than guessed, and a pad's face-button legends are resolved
from the pad that is actually connected — A on an Xbox pad, Cross on a
PlayStation one.
local controls = require("game.ui.controls")
controls.family() --> "kbm" | "gamepad" | "touch"
controls.hint("confirm") --> "ENTER" | "A" | "TAP"
# rdr
The control scheme for RED DOT RISING: hold the laser, whistle the cats, work a
mirror or the lift, pause.
| Action | Keyboard / mouse | Touch | Controller |
|---|---|---|---|
| `laser` (hold) | left mouse button | LASER button, right-lower | right trigger |
| `whistle` | Q | WHISTLE button | left trigger |
| `interact` | E | the builtin INTERACT button | West (X / Square) |
| `menu` | Escape | MENU pip | Start |
Walking, looking and running are the builtin `default` scheme's `move`, `look`
and `sprint`, which the avatar's character controller and first-person camera
hold live under this map on all three device classes. This map composes with it
rather than standing it down, so a thumb on the on-screen stick moves the keeper
through the same path a W key does.
Two bindings are shaped by what the builtin scheme already reads:
- `whistle` is on the LEFT TRIGGER rather than a shoulder. The builtin `zoom`
reads both shoulders, so a whistle bound there pulls the camera in every time
the keeper calls the cats.
- `interact` declares no touch class. The builtin scheme draws an INTERACT
button of its own, and the laser slice holds a claim on that scheme as well so
the button on screen reaches the mirror. A second circle with the same word on
it reads as a second action.
The `laser` slice owns this map and activates it for the whole run. The `menu`
scheme the `ui` slice owns names this map's group, `rdr`, in its `suppresses`,
so nothing here answers while a card is up.
# back
<!-- zero:scaffolded-readme -->
<!-- Replace the line below with what this inputBinding provides and who
consumes it, then delete both comment lines. -->
TODO: describe this inputBinding.
# confirm
<!-- zero:scaffolded-readme -->
<!-- Replace the line below with what this inputBinding provides and who
consumes it, then delete both comment lines. -->
TODO: describe this inputBinding.
# navigate
<!-- zero:scaffolded-readme -->
<!-- Replace the line below with what this inputBinding provides and who
consumes it, then delete both comment lines. -->
TODO: describe this inputBinding.
# navigateStick
<!-- zero:scaffolded-readme -->
<!-- Replace the line below with what this inputBinding provides and who
consumes it, then delete both comment lines. -->
TODO: describe this inputBinding.
# unpause
<!-- zero:scaffolded-readme -->
<!-- Replace the line below with what this inputBinding provides and who
consumes it, then delete both comment lines. -->
TODO: describe this inputBinding.
# interact
The 'Interact' control.
# laser
The 'Laser' control.
# menu
The 'Menu' control.
# whistle
The 'Whistle' control.