# game.core
The one table every slice of *First Light: Lucina* reads.
```luau
local Core = require("game.core")
```
| What | Where |
|---|---|
| The plate grid, cell ↔ world conversions | `Core.GRID`, `Core.PLATE`, `Core.cellToWorld`, `Core.worldToCell`, `Core.cellIndex` |
| The five piece shapes and the placement rules | `Core.SHAPES`, `Core.SHAPE_ORDER`, `Core.footprint`, `Core.canPlace`, `Core.pieceAt` |
| The exposure and dose numbers | `Core.EXPOSURE`, `Core.DOSE` |
| The phases and the level shape | `Core.PHASES`, `Core.LEVEL_COUNT`, the `Level` type |
| The live game state, held across hot reloads | `Core.state` — the `State` type says which slice writes each field |
| The signal bus | `Core.on(name, fn)`, `Core.off`, `Core.emit(name, ...)` — the signal list and its senders are in the source |
| The three shorthands | `Core.request(action)`, `Core.sfx(name, opts?)`, `Core.say(lineId)` |
| Which component mounts where | `Core.MOUNTS`, `Core.mount()` — the scene entrypoint calls it on every load |
Subscribe in `awake`, subscribe again from `onModuleReload` (a duplicate is
dropped), release in `onDestroy`. The rules of the game are stated in
`design/spec.md`; this module carries the numbers and the shapes they share.
# board.inputMap
The night's control scheme: the cursor over the plate, the tray, and the
hands that lay a piece down. Group `player`; the `Board` component holds it
live exactly while the phase is the night, and `game/screens/ui` stands it
down whenever a card is on screen.
| Control | Kind | Keyboard / mouse | Touch | Pad |
|---|---|---|---|---|
| `cursor` | axis2 | WASD | — | left stick |
| `cursorAlt` | axis2 | arrows | — | d-pad |
| `place` | button | Space, left click | **Place** | south |
| `lift` | button | X, right click | **Lift** | east |
| `rotate` | button | R | **Turn** | west |
| `prevShape` | button | Q | — | left shoulder |
| `nextShape` | button | E | — | right shoulder |
| `fix` | button | F | **Fix** | north |
| `pause` | button | Escape | — | start |
## Why some controls carry `touch = false`
The plate and the tray are touched **directly**: a tap on a cell puts the
cursor there, a second tap on the same cell lays the piece down, a drag
carries the cursor with the finger, and a tap on a tray slot picks that
shape. A virtual stick and two arrows would be a second, worse way to do what
a fingertip already does — so `cursor`, `cursorAlt`, `prevShape` and
`nextShape` say so in the record rather than putting them on screen. `pause`
is the same: the HUD draws a pause pip and the screens slice owns it.
The four things a finger has no direct counterpart for are the four on-screen
buttons. Place and Lift share the `act` group so they land adjacent and
largest under the thumb; Turn is a step smaller beside them; Fix is small and
low and deliberately away from the others, because it is the one press in the
night that cannot be taken back.
## Two axes for one movement
A `vector` arm takes one binding, so one axis2 control cannot carry both WASD
and the arrow keys. `cursor` and `cursorAlt` are that pair — two controls in
the scheme, one movement to the player, feeding one handler in `Board`.
# Screens
Puts every authored screen of *First Light: Lucina* on the glass and drives
them. Mounted once, on `mount_screens`; it needs nothing else on that entity.
## What it draws
| Screen | Layer | What it is |
|---|---|---|
| `lu-menu` | 960 | whichever card the phase calls for |
| `lu-hud` | 120 | the dawn rail, the dose rail, the plate readout, the tray hint, the pause pip and the Keeper's line |
| `lu-speech` | 980 | the Keeper's line alone, wherever no HUD is carrying it |
The HUD is two vertical rails — dawn down the left edge, dose down the right —
with the plate's readout above the plate and the pause pip in the top-left
corner. Nothing spans the frame: the game films at 1080x1440, and every anchor
is a fraction of the live `ui.screenSize()`, so the same arrangement holds on a
television and on a phone held either way.
## What decides which card is up
`Core.state.phase`, which the rules slice's `Game.component` owns.
| Phase | Card |
|---|---|
| `title` | the title card |
| `opening` / `play` / `fixing` | none — the HUD |
| `paused` | the pause card |
| `result` | the result card: the plate as it was fixed, and its match |
| `won` / `lost` | the two end cards |
*How to play* is an overlay over whichever card opened it, not a phase, so it
reads the same from the title card, the pause card and a failed result.
When the player takes a line this component emits `uiRequest(action)` —
`"start"`, `"pause"`, `"resume"`, `"restart"`, `"next"`, `"title"` — and waits.
If nothing has moved the phase within a moment it moves the phase itself, so
the game is reachable from its own title card whatever else is loaded. The
moment `Game` answers a `uiRequest`, that fallback stops firing on its own.
## Controls
`ui` is activated the instant a card goes up and released the instant the last
one comes down; it stands down `player` and `uipause` while it is live.
`uipause` is live through `play` alone and carries the one control that opens
the pause card.
- **Focus** moves on `navigate` (arrows / d-pad) and `navigateStick` (left
stick), wraps at both ends, and lands on a line the instant a card opens. The
focused line carries a brass ring, a lit edge marker and a diamond, and
shifts a few points to the right.
- **Choose** on `confirm` (Enter, Space, south), **leave** on `back` (Escape,
Backspace, right mouse, east); a result or end card also answers `restart`
(R, north).
- **A pointer or a thumb** takes a line by touching it, which also moves the
ring there, so mixing a mouse and a pad never loses the player's place.
- Each card carries the legend for the family in the player's hands, read from
`/runtime/input/device` and published as `Core.state.device`. The instruction
page redraws its whole control table for that family.
## Public fields
- `menuMap: assetRef(inputMap)` — the card scheme. Default `ui`.
- `pauseMap: assetRef(inputMap)` — the pause control. Default `uipause`.
## Methods
- `:repaint()` — force the next tick to rebuild every screen. The screens are
repainted only when their own inputs change, and a hot reload of `theme` is
not one of those inputs, so this is how an edit to the design system reaches
the glass without restarting anything.
- `:focus()` — `{ card, id, device }`: the card that is up, the line the ring
is on, and the family being drawn for.
- `:setDevice(name?)` — pin the family the legends and the instruction page are
drawn for; `nil` hands it back to the device counters.
- `:choose()` — take the focused line, as if the player had chosen it.
- `:move(delta)` — move the focus ring by `delta` lines, wrapping.
## Lifecycle
- `awake()` — paints the first frame.
- `update(dt)` / `editorUpdate(dt)` — one tick either way: reads the device,
resolves the card the phase asks for, activates or releases the schemes, and
repaints only the screens whose inputs changed.
- `onDestroy()` — releases both schemes and unregisters every screen it put up.
# Mesh
Mesh asset. `data.zmsh` is the engine-native geometry payload (`renderer.mesh.encode`); consumers reference the asset's guid.
# uipause
The one control that opens the pause card, live through the whole night.
Separate from the `ui` map because the two have opposite lifetimes: `ui` is
live only while a card is up, and this is live only while one is not. The `ui`
map names this group in its `suppresses`, so the key that opens a card cannot
also be read by the card it opened.
| Control | Kind | Keyboard & mouse | Gamepad |
|---|---|---|---|
| `pause` | button | Escape, P | start |
Touch is unbound: a thumb pauses through the pip on the HUD, which is a real
tap target in the corner the engine's own touch overlay leaves free.
# 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.
# Mesh
Mesh asset. `data.zmsh` is the engine-native geometry payload (`renderer.mesh.encode`); consumers reference the asset's guid.
# ui
The controls a CARD answers to, and nothing else. Activated the instant a card
goes up and released the instant the last one comes down, so the night never
reads a keypress meant for a menu: it stands down the whole `player` group (the
board's own scheme) and `uipause` (the control that opens the pause card), so
one Escape cannot close a card and re-open it in the same frame.
| Control | Kind | Keyboard & mouse | Gamepad |
|---|---|---|---|
| `navigate` | axis2 | the arrow keys | the d-pad |
| `navigateStick` | axis2 | — | the left stick |
| `confirm` | button | Enter, Numpad Enter, Space | south |
| `back` | button | Escape, Backspace, right mouse | east |
| `restart` | button | R | north |
Touch is deliberately unbound. Every line on every card is a real tap target at
thumb size carrying its own `onClick`, so a finger drives a menu by touching
the thing it wants rather than through a floating overlay button pointing at
it — which is also why this map takes the engine's own touch controls off the
screen while it is live.
`navigate` and `navigateStick` are separate controls because an `axis2`
evaluates ONE binding per class and so cannot carry both a d-pad and a stick;
`Screens.component` subscribes both to one handler.
# Mesh
Mesh asset. `data.zmsh` is the engine-native geometry payload (`renderer.mesh.encode`); consumers reference the asset's guid.
# Board
The plate, the pieces, and the hands. Mounted on `mount_board`.
Everything the apprentice looks at and touches during the night lives here:
the seventy cells of the glass, the cursor, the tray at the near end of the
table, and every frisket and crystal standing on the plate.
## What it draws
| Layer | What it says |
|---|---|
| **the emulsion cell** | the gelatin at that cell, bone when untouched, true black once the betafite has struck it, and dimmed in a frisket's shadow |
| **the target frame** under it | gold where the plate wants that cell dark, warming toward the betafite's own green as the exposure climbs; red where a cell that had to stay clear has been spoiled; dark where a frisket covers it; absent where the plate wants nothing |
| **the cursor** | a viewfinder bracket, breathing, red while a placement has just been refused |
| **the preview** | the footprint the selected shape would take, green where it will lie and red where it will not |
| **the tray** | five slots, each with the shape it holds drawn as a brass diagram, what is left in it, and a rim of light on the one selected |
Every quad is one of two materials over `art/board_ink.shader`, and differs
from its neighbours only in two per-instance lanes — so the whole board is
one pipeline and one material row rather than two hundred materials. The
friskets themselves are real brass and are lit as brass.
## What it reads and writes
Reads `Core.state.cells` (the rules slice writes `exposure` and `masked`) and
`Core.state.phase`. Writes `Core.state.pieces`, `Core.state.tray` and
`Core.state.cursor`.
Emits `placed`, `lifted`, `rotated`, `refused`, `cursor`, `select`, the sfx
names `place_crystal` / `place_frisket` / `lift` / `rotate` / `refused` /
`select` / `ui_move`, and the two requests the night can raise from the
player's hands: `uiRequest("fix")` and `uiRequest("pause")`.
On `levelStart` it fills the tray from the plate the rules slice dealt and
puts the cursor back in the middle of the glass.
A crystal is spawned as `crystal_<id>` and handed to the hero slice's
`Betafite` component when that is registered. If `Betafite` is not there — or
attaches and draws nothing — the crystal wears this slice's own stand-in
instead, because an invisible crystal on the plate is worse than a plain one.
## The controls
`board.inputMap`, group `player`, live exactly while the phase is the night
and nothing has stood that group down.
| Action | Keyboard / mouse | Touch | Pad |
|---|---|---|---|
| move the cursor | WASD, arrows, or the pointer over the glass | tap a cell, or drag across the plate | left stick or d-pad |
| place | Space, left click, or a second tap on the cell the cursor holds | **Place** | south |
| lift | X, right click | **Lift** | east |
| turn | R | **Turn** | west |
| pick a shape | Q / E, or click a tray slot | tap a tray slot | shoulders |
| fix the plate | F | **Fix** | north |
| pause | Escape | the HUD's pause pip | start |
The plate and the tray are pointed at and touched **directly** rather than
through a virtual stick, which is why `cursor`, `prevShape` and `nextShape`
carry `touch = false`. Where a cell sits on screen is solved against the live
view matrix each frame, so it is right at any aspect ratio and from wherever
the staging slice has moved the camera. Which way the grid runs on screen is
read off that same matrix rather than assumed, so pushing right moves the
cursor right whichever side of the table the camera is standing on.
## Public fields
| Field | What it is |
|---|---|
| `inputMap` | the night's control scheme |
| `repeatDelay` | seconds a held direction waits before it starts stepping again |
| `repeatRate` | seconds between steps once it is repeating |
| `pointerCursor` | whether the mouse moving over the glass carries the cursor |
## Bench
`benchPlate()` deals a practice target and a full tray straight into
`Core.state` so the board can be played with no other slice mounted, and
`readout()` reports the cursor, the tray, the piece count and whether the
scheme is live. Neither is part of the game; they are how this slice is
proved on its own.
# cursor
<!-- 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.
# cursorAlt
<!-- 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.
# fix
<!-- 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.
# lift
<!-- 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.
# nextShape
<!-- 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.
# pause
<!-- 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.
# place
<!-- 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.
# prevShape
<!-- 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.
# rotate
<!-- 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.
# pause
**Pause**
Holds the night and puts the pause card up. Bound on the keyboard and the pad only; a thumb pauses through the pip drawn on the HUD.
# back
**Back**
Leaves the card that is up. On a title card there is nothing behind it, so back does nothing there; everywhere else it returns to whatever put the card up.
# confirm
**Choose**
Takes the focused line. The mouse is not bound here: a pointer confirms by clicking the control it is over, which every card's own onClick answers, and a bound left button would fire the focused line from anywhere on the screen.
# navigate
**Navigate**
Moves the focus ring around a card. The keyboard class is the arrow keys and the gamepad class is the d-pad; the left stick is carried by `navigateStick` beside it, because an axis2 evaluates one binding per class. Touch is unbound: a finger taps the line it wants, which is the direct form of the same action.
# navigateStick
**Navigate (stick)**
The left stick doing what the d-pad does. It exists as its own control because an axis2 evaluates ONE binding per class and so cannot hold both; the screens component subscribes this and `navigate` to a single handler.
# restart
**Again**
Takes the plate again, from a result card or an end card - the one-press way back into a run without walking the menu to find it.