# Staging
The camera of THE EIGHTH KETYL, and the moments it attends to. It sits on the
`Staging` host entity in `still.scene` and is the only thing in the game that
moves the camera.
## What it does
**The playing shot.** On the first frame it can, it installs `staging.KetylCam`
on the player's `CameraRig` as that camera's `behavior`, in place of the orbital
rig the scene ships with, and points it at whatever `Camera.follow` names. That
component owns the framing; this one owns getting it there and getting it back
after a cinematic.
**The moments.** Five cutscenes out of `staging.Beats`, each triggered by an
event the core slice emits:
| Event | Moment | Runs for |
|---|---|---|
| `runStart` / `phase("intro")` | the opening — the still, before you may move | the core slice's own `INTRO_SECONDS` |
| `plateCleared(n)` | the push onto the riser as it lights (plates 1–7) | 2.7 s |
| `ascend(n, to)` | up through the riser onto the plate above | 2.6 s |
| `sealed()` | the win — in on the ketyl, then the whole column | 11.6 s |
| `quenched()` | the loss — close, then away and down | 6.6 s |
Plate 8 gets no clear beat: the seal is arriving a moment later and two
cinematics on one beat cut each other off.
**The lines.** It cues spoken lines by id through `staging.Voice` and never
writes the words — the sound slice owns those. The opening cues `v_open_1..3`,
an ascent cues that plate's `v_plate_n`, a clear cues `v_clear`, the endings cue
`v_quench` and `v_seal_1` / `v_seal_2`. Wetness crossing 72% of maximum cues
`v_wet` and charge falling under 18% cues `v_low`, each once per crossing.
**Its own sound.** Three clips under `staging/audio` play under the beats,
below whatever the sound slice plays on the same moment: `stg_frame` when a
moment takes the frame, `stg_rise` on an ascent, `stg_reveal` on the seal.
## Events
Emits `shot(name)` when a moment takes the camera — `"opening"`, `"plateClear"`,
`"ascend"`, `"sealed"`, `"quenched"` — and `shot("free")` when one gives it
back. A screen that should not land on top of a resolution waits for `"free"`
rather than for the phase, which changes while the shot is still running.
Emits `line(id, text, speaker)` and `lineEnd(id)` through `staging.Voice`.
Listens for `runStart`, `phase`, `plateEnter`, `plateCleared`, `ascend`,
`sealed`, `quenched`, `wetness` and `charge`.
## Skipping
Every moment but the opening can be skipped from all three device families: for
the length of a moment this component holds the `console` input map, whose
`confirm`, `back` and `resume` each carry a key, a pad button and an on-screen
button. Map holders are refcounted, so this takes nothing from the slice that
owns that map for menus.
The opening is the core slice's to end — its `pause` control leaves the intro
phase, and this component skips the camera when the phase moves to `play`, so
the two clocks cannot disagree.
## Fields
| Field | Default | What it does |
|---|---|---|
| `cinematics` | `true` | Off, the game plays with no moments at all. The playing camera still works — for filming the loop with nothing in the way. |
| `beatVolume` · `riseVolume` · `revealVolume` | 0.55 · 0.62 · 0.50 | The camera's own sounds, under the mix. |
| `wetWarnAt` / `wetRearmAt` | 0.72 / 0.55 | Where `v_wet` fires and re-arms. |
| `lowWarnAt` / `lowRearmAt` | 0.18 / 0.38 | Where `v_low` fires and re-arms. |
## Rehearsing
```lua
local staging = require("Still").anchor("Staging").component.get("Staging")
staging:rehearse("sealed") -- watch a moment now
staging:rehearse("plateClear", 5) -- ... on a particular plate
staging:release() -- take it off and give the shot back
staging:report() -- the moment, the rig, the live framing
```
# Still
One-paragraph description of what this module provides and who consumes
it.
## Exports
- `M.exampleFn(arg) -> result` — what it does and when to call it.
- `M.exampleValue: type` — what value it carries.
## Usage
Replace `<identity>` with the string `asset.create` returned — this
module's source path with `/` written as `.` and each segment's type
suffix dropped, the way a module at `/source/lib/thing.module` is
`lib.thing` and one at `/source/kit.package/thing.module` is
`kit.thing`.
```luau
local M = require("<identity>")
M.exampleFn("foo")
```
## Notes
- Anything callers need to know about THIS module: required runtime
ordering, state ownership, side effects.
# Screens
The presentation slice of THE EIGHTH KETYL, on the `Interface` host entity. It
owns every authored screen the game has — the title card, the how-to-play card,
the heads-up display, the pause card and the two end cards — and it owns menu
FOCUS: the ordered ring a controller walks, the ring you can see, and the rule
that a mouse crossing a row moves the focus there so the pad, the keyboard, the
mouse and a finger never disagree about what is selected.
**It never runs the game.** A chosen item leaves here as
`Still.emit("uiIntent", name)` — `"start"`, `"howto"`, `"resume"`, `"restart"`,
`"quitToTitle"` — and the core slice decides what that means. The phase that
comes back is what this component draws. The only game state it touches is
`Still.state`, and only to read it.
**Input.** It activates `/source/input/console.inputMap` while a menu is open —
group `ui`, which stands the whole `player` group down, so the ketyl does not
drift across the plate behind a menu — and releases it the moment the last menu
closes. `navigate` walks the focus, `confirm` acts on it, `back` leaves, and
`resume` closes the pause card. The touch overlay's own Confirm / Back / Resume
buttons come from those same bindings, which is why the menu screen draws BELOW
them.
**Layout.** Every number comes from the live `ui.screenSize()` through `ui.Kit`.
The filmed shape is 1080x1440 and the same screens land on a phone held either
way, a laptop and a television.
## Public fields
- `consoleMap: assetRef(inputMap)` — the menu controls. Defaults to
`input.console`.
- `previewInEdit: string` — an author's window onto the screens while the engine
sits in edit. Empty (the shipping value) means nothing of this slice paints
until play runs, so a title card never covers another slice's scene work. Set
it to `"title"`, `"howto"`, `"pause"`, `"sealed"`, `"quenched"` or `"hud"` to
hold that card on the glass in edit.
- `ownCaptions: boolean` — draw this slice's own caption band. Off, because the
staging slice ships one (`still-captions`) and two bands anchored to the same
edge would print every line twice. Turn it on if that band ever stops
appearing: the words must be on screen whether or not the audio played.
## Lifecycle
- `awake()` — registers the `Interface` anchor, registers the three screens
(`ketyl_hud` at 20, `ketyl_caption` at 40, `ketyl_menu` at 150), subscribes to
`line` / `lineEnd` / `phase`, and drops any control subscription left behind by
a previous instance.
- `update(dt)` / `editorUpdate(dt)` — reads `Still.state.phase`, decides which
card is on the glass, keeps the console map live exactly while one is, lets the
pointer move the focus, and republishes the trees. The menus rebuild only when
something they draw changed; the HUD is live data and rebuilds every frame.
- `onCallback(id)` — every `mi:<item>` a row carries lands here.
- `onDestroy()` — unsubscribes, deactivates the map, unregisters the screens.
## Usage
It is placed by `still.scene` on the `Interface` entity and needs no setup.
```luau
local iface = require("Still").anchor("Interface")
iface.component.get("Screens").previewInEdit = "title"
```
# Column
The still — eight sieve trays stacked inside a glass column, the brass that holds
them, the risers between them, the reboiler underneath and the condenser head
above. It is also every light in the game and the air between them.
Everything stands as children of the `Column` host entity, built from the
geometry constants in `Still.module`, so the trays, the growths and the pilot all
agree about where the floor is. Shapes are assembled as triangles by
`ColumnGeometry` and uploaded one merged mesh per part, which is why eight plates
of trays, weirs, skirts, ribs, risers and a hundred and sixty bubble caps cost a
few dozen draws rather than a thousand entities.
## What is in it
| Part | What it is |
|---|---|
| `still_shell` | the dark cabinet the still stands in, ribbed, inward-facing |
| `still_wall_glass` | the column wall — beaded condensation, a rim that carries the light |
| `still_frame` | a flanged collar at every plate joint, and six tie rods |
| `still_deck_n` | plate *n*'s perforated sieve tray, with the hole the riser below comes up through |
| `still_brass_n` | the weir ringing the tray, and the bubble caps standing on it |
| `still_skirt_n` | the narrow shelf, the six wall brackets, the ribs under the tray |
| `still_hoods_n` / `still_lenses_n` | six hooded inspection lamps on the wall, eight pilots in the tray's underside |
| `still_rim_n` | the lit strip let into the weir, and a slot around every bubble cap |
| `still_riser_n` / `still_riserglow_n` | the brass cage from plate *n* to plate *n+1*, and the violet it fills with |
| `still_reboiler` / `still_ember` | the kettle below plate 1 and the fire in it |
| `still_head` / `still_condenser` / `still_seal_ring` | the dome, its hoops, and the seal |
The annulus between the tray shelf and the wall is deliberately **open**: it is
the channel the shaft is read through, so a camera anywhere near the wall sees
the rims of the plates above receding into the dark.
## What it listens to
It emits nothing. It answers core's events with light:
- `plateEnter(n)` — the plate the ketyl stands on burns brighter than the ones
above it, and its raking lamp comes up with it.
- `riserOpen(n)` — riser *n* fills with violet and breathes. The level is written
into the entity's own instance lane, so one material serves all eight.
- `plateCleared(n)` — a flash through the whole shaft.
- `sealed()` — the ring in the head comes up.
- `phase(name)` — `title` and `intro` light the whole column, for the shot that
establishes it before control is handed over.
## The atmosphere
The sky is off and the clear colour is near black: the still is sealed inside
something and there is no daylight anywhere in it. The ambient is a dim cold
violet; everything else is emissive or a lamp. The post-process chain is fog,
bloom, an ACES tonemap and a vignette, and the interior is baked once into the
reflection environment — a metal with nothing to reflect renders black, and this
still is all metal.
# KetylPilot
The hands of THE EIGHTH KETYL. It turns controls into motion and does nothing
else. It lives on the PlayerPrototype's body, so the engine mints one per
joining player, and only the peer that owns a body reads the local devices for
it.
## What it does
- Makes `/source/input/ketyl.inputMap` live — six controls, each carrying
keyboard, gamepad **and** touch, so activating the map is the whole of "this
game is playable on three device families". The on-screen controls appear by
themselves, built from those bindings.
- Drifts the radical across the plate it is on, in the **camera's** basis, so
"push up" means "away from me" whatever shape the screen is and whichever
device is in the player's hands.
- Faces it where it is going, or — standing still — where the player is looking,
so the discharge arc is aimable from a stick, a mouse or a thumb with no
second aiming control.
- Flies it up a riser when the Conductor grants one.
- Publishes where it is, which way it faces and how fast it is going, as
`KetylCore.body`, so the Conductor can answer for it.
- Stands the engine's own default player scheme down, once a second. Whatever
camera behaviour the rig is wearing brings that scheme up with it, and its
Jump and Sprint buttons do not belong on this game's touch overlay — six
buttons instead of four costs every one of them a size step on a phone.
## What it does not do
It draws nothing: the ketyl slice's `KetylBody` does that, on this same entity.
It moves no camera: the `look` control is handed straight to the staging slice as
`look(v, dt, active)` and never read here. And it decides nothing — whether a
discharge may fire, whether a riser will take you, whether the game is even
listening are all the Conductor's.
## Fields
| Field | What it is |
|---|---|
| `inputMap` | The controls this ketyl answers to. Defaults to `input.ketyl`. |
## Telling a stopped ketyl from a dead one
`KetylCore.body.ticks` rises once per frame this component drives. An engine that
disables a component instance after a runtime error leaves it looking exactly
like a ketyl standing still; a heartbeat that has stopped rising is the reading
that tells the two apart. `component.setEnabled("KetylPilot", true)` brings a
disabled instance back.
# Score
The sound of **The Eighth Ketyl**: the music bed, the still's ambience, every
noise the game makes, the mix, and both spoken voices. One component on the
`Score` host entity; everything it plays hangs underneath as children.
It never runs the game. It listens to the shared event bus in `Still` and
answers with sound, so a slice that emits `growthBurned` gets a growth bursting
without knowing this file exists.
## What it plays
| Child | Channel | What |
|---|---|---|
| `score_bed` | `music` | `bed_music` — four bars of eight, looping |
| `score_ambience` | `ambient` | `amb_still` — the computed hum, boiling and brass |
| `score_room` | `ambient` | `amb_room` — the recorded room, under the computed one |
| `score_voice` | `voice` | whichever spoken line is up |
| `clip_voice_1..14` | `sfx` / `ui` | the one-shot pool `audio.Clips` cycles |
## Public fields
- `master: number` — the whole mix at the master fader (0..1).
- `music: number` — the bed, under everything.
- `ambient: number` — the still itself.
- `sfx: number` — the game's own noises.
- `ui: number` — the interface, quieter than the game because it is furniture.
- `voice: number` — the two speakers, the loudest thing in the mix on purpose.
- `captions: boolean` — this component's own caption band. **Off**: the
presentation slice draws the game's caption from the same `line` event, and
two bands would print every line twice. The words live in `audio.Script`
either way, so turning this on is the fallback if that band ever stops.
## Methods
- `:say(id)` — speak a line by id, the same path the `line` event takes. For
driving one by hand from a test; staging cues lines through the bus.
- `:report()` — what the mix is doing right now: levels, mixer, live voice
count, the duck, the bed's gain and pitch, how many clips are registered, and
which line is up. Check the mix with this rather than assuming it.
## The mix
Foreground is a voice and whatever the player just did; background is the bed
and the still. A spoken line pulls the background down to 30% over about an
eighth of a second and lets it back up over a second and a half — that is the
whole difference between a mix and a pile. Measured at the busiest moment the
game can produce (a line, a discharge, a growth bursting, a splash and a sodium
node together) the master mix peaks at 0.74, so nothing clips.
The bed also follows the ascent: it comes forward and runs a shade faster as the
plate number rises, so plate eight does not sound like plate one.
## Lifecycle
- `awake()` — registers the `Score` anchor and the caption screen, builds the
four sources as children (reusing them if a hot reload left them standing),
seeds the clip library, subscribes to the bus, applies the mix.
- `start()` / first `update` — starts the bed and both ambiences. The still is
never silent, even on the title.
- `update(dt)` — the duck envelope, the bed's arc, the wetness alarm's
quickening pulse, and the mix.
- `onDestroy()` — unsubscribes everything and stops every source.
## Usage
```luau
local score = Still.anchor("Score").component.get("Score")
score:say("v_seal_1")
score.music = 0.2
print(score:report().levels.rms)
```
# BlennomaField
Everything mucous on a plate of the still, and everything sodium.
One instance stands on the `Growths` host entity and builds the whole field of
whichever plate the ketyl is on: the growths, the sodium nodes, and every
transient thing they make — droplets, pools, weals, bursts and scars — as
children of that host.
## What it puts on a plate
| Thing | What it is |
|---|---|
| growth | A blennoma. A closed lumpy solid contoured from `Blennoma.procGraph`, wearing `mucus_flesh`, with four beads of condensation on it and a bead at the teat that fills before it drips. It pulses, it weeps, it chars and swells with weals as it burns, and it dies in a burst. |
| sodium node | A lump of cut sodium from `SodiumNode.procGraph` wearing `sodium_metal`, turning slowly so the knife-cut face catches the light. Taken on contact, and it reforms after `Still.SODIUM_RESPAWN`. |
| droplet | A bead of water off a teat, falling and stretching as it goes. It soaks the ketyl if it lands on it and leaves a pool if it does not. |
| pool | A spreading film on the plate. It grows, it holds, and it shrinks away after `Still.POOL_LIFETIME`. |
| scar | What a burned growth leaves where it stood. It stays for the rest of the plate. |
## What it reports
It knows nothing about health, charge or the run. It reports what physically
happened and lets the core slice price it:
growthCount(alive, total) after a build, and after every death
growthHurt(pos, frac) a growth took damage; frac is the health left
growthBurned(pos) a growth died
dropletSpawn(pos) a droplet left a teat
dropletLand(pos) it hit the plate, or the ketyl
soaked(amount) the ketyl took this much water this frame
sodiumTaken(pos) a node was consumed
## What it listens to
plateEnter(n) build plate n's field
runStart() build plate 1
discharged(pos, dir) decide what stood in the arc and burn it
The arc test reads `Still.DISCHARGE_RADIUS`, `Still.DISCHARGE_ARC`,
`Still.BLENNOMA_BURN` and `Still.BLENNOMA_FALLOFF`, so the reach and the bite of
a discharge are tuned in one place for the whole game.
## Sound
Five clips are synthesised into `/source/growths/` by `bake_sfx.luau` and
registered into the shared library under the ids the game names them by —
`sfx_drip`, `sfx_splash`, `sfx_growth_hurt`, `sfx_growth_burn`, `sfx_sodium`.
Every noise goes out through `audio.Clips`, so the sound slice owns the mix.
Where that library is not loaded the component falls back to a voice of its own
rather than going quiet.
## Determinism
A plate's layout comes from a generator seeded on the plate number alone, so
every peer in this multiplayer world lays the same growths in the same places.
Nothing here reads `math.random`.
## Rebuilding the assets
execute { file = "/source/growths/bake.luau" } -- the meshes
execute { file = "/source/growths/materials.luau" } -- the materials
execute { file = "/source/growths/bake_sfx.luau" } -- the sounds
# Conductor
The rules of THE EIGHTH KETYL. One component, on the `Game` entity, and the only
writer of `Still.state`.
## The state machine
```
title ── "start" ──▶ intro ──(9s, or any control)──▶ play
│
the plate's growths are gone ──▶ cleared
│ │
wetness hit maximum ──▶ quenched ascend │
│ │
plate 8 went clean ──▶ sealed │
◀──────┘
```
`cleared` is a **playable** phase. The plate is done and the riser is lit, but
the ketyl still has to drift to it and commit, so movement, discharge and surge
all answer during `cleared` exactly as they do during `play`. `Still.state.phase`
distinguishes them so a screen can say "the way up is open" without the game
having stopped.
`paused` remembers what it interrupted and returns to it.
## What it emits
`phase` · `runStart` · `plateEnter` · `plateCleared` · `riserOpen` · `ascend` ·
`sealed` · `quenched` · `charge` · `wetness` · `discharged` · `surged`.
`charge` and `wetness` are announced on a real change rather than every frame; a
bar that draws every frame should read `Still.state` directly instead.
## What it listens for
| Event | From | What it does with it |
|---|---|---|
| `uiIntent(name)` | presentation | `start` · `resume` · `restart` · `quitToTitle` |
| `growthCount(alive, total)` | blennoma | writes the count; `alive == 0` lights the riser |
| `soaked(amount)` | blennoma | `amount` is **wetness points** — a pool sends its per-second rate times dt, a droplet sends its whole lump |
| `sodiumTaken(pos)` | blennoma | credits `Still.SODIUM_RESTORE` charge |
It does not know what a blennoma is, where a droplet fell, or what any screen
says. A discharge goes out as `discharged(pos, dir)` and the blennoma slice
decides what stood in the arc.
## When nobody reports a plate
`plateEnter(n)` asks the blennoma slice to build plate *n*'s field. If no
`growthCount` arrives within `KetylCore.GROWTH_GRACE` seconds the plate is
treated as empty and the riser opens, so the column stays climbable while that
slice is mid-build — and it says so at WARN, because a riser that opens for no
reason is otherwise indistinguishable from a plate that was genuinely cleared.
## Callable
The same decisions the controls reach, by name, for a test or anything that has
to drive the game without a pair of hands:
```lua
local cond = Still.anchor("Conductor").component.get("Conductor")
cond:startRun(false) -- begin a run, skipping the opening
cond:toTitle()
cond:discharge()
cond:surge()
cond:ascend()
cond:togglePause()
```
# KetylBody
The ketyl radical's look — the thing the player IS, and the brightest object in
the still.
The player is a sodium benzophenone ketyl: a ketone that took one extra electron
and went deep blue-violet, and that dies the instant it meets water. So the body
is built as the molecule it is rather than as a ball of light. A carbonyl carbon
at the centre, an oxygen off-axis carrying the charge, two phenyl rings in
benzophenone's propeller — each ring six thin glowing rods closing a hexagon —
the C=O drawn as the double bond it is, and one small fast spark on an inclined
orbit for the unpaired electron. Around that: a shell of light with an edge you
can see, a soft aura with no edge at all, and the afterimages the radical lays
down as it drifts.
It reads no input and moves nothing. Where the ketyl IS belongs to `KetylPilot`;
what it LOOKS LIKE belongs here.
## What it listens for
| Event | What it does |
|---|---|
| `charge(v, max)` | the light in it, the speed of the spark and the rings, the pitch of its hum |
| `wetness(v, max)` | takes the colour out first, then the light; the corona breaks into beads |
| `discharged(pos, dir)` | flares, and throws a forward half-circle of cyan-white light out to `Still.DISCHARGE_RADIUS` |
| `surged()` | flares shorter, and lays the trail down four times as fast |
| `quenched()` / `sealed()` / `runStart()` | the two endings and the reset |
`Still.state.charge` / `.wetness` are read directly until the first event of that
kind arrives, so the radical is right on its very first frame rather than after
core has spoken once.
## How one material serves forty parts
Every piece draws through one of two shaders in `/source/ketyl`, and every piece
reads the same per-instance lane:
```
lane 0 = ( charge 0..1, wetness 0..1, flare 0..1, fade 0..1 )
```
`renderer.instanceData.set` writes it. That is what lets one `.material` serve
the nucleus, both rings, the whole trail and the discharge fan while each of them
differs in a number. A block nothing has written yet is all zeros, which would
read as a dead, invisible radical, so both shaders fall back to `(1, 0, 0, 1)`
when the whole lane is zero.
## Fields
| Field | Default | What it is |
|---|---|---|
| `scale` | 1.45 | size, as a multiple of `Still.KETYL_RADIUS` |
| `trailNodes` | 14 | how many afterimages the trail is made of |
| `trailStep` | 0.045 | seconds between trail samples while moving |
| `trailLife` | 0.85 | seconds an afterimage takes to go out |
| `lightIntensity` | 26 | peak intensity of the point light the radical throws |
| `lightRange` | 11 | that light's range, in world units |
| `humVolume` | 0.62 | how loud its own hum sits under the rest of the mix |
| `claimBloom` | true | register a bloom pass if nothing else in the world has |
`claimBloom` yields: it looks for an effect already named `bloom` and does
nothing if one is there, so whichever slice owns the atmosphere wins simply by
registering its own.
## The sound of it
`/source/ketyl/bake_hum.luau` computes `ketyl_hum.soundClip` in Luau — a low
fundamental with its fifth and its octave, three of them half a hertz apart so
the tone beats, and a thin shimmer above. It travels with the body on the
`ambient` channel, under everything else. Charge pitches it up, water pulls it
flat and shuts it, a discharge lifts it.
It is a loop, so every partial is a whole number of cycles across its two
seconds and it is encoded as `codec = "pcm"`. Re-bake it with
`execute { file = "/zero/source/ketyl/bake_hum.luau" }`; it is idempotent.
# Mesh
Mesh asset. `data.zmsh` is the engine-native geometry payload (`renderer.mesh.encode`); consumers reference the asset's guid.
# console
The controls a CARD answers to: the title, the how-to-play handbill, the
interval between two acts, the pause card and the two end cards.
It is live exactly while a card is up — `game.ui.Screens` activates it the
instant one goes up and releases it the instant the last one comes down. Its
group is `ui`, and it stands down `player` and `table`, so nothing behind a card
answers while the card has the player.
| Control | Keyboard | Pad | Touch |
|---|---|---|---|
| `navigate` | Arrows or WASD | Left stick | — the rows |
| `confirm` | Space or Enter | South (A / Cross) | — the rows |
| `back` | Backspace | East (B / Circle) | — the row that says so |
| `resume` | Escape | Start | — the Resume row |
`navigate` binds each direction to two keys at once — the WASD key and the arrow
key beside it — because a player reaching for a menu uses whichever is already
under their hand.
## Why no touch class
Every row of every card is a button under a finger: tapping one moves the focus
there and acts on it in the same gesture. A finger does not walk a menu to reach
a row and then confirm it — it puts itself on the row. So each of these four
declares `touch = false` rather than adding an on-screen stick and three
unlabelled circles that would each be a second, worse way to do what the card
already is. Backing out is the how-to's Back row and the pause card's Resume row;
leaving the table is the pause glyph the Table draws.
# Mesh
Mesh asset. `data.zmsh` is the engine-native geometry payload (`renderer.mesh.encode`); consumers reference the asset's guid.
# Mesh
Mesh asset. `data.zmsh` is the engine-native geometry payload (`renderer.mesh.encode`); consumers reference the asset's guid.
# back
The 'Back' control.
# confirm
The 'Confirm' control.
# navigate
Walking the rows of a card: one flick is one step, a held stick repeats.
Each keyboard direction answers to TWO keys — the WASD key and the arrow key
beside it — because a player reaching for a menu uses whichever is already under
their hand. An axis takes one binding per device class, so each direction is
itself a vector of the two keys that mean it.
On a pad it is the left stick; under a thumb it is the on-screen stick in the
left zone.
# resume
The 'Resume' control.