# game.stage.tentGeometry
The shapes the big top is made of, as pure geometry.
Every function returns a `MeshGeometry` — flat `positions` / `indices` /
`normals` / `uvs` arrays, the shape `renderer.mesh.create` and
`asset.create("mesh", …)` both take. Nothing here touches the scene;
`game.stage.Tent` turns these into meshes and stands them up.
| Call | Shape |
|---|---|
| `revolve(profile, segments, axisX, axisZ)` | A surface of revolution wound and normalled to be seen from INSIDE — the canvas. `profile` is `{ r, y }` from the apex outward and down. UV.x is the fraction round the tent, UV.y climbs apex to hem, which is what lets a shader stripe it. |
| `bleachers(opts)` | Raked plank seating swept round an arc: a seat board and its riser per tier. Angles are measured the way the ring is — 0 is +Z, toward the seat. |
| `house(seats, opts)` | The whole audience as two geometries — `cloth` and `faces` — with every sitter's place, turn, lean and size baked into the vertices where they sit. A `seat` is `{ angle × π, tier, slouch, size, hat }`. Nothing in the house moves, so nothing in it needs to be an entity. |
| `pushEllipsoid(g, cx, cy, cz, rx, ry, rz, segments, rings, yaw)` | Appends a sphere with a radius per axis, carrying true ellipsoid normals — so one flattened into a hat brim still lights like one. |
| `pushCylinder(g, cx, cy, cz, rBottom, rTop, height, segments, yaw)` | Appends a capped cylinder, or a frustum when the radii differ. `cy` is the bottom, so a thing that sits on a plank is placed at the plank. |
| `cone(topRadius, bottomRadius, height, segments)` | An open shell seen from outside — the shaft of a spotlight. UV.y is 0 at the lamp and 1 at the pool. |
| `disc(radius, segments)` | A flat disc facing +Y. |
| `pushBox(g, cx, cy, cz, sx, sy, sz, yaw)` | Appends a box, each face carrying its own normal, to a geometry `empty()` made. |
| `triangleCount(g)` | What a build produced — the cheap sanity read. |
# game.stage.beats
The staging of GREASEPAINT, written as data.
A beat is a camera path over a few seconds plus a handful of cues that fire as
the clock crosses them. `Beats.list` holds them by name — `opening`,
`entrance`, `laugh`, `kick`, `kill`, `won`, `lost` — and `Beats.forEvent` says
which Core event asks for which. `game.stage.Cinematics` drives them; nothing
here touches the world.
A path is keyframes of `{ t, pos, look, fov, shake }`, seconds from the
beat's start. The first is where the camera jumps to and the last is where it
is left — and the last keyframe of every beat is the seat, so a beat cannot
strand the camera somewhere else. `Beats.sample(path, t)` reads a path at a
moment; `Beats.seat()` returns the front row in the same shape.
No beat runs longer than 2.5 seconds. A player is holding cards the whole time.
## The lamp
`Beats.RIG` is what the cue `lamp = "rig"` means and what every beat hands the
lamp back to: `{ intensity, angle, inner, color }`, `inner` being the inner
cone as a fraction of `angle`. `game.stage.Tent` writes what it actually built
into it on every build and nothing else writes it, so retuning the tent's
`lampBrightness` is retuning what a beat returns to.
## The framing
`Beats.framing` says how the seat is framed for each clown: `lookY` is added to
`Core.SEAT.lookAt`'s height and `fov` replaces `Core.SEAT.fov` for the act.
`Beats.frameFor(clown)` selects one — `Cinematics` calls it on `act.start`, and
with nil at the title. `Beats.lift()` and `Beats.lens()` read back what is in
force.
Every keyframe aimed at the seat's own look-at, and every keyframe asking for
the seat's own lens, travels with the framing; a keyframe that names a target
or a focal length of its own keeps it. So a beat that ends in the front row
ends framed on whoever is standing in the ring.
The company is between 1.7 m and 3.1 m tall and the seat is a fixed chair 7.5 m
from the mark, so the lens is the only thing that can hold them all: 45° for
Pinch, 55° for Bello and Mr. Marrow, 58° for Slats on his stilts. Every head
lands at 0.30 of the frame's height, clear of the plate above it.
# Core
The one table every part of GREASEPAINT reads. It carries the game's
constants — the cards, the four clowns, the barker's lines, the palette and
the fonts — the live state of the night, and the event bus the game talks
over. Every other module requires this and nothing else of each other.
`game.rules.Director` is the **only** writer of `Core.state()`. Everything
else reads the state, listens on the bus, and asks the Director for changes
through `Core.cmd`.
```luau
local Core = require("game.Core")
Core.state().nerve -- read the night
Core.on("clown.hit", function(p) ... end) -- react to it
Core.cmd("playCard", { index = 2 }) -- ask the Director to act
```
## Constants
- `Core.RULES` — Nerve, Breath, hand size, the Laughter ceiling and where the
meter carries to after a laugh, how warm the house can get and what a step
of heat is worth, what a held Grin bites, how many acts a night runs, which
act's interval takes a card away instead of giving one, the starter deck
and the reward pool.
- `Core.CARDS` — the twelve cards, each with `kind` (`gag` / `guard` /
`trick` / `curse`), `cost`, and whatever it does: `dmg`, `block`, `heal`,
`laughter`, `effect` (`weaken` / `reflect` / `burn`), `exhaust` when playing
it takes it out of the act, and the `text` its face prints.
- `Core.CLOWNS` — Pinch, Bello, Slats and Mr. Marrow: `hp`, a looping
`pattern` of intents, and three spoken `lines` (`enter`, `laugh`, `die`).
- `Core.RUN` — the three-clown pool and the finale.
- `Core.BARKER` — the Ringmaster's four narrated lines, and `heat`, the one
he marks each step of the warming house with.
- `Core.PALETTE` / `Core.FONTS` / `Core.ANCHORS` / `Core.SEAT` /
`Core.PHASES` / `Core.EVENTS` / `Core.COMMANDS`.
## State
- `Core.state()` — the live night. Phase, act, turn, Nerve, Breath, Block,
Laughter, the `heat` the house has reached, the `library` (every card the
night owns) and the working piles drawn from it (`deck`, `hand`, `discard`,
`exhaust`), the current `clown`, the `reward` on offer — its `cards`, the
`next` clown it names and its `mode` (`take` or `leave`) — the focused
index, and the `order` the four acts run in.
- `Core.reset()` — replaces the state with a fresh night, in place, so a
reference held across a restart stays valid.
## The bus
- `Core.on(name, fn, tag?) -> unsubscribe` — subscribe. A `tag` makes it
idempotent: registering the same event and tag again replaces the earlier
subscription rather than stacking a second copy, which is what keeps a
reloaded component from handling everything twice.
- `Core.off(name, tag)` — drop a tagged subscription.
- `Core.emit(name, payload)` — fire to every subscriber, in order. A
listener that errors is reported and the rest still run.
- `Core.cmd(name, payload)` — `Core.emit("cmd." .. name, payload)`.
- `Core.listenerCount(name)`, `Core.clearListeners()`.
## Chance
The night's randomness is one stream, seeded per run, so a seed replays a
whole night: the act order, the shuffles, which card the tent paints, which
three cards a reward offers.
- `Core.random()` — the next value in [0, 1).
- `Core.randomIndex(n)` — an integer in [1, n].
- `Core.shuffle(list)` — shuffle in place, return the list.
- `Core.rand(salt)` — a stable value for a salt, independent of the stream.
## The frame
Every screen, plate and card lays out from the live frame, so any shape holds.
- `Core.frame(override?)` — `{ w, h, u, wide }`. `u` is a hundredth of the
short side: the unit every proportion is stated in. `wide` is true from an
aspect of 1.25 up. Pass `{ width, height }` to lay out against a shape the
engine is not running at.
- `Core.rgba(hex, a)` — a palette hex with an alpha, as the `rgba()` string a
widget takes.
- `Core.hairline(n)` — a rule of `n` pixels, never thinner than one whole one.
## Also
- `Core.card(id)` / `Core.clown(id)` — a record, or nil.
- `Core.readingTime(text)` — how long a spoken line should sit on screen, in
milliseconds, when no generated clip has answered for it. Between 1.4 and
9 seconds, whatever the length.