# RITE — the rules of The Acceding
The nine gates, the touch/cool/flinch economy, and the loop that turns a
litany into a verdict. RITE is the only writer of `A.state`; everything else
reads that table and listens on the bus.
## The nine gates
| # | Gate | Demand | A refusal costs |
|---|---|---|---|
| 1 | THE PORCH | three different glyphs | 0.07 |
| 2 | THE FONT | all three of WATER | 0.08 |
| 3 | THE HINGE | a glyph and its undoing | 0.09 |
| 4 | THE VOW | three withholding, and not the tenderest word | 0.10 |
| 5 | THE UNBARRED | no IRON, and one word twice with a different third | 0.11 |
| 6 | THE CENSER | all three of ASH | 0.12 |
| 7 | THE AMNESTY | nothing from the last litany, and three suits | 0.13 |
| 8 | THE OPEN HAND | three giving, opening and closing on one word | 0.15 |
| 9 | THE CROWN | one word, three times | 0.17 |
Two glyphs to a suit and three slots to a litany, so gates 2 and 6 cannot be
answered without saying a word twice, and gates 5, 8 and 9 ask for the repeat
outright. The re-saying is forced by the content.
Gate 9 is the whole plan in one move: three sayings of a word from cold land
it **exactly on** the flinch threshold and never over it, so the crown is free
to whoever kept one word rested and costs a flinch to whoever did not.
## The numbers, and why
Applied over `A.TUNING` at load. Every one of them was measured against a
simulated run of five player models, not guessed.
| | | |
|---|---|---|
| `TOUCH_PER_SAY` | 0.22 | three sayings from cold = 0.66, the threshold exactly |
| `FLINCH_AT` | 0.66 | at or over, the next saying flinches |
| `COOL_PER_TIER` | 0.18 | what an unspoken glyph sheds when a gate opens |
| `FLINCH_EXTRA_TOUCH` | 0.15 | a flinched word is left tenderer still |
| `IRRITATION_PER_FLINCH` | 0.22 | doubled at full touch, by `FLINCH_SCALE` |
| `RELIEF_PER_ACCEDE` | 0.10 | an opened gate settles the machine |
| `REFUSAL_TOUCH_REFUND` | 0.50 | see below |
| `TENDER_AT` | 0.45 | LITANY marks a slab tender from here up |
**A refused litany is half-remembered.** The Engine takes the full touch of
every word as it is uttered and gives half of it back when it does not
accede — it keeps what it granted, not what it heard. This is what makes nine
unfamiliar demand kinds learnable: a wrong guess costs patience and a beat,
not a whole word. The economy of an *accepted* litany, which the curve is
tuned against, is untouched by it. A refusal emits `cool` with
`cause = "refusal"`, so the meters visibly drop back.
**The utterance is paid for before it is judged.** If the flinches in a litany
fill the patience, the Engine seals on the spot and the litany never gets a
verdict — right words or not.
Measured outcomes, over the real module:
| player | result |
|---|---|
| plans a rotation | wins at gate 9, no flinches |
| wrong once at **every** gate, then corrects | wins at gate 9, patience 0.75 |
| leans on one word every litany | sealed around gate 4–5 |
| ignores the demand | sealed by gate 5 |
| says one word three times, always | sealed at gate 1 |
## What the other slices need from it
### LITANY
`M.demand()` returns `{ n, title, liturgy, plain, barred, miss }` for the gate
the player stands at. `barred` is the list of glyphs that cannot appear in
**any** answer to this gate — brute-forced, so it stays true for the composite
gates and for gate 4, which reads the touch table. Dim them.
`M.tenderness(glyphId)` returns `{ touch, tender, wouldFlinch, projected,
placed, barred }` — everything a glyph slab needs to warn before it costs
something. `projected` accounts for the copies of that glyph already in the
litany, so a slab can show what it will be worth **when this litany is
spoken**, not what it is worth now.
**`M.ladder()` and `M.upcoming(k)` are not optional decoration.** The Engine
does not hide what it will ask for. A player who cannot see that gate 6 wants
ASH has no way to avoid spending ash at gate 5, and the game becomes a memory
test instead of a plan. Render the gate above at minimum; the whole ladder on
the how-to-play card is better. The difficulty was tuned against a player who
can see one gate ahead.
`M.bestLitany({ lookahead = true })` returns a satisfying litany, cheapest
first, if the Verger should ever offer help.
Callback: RITE answers `rite:skip`. LITANY's own `lit:skip` already calls
`M.skip()` directly, which is fine.
### CHOIR
RITE speaks these ids, and shows the written form of any line that has no clip
rather than dropping it:
`verger.open.1`, `verger.open.2`, `verger.touch` (the brief, in that order),
`engine.greet`, `engine.accede`, `engine.refuse`, `engine.flinch`,
`engine.seal`, `engine.crown`, and `glyph.<id>` for all eight glyphs — spoken
once as the word is set down, and again as part of the chant when the litany is
committed. That chant is the resaying, heard.
**Three demand kinds are new and are registered into `A.DEMAND_KINDS` at
runtime:** `twice_said`, `bookend`, `not_tenderest`. Anything generating a
clip per demand kind needs those three as well as the seeded nine.
Sfx ids RITE fires: `place`, `unplace`, `accede`, `refuse`, `seal`.
### STAGECRAFT and COLUMN
`M.begin()` enters `brief` and RITE holds the timer itself, so the opening
always ends whether or not anything else is listening; `M.skip()` cuts it
short. Bus events, in the order one gate produces them:
`gate` → `place` ×3 → `speak` → (`flinch`) → `verdict` → `accede` → `cool` →
`gate`, or `verdict` → `irritation` on a refusal, or `phase` + `lose` on a
seal. `win` fires when the ninth gate accedes.
## Known seam
While the chant and the verdict line are playing, LITANY's line card sits over
the glyph slabs and pointer input to them is lost. RITE keeps that window as
short as it can — the chant is 0.34 s a word — but the card's own dismissal is
LITANY's.
## Re-running it
`M.audit()` brute-forces all 512 ordered litanies against every gate and
reports how many answer each. A zero anywhere is an unwinnable game. It is
cheap; run it after any change to the curriculum.
# vault
The drowned crypt the Assent Engine stands in, and the cold light that makes the
machine's fire read warm.
A square well thirty-six metres deep, flooded to the ankle. The Engine rises up
the middle of it. Two ranks of dressed piers stand in the flood either side of
the machine and run the whole height of the well, so the narrow strips of frame
beside it hold near geometry against a far wall and the camera rising past their
collars is how a player sees a tier go by. A chancel arch at the bottom opens
north into a drowned nave whose arcade recedes into the fog; above that arch a
flue five metres deep runs the full height, lit by nothing, and that is what
stands behind the machine at every gate above the porch.
Also here: the flagstones under the water, the flood itself (screen-space
reflective, so it carries the machine), fallen masonry, drowned pews, hanging
chain, candles somebody lit a long time ago, motes and falling water, three
spatialised drips, and the scene's ambient, fill, sconce and height-fog
settings. The Engine's own fire belongs to the `column` slice and is deliberately
the only warm source in the room.
## Interface
Everything is built by code under the `Vault` anchor and never baked into the
scene. `install(ctx)` is re-runnable — it clears the anchor first, so a hot
reload rebuilds rather than doubling. `tick(dt)` flickers the candles.
- `M.install(ctx) -> number` — build the place; returns how many entities it made.
- `M.tick(dt)` — candle flicker.
- `M.WATER_Y` — the world height of the flood, for anything that needs to sit in it.
## Assets
`/source/art/vault/` — `vlt_stone`, `vlt_water` and `vlt_shaftlight` shaders and
the ten materials over them, all prefixed `vlt_`. `vlt_drip.soundClip` is
synthesised on first install and reused after.
# litany — the screens
Everything the player of **The Acceding** looks at. The player never moves in
this game: they read what the Assent Engine has asked for and they compose three
words out of eight. So the interface is not a layer over the game, it *is* the
game, and this module is where most of the playing happens.
## The screens
| screen | layer | what it is |
|---|---|---|
| `acc-title` | 40 | the title — the rosette, the name, DESCEND |
| `acc-how` | 42 | how it is done: four rules and the whole vocabulary |
| `acc-hud` | 30 | the console the game is played on |
| `acc-brief` | 32 | the descent — carries nothing but SKIP |
| `acc-line` | 46 | the written form of every spoken line, over whatever is showing |
| `acc-flash` | 34 | the verdict, stamped across the machine |
| `acc-pause` | 60 | HELD — and the ledger of what the Engine has heard |
| `acc-end` | 52 | the crown, and the seal |
## What share of the frame the machine gets back
The screen is **1080 × 1440**, three units wide by four tall. The console is a
fixed **728 px** of brass along the bottom: everything a hand touches, stacked
down the tall axis, because a row of eight glyphs across 1080 px does not fit
and a 2 × 4 grid does.
The remaining **712 px** belongs to the vault and the Assent Engine. A 54 px
gate ribbon and the liturgy plate sit in it, both translucent and both blurring
what is behind them rather than hiding it, which leaves **330–430 px of
completely clear glass** looking up the column, depending on whether the gate's
demand runs to one line or two. The verdict is stamped in that clear band and
never over the console, so the one moment the game turns on happens *on the
machine*.
## The art
`art.ui.sigils` (`/source/art/ui/sigils.module`) holds every mark as inline SVG:
one sigil per glyph, drawn so a suit's giving and withholding words are the same
figure turned against itself; the four alchemical suit seals; the nine-toothed
rosette; the crown and the seal; the rules and sockets. Inline SVG rasterises at
the element's display resolution, so a mark is crisp at 16 px in a slab and at
268 px on the title.
Three generated textures carry the metal: `ui_plate` (the console and every
panel), `ui_frame` (the reliquary moulding around the liturgy, nine-sliced), and
`ui_slip` (prayer-slip paper).
## What it reads and what it drives
- Reads `A.state` and never writes it — RITE is that table's only writer.
- Drives the game only through `Rite.*`: `begin`, `skip`, `place`, `unplace`,
`speak`, `reset`. It asks `Rite.tenderness(glyph)` what one more saying of a
word would cost, which is what puts the amber warning and the hatched
projection on a slab before the player commits, and `Rite.stats()` for the
ledger on an end card.
- Renders `Choir.line(id).text` for every spoken line. A line with no audio is
marked **UNVOICED** rather than dropped: the words are on screen whether or
not the generation arrived.
- Every callback id is prefixed `lit:` so no two slices collide.
## Two things that are load-bearing
**The driver holds the module table it captured at load.** A hot-reloaded slice
keeps being ticked by the OLD table until the driver component itself
re-evaluates, so for a while the tree a fresh `require` reads and the frame on
the glass are built by two different versions of this file. That is the
documented `require` semantics, and it is why anything with a lifetime longer
than one chunk run — the clock, the pause card's state, the bus subscriptions —
lives on `A.litany` rather than in a local.
**State lives on `A.litany`, not in module-locals.** Six agents share one engine;
any save re-runs the boot pass, and a module-local dies with it. The pause card
survives a reload because its state is parked on the shared contract table.
# STAGECRAFT — the camera, and the three moments
The camera of *The Acceding*. It drives the one camera the scene already
carries and never adds another, because the world is always multiplayer and a
second rig beside the PlayerPrototype's passes single-peer testing and breaks
the moment anyone joins.
## What it does
**The standing shot.** A station per gate, from `A.cameraStationForTier(n)`,
with an eased travel between them. A locked shot with nothing moving in it
reads as a still, so the frame breathes — three incommensurate periods on the
eye, the look target and the lens — and the breath gets faster and wider as
the Engine's patience runs out. A refused litany knocks the frame; a flinch
shakes it hard.
**THE ACCEDING.** A gate opens and the camera HOLDS on the flare for 0.62 s
with the lens pushing in three and a half degrees, then the whole frame RISES
one tier over 1.85 s. In a frame three wide and four tall the camera move is
the reward.
**THE DESCENT.** The opening. Thirty metres, three shots and a settle, on a
slow arc around the machine so it turns as it is fallen past. Skippable, and
the shot the trailer opens on.
It runs on RITE's `brief` phase and lasts exactly `BRIEF_SECONDS`, so the
camera comes to rest at the porch as the first gate takes the frame. RITE
speaks the Verger over it and LITANY writes what he says, so the descent keeps
the `ui` layer and fires no cues of its own — it takes the camera and nothing
else. Skipping it ends the brief with it.
**THE REFUSAL.** Shoved back against the wall of the well, then sinking toward
the standing water while the lens opens and the machine grows over the top of
the frame, until the dark closes.
**THE CROWN.** Hold on the ninth gate's flare, rise above the machine, and turn
to look back DOWN the whole climb — then fade to firelight. It bookends the
descent: the game opens falling down the column and ends looking back down it.
## What other slices can call
| | |
|---|---|
| `M.install(ctx)` / `M.tick(dt)` / `M.onCallback(id, data)` | the driver calls these |
| `M.play(name)` | `"descent"` · `"crown"` · `"refusal"` — play a moment on its own, for a capture or a trailer take |
| `M.skip()` | end the moment that is running |
| `M.isCinematic()` | true while a moment owns the camera |
| `M.moment()` | which one, or nil |
| `M.descentDef()` / `M.crownDef()` / `M.refusalDef()` | the shot lists, for `cutscene.evaluate` |
| `M.TUNING` | every number |
`stg-skip` is the callback id for a skip affordance on screen.
**Ask `M.isCinematic()` through the function, never as a field.** A field is
read off whichever module table the caller happens to hold, and after a hot
reload that is not the one running the game.
The HUD does not have to hide itself during a moment: each moment draws with
`renderLayers = "all !ui !EditorUI !debug"`, so the `ui` layer is already out
of the frame. `isCinematic()` is there for anything that wants to know anyway.
## What it puts in the world
Under the `Stagecraft` anchor, all of it temporary, all rebuilt on install:
- `stg_key` — a warm point light that rides the descent, so the brass the
camera falls past has something to catch.
- `stg_crown` — a point light over the machine's upper drums, lit only during
the crown.
- `stg_caption` / `stg_speaker` — the cinematic caption, in the game's own
faces, wrapped to the width a 3:4 frame has.
## Three things this slice learned the hard way
**The room is a well.** Walls at x = ±8.05 and z = ±8.05, floor at 0, a lid at
y = 36.5, and the ninth gate at y = 26.6. A pull-back to z = −13 fills the
frame with one flat brick slab; a climb to y = 42 comes back with every pixel
black. `ROOM_Z` is the furthest back a shot may stand. Capture a shot list —
do not reason about it.
**`camera.active()`, not `camera.main()`, and not `layers.active.camera`.**
Only one of the three asks the question this slice needs answered — which
camera is the frame actually drawn from. `layers.active.camera` returned the
PROTOTYPE's rig in play (it reports `ready = true` and renders nothing) and the
editor fly-camera in edit. `camera.main()` answered correctly once and, later
the same session, named the prototype while `camera.list()` showed the clone as
the one with `rendering = true`. Both failures look identical from inside: every
write succeeds and the picture does not change. `camera.active()` is the
on-screen camera by definition; it is the editor's in edit, so refuse that one
and fall back to `camera.main()`.
**Nothing mutable lives on the module table.** `AccedingDriver` captures the
module once and ticks that table forever; the bus holds closures over whichever
table was current when each `install` ran; a fresh `require` returns the newest.
A hot reload splits all three apart. So the live state and the unsubscribe list
both hang off `A.bus`, which survives — otherwise a moment plays with
`cinematic` reading false, the camera freezes mid-shot, and each reload leaves
one more handler on the bus writing camera stations nobody asked for.
## One thing it costs the session
Driving the camera writes the CameraRig's transform. When a player's body is in
the world that is the clone and nothing is touched; when there is none, it is
the prototype the scene owns, and the session ends up with one tuned entity
("Tuned in play: CameraRig") to accept or reject before the next mode flip.
`tools.use("sceneAuthoring", "changes")` then `rejectChanges` is the whole of
settling it.
# column
The Assent Engine — the brass reliquary column the whole game looks at.
It stands nine gates up one continuous turned body: a lathe profile that
swells into a drum at every tier and joins itself at the waist, so the machine
reads as one made thing rather than a stack of cylinders. Each drum carries a
porcelain reliquary face behind a brass bezel, an iron grille with the fire
standing behind it, ribs, rivet courses, prayer-slips, wax somebody left
burning, and a valve wheel. The gate the player stands at burns; the gates
above it are banked to a seam of embers.
All of it is modelled at install time by `art.column.colgeom` and uploaded as
runtime meshes — nothing here mints an asset, so a hot reload rebuilds the
machine instead of leaving a second copy of it behind.
## What other slices call
| Call | Does |
|---|---|
| `M.install(ctx)` | Builds the machine under the `Column` anchor. Re-runnable. |
| `M.tier(n)` | The record for gate `n` — its entities and its live animation state. |
| `M.setLit(n, k)` | `k` in 0..1: how hard gate `n`'s interior fire burns. |
| `M.accede(n)` | Gate `n` opens — the valve turns a quarter, the lip drops and swings, the fire flares white. |
| `M.flinch()` | The machine takes offence: every fire in it chokes for a beat and the whole column shudders. |
| `M.tick(dt)` | Driven by `AccedingDriver`; carries the flicker and every animation above. |
| `M.rebuildKit()` | Drops the shared meshes so the next install models them again. |
## Materials
`col_brass` (verdigris body), `col_brass_wet` (polished fittings), `col_iron`,
`col_porcelain`, `col_verdigris`, `col_wax`, `col_paper`, `col_lens`, and the
three fires — `col_ember`, `col_ember_deep`, `col_ember_flare`.
`install` also puts the scene's `bloom` pass up, under the preset's own name:
emissive geometry with no bloom over it reads as bright paint rather than as
something burning.
# choir — the sound of The Acceding
The mix, the score, the vault's breath, the Engine's own bronze resonance,
the one-shot library the other slices fire, and every word anybody says.
## What another slice calls
```lua
local Choir = require("choir")
Choir.line(id) -- the WRITTEN form: { id, text, speaker, voice, clip }
Choir.speak(id) -- say it aloud on the narrative voice
Choir.speakNow(id) -- say it aloud on the Litanist's own voice, immediately
Choir.sfx(name) -- a one-shot from the library
Choir.ui(name) -- the interface's own one-shots, on a quieter channel
Choir.music(phase) -- crossfade the score
Choir.demandLine(k, a) -- the line id for a gate's demand
Choir.glyphLine(id) -- the line id for a spoken glyph
Choir.report() -- what sounds, and what does not
```
`line` always answers, whether or not the audio arrived: the written form is
the source of truth and the screen shows it either way. `speak` returns false
when a line has no clip, and records the gap in `Choir.missing` rather than
losing the line.
## What it owns
Nine emitters under the `Choir` anchor — the bed, the Engine's resonance,
the standing water, two music decks, the narrative voice, the Litanist's own
voice, and the two one-shot sources. Assets are `snd_*` under `/source/audio`.
Sixty-five clips: forty-five spoken lines, eight CPU-synthesised sounds, eight
generated one-shots and four pieces of score.
## Hearing it
`choir:say:<lineId>`, `choir:play:<clipName>`, `choir:install`, `choir:generate`
and `choir:report` are callback
ids the driver forwards here, which is how a sound is auditioned from outside
the game: a module required from a loose `execute` is a second copy with no
emitters on it, and a call made there plays nothing.
Each writes its answer to `/source/audio/audition.txt`.