# Shaft
The authored course of WICK, as data: where the granite is, where it is open,
what is on it, and which way it leans.
`game.Climb` simulates against these records and the lighthouse dressing is
built from the same ones, so what punishes the player and what the player can
see are the same thing by construction. Nothing here mutates the blackboard.
```luau
local Shaft = require("game.Shaft")
local course = Shaft.segments()
```
## What `segments()` returns
| Field | What it holds |
|---|---|
| `version` | The course revision. Bump it when the shape changes. |
| `walls` | Solid spans: `{ id, side, y0, y1, x0, x1 }`. `x0` / `x1` are the inner-face distance from the shaft axis at each end, so a leaning span is built leaning. |
| `windows` | Openings: `{ id, side, y0, y1 }`. No granite here — a kick into one catches nothing. |
| `hazards` | `{ id, kind, side, y0, y1, y, amplitude?, period?, duty?, phase?, reach? }`. |
| `leans` | `{ side, y0, y1, delta }` — where the face is not plumb. |
| `lamp` | `{ y }` — the lamp-room floor. |
| `bounds` | `{ y0, y1 }` — the lowest and highest height worth dressing. |
`side` is `-1` for the left wall and `1` for the right. A chain carries
`side = 0`: it hangs in the middle of the shaft.
## Hazard kinds
| `kind` | What it does |
|---|---|
| `spike` | Rusted iron. Always live. |
| `wire` | Sparking wire, live on a duty cycle — read `Shaft.hazardLive(h)` every frame and drive the spark from it. |
| `plaster` | Holds a landing for a moment, then comes away once. `Shaft.isConsumed(h.id)` afterwards; that patch is bare for the rest of the run. |
| `chain` | Swings across the shaft at one height. `Shaft.hazardX(h)` is where it is right now. |
## Queries
| Call | Answers |
|---|---|
| `Shaft.faceX(side, y)` | The inner face of that wall at that height, in metres from the axis. |
| `Shaft.clingX(side, y)` | Where the cat's centre sits clinging there. |
| `Shaft.hasWall(side, y)` | Is there granite to catch? |
| `Shaft.windowAt(side, y)` | The opening at that height, if any. |
| `Shaft.hazardAt(side, y)` | The wall hazard standing there, live or not. |
| `Shaft.hazardsBetween(y0, y1)` | Everything overlapping a height range — what a dresser asks for when it only builds what the camera can see. |
| `Shaft.byId(id)` | One hazard by its id. |
| `Shaft.hazardLive(h, t?)` | Is it dangerous at this instant? |
| `Shaft.hazardX(h, t?)` | Where it is, in metres from the axis. |
| `Shaft.chainHitting(x, y, t?)` | The chain whose sweep covers that point right now. |
| `Shaft.isConsumed(id)` / `Shaft.consume(id)` / `Shaft.reset()` | The once-only state. `game.Climb` resets it at the top of a run. |
`t` defaults to `Core.state.elapsed`, which is the clock the rules run on — so
a dresser that leaves it out is animating on exactly the clock the simulation
is using, and the chain on screen is the chain that hits.
## The curve
| Band | What is in it |
|---|---|
| 4 – 25 m | Clean granite both sides. Nothing but the rhythm and the sea. |
| 25 – 45 m | The first window slits, the first spikes. |
| 45 – 70 m | Sparking wire and crumbling plaster; the windows widen. |
| 70 – 95 m | The first swinging chain, the wall leaning inward so the rhythm breaks, wire and spikes together. |
| 95 – 120 m | Everything at once, two more chains, the long windows, one stretch where the wall bulges out — then 117 – 120 clean, so the lamp is reached and not stolen. |
# Core
The one table every slice of WICK shares: the constants that define the shaft,
the sea and the camera framing, the blackboard the rules write and everyone
reads, an event bus, the phase machine's single writer, and the Keeper's
script. `local Core = require("game.Core")`. See `/zero/source/design/spec.md`.
## Exports
- `Core.SHAFT`, `Core.SEA`, `Core.CAMERA`, `Core.STAGING_TIMEOUT` — the place.
- `Core.state` — `phase`, `player {x, y, vx, vy, wall, facing, gripping, grip, alive}`, `height`, `best`, `seaHeight`, `seaRate`, `surged`, `lampLit`, `elapsed`, `kicks`, `run`.
- `Core.events.on(name, fn) -> unsubscribe`, `Core.events.emit(name, ...)`, `Core.events.count(name)`.
- `Core.setPhase(p)` — called by `game.Climb` only.
- `Core.LINES`, `Core.say(id)` — the Keeper's lines; `say` emits `"say"` and `"caption"`.
- `Core.heightClimbed()`, `Core.seaGap()`, `Core.resetRun()`, `Core.clamp`, `Core.lerp`.