# Focus
Menu focus navigation for the cards of GREASEPAINT. A controller has no
pointer, so every menu carries an ordered ring of focusable ids and exactly one
of them is lit; this module walks that ring.
- `Focus.open(list, prefer)` — open a menu: the ids in stick order, and which starts lit.
- `Focus.axis(v, dt)` — feed the `navigate` axis; one flick is one step, a held stick repeats.
- `Focus.point(id)` — a mouse crossing a row or a finger tapping it moves the focus there.
- `Focus.id()` / `Focus.isFocused(id)` — read it.
- `Focus.close()` — nothing is focused until the next `open`.
- `Focus.release()` — let go of a held direction, so a stick held through a transition does not keep walking.
The ring is whatever the card offers, so an interval with three faces and a
fourth row is a ring of four and needs nothing said about it here: `open` takes
the list `screens.itemIds` returns for the phase.
A held stick repeats on a delay measured in seconds AND in frames. On a machine
running long frames a single flick can be one frame carrying half a second of
`dt`, and a delay measured only in time turns that flick into two steps.
Fed by `game/input/console.inputMap`; consumed by `game.ui.Screens`, which asks
it which row to draw lit.
# Sim — the simulation of FIRST LIGHT: RESOWN
`game.rules.Sim` is the whole run: the day cycle, the plot state machine, the
zyme's well-up and spread, leavening, the beam's burn, the seed economy and the
win/lose conditions. It is the **only writer of `FL.state`**. Every other slice
reads that table and reacts to `FL.events`.
Nothing here touches an entity, a material or a screen. `Rules.component`
(mounted on the `game` anchor) hands it the frame's elapsed time; the rest of
the game calls the functions below.
```luau
local Sim = require("game.rules.Sim")
local FL = require("game.FL")
Sim.start() -- a fresh run, the first night
Sim.beamAt(x, z, dt, Sim.beamPowerFor(FL.state.phase, FL.state.phaseT))
local did = Sim.act(col, row) -- "sowed" | "harvested" | nil
```
## The API
| call | what it does |
|---|---|
| `Sim.start()` | Throws the hillside away and lays a fresh one out. Emits `start`, `screen`, `phase`, `shrineThreat`, and one `plotChanged` per plot. |
| `Sim.restart()` | The same thing, reachable from `paused` / `won` / `lost`. |
| `Sim.pause()` / `Sim.resume()` | Freeze and unfreeze. Only `playing` pauses; only `paused` resumes. |
| `Sim.setScreen(screen)` | `"title"` `"howto"` `"playing"` `"paused"` `"won"` `"lost"`. Emits `screen`. An unknown name is refused with a log line. |
| `Sim.act(col, row) -> string?` | Sows bare or scorched soil for a seed; lifts a ripe crop for seed. Returns `"sowed"`, `"harvested"`, or `nil`. |
| `Sim.beamAt(x, z, dt, power) -> boolean` | The beam lands at a world point for `dt` at `power` (0..1). Returns true on a frame that took bloom off. |
| `Sim.tick(dt)` | Advances the run. `Rules.component` calls it once a frame. |
| `Sim.beamPowerFor(phase, t) -> number` | The plate's power for a phase, 0..1. **The mapping every caller should use.** |
| `Sim.lastActReason() -> string?` | Why the last `act` did nothing: `"no-seed"`, `"growing"`, `"zyme"`, `"nothing-here"`, `"not-playing"`. Also mirrored onto `FL.state.lastActReason`. |
| `Sim.beamHit() -> col, row, age` | Where the beam last took bloom off, and how many seconds ago. |
| `Sim.isRunning() -> boolean` | True while the clock runs. |
| `Sim.phaseProgress() -> number` | 0..1 through the current phase. |
| `Sim.announce()` | Re-states the run as it stands — screen, phase, threat and every plot — without changing any of it. A slice that has just mounted, hot-reloaded or come through an edit/play flip calls nothing; `Rules.component` calls this in `start()` so it catches up. |
| `Sim.setPhase(phase, dayIndex?)` | Put the clock at the start of a phase, hillside untouched. For a staged beat or a test that does not want to wait. |
| `Sim.summary() -> string` | One line of the run, for a log or a test. |
## Two things a caller has to get right
**Power is the aim factor, not the sun.** `beamAt` multiplies the `power` you
pass by the phase's own gate (`PHASE_BEAM`), so a beam at night burns nothing no
matter what you pass, and a day beam is a quarter as fast as a first-light one.
Pass `Sim.beamPowerFor(phase, phaseT)` — or your own 0..1 alignment factor on
top of it. Do **not** pass `FL.sunHeightFor` directly: that function answers
where the sun is (−1..1, and only ~0.3 at the top of first light), not how hard
the plate burns, and handing it over makes the beam roughly three times too
weak.
**`tick` sub-steps.** It advances in slices of 0.1 s, so `Sim.tick(30)` in one
call produces the same run thirty seconds of frames would — a test can reach a
later dawn without waiting for it, and a frame hitch cannot skip a well-up.
## The day
`night 40 → firstlight 30 → day 40 → dusk 10`, from `FL.PHASE_LEN`. `dayIndex`
rises as **first light ends**, which is what a dawn survived means; reaching
`FL.DAWNS_TO_WIN` (5) there ends the run with `won`, at 550 s of a clean run.
| phase | the hill | the beam (`PHASE_BEAM`) |
|---|---|---|
| night | the valley pushes bloom onto the lowest terrace it can reach, mature blooms spread, crops beside a bloom leaven | 0 |
| firstlight | nothing new comes up; the window | 1 — a full bloom hisses off in `BURN_SECONDS` (2 s) |
| day | crops grow | 0.25 |
| dusk | blooms spread again, crops still grow | 0.25 fading to 0 |
## The plot state machine
```
bare ──sow(1 seed)──▶ sown ──grow──▶ crop ──grow──▶ ripe ──harvest──▶ bare (+2 seed, +4 leavened)
any ──bloom──▶ zyme ──beam──▶ scorched ──sow(1 seed)──▶ sown
```
- A plot turning to `zyme` **loses whatever grew on it** — that is the cost of
letting a terrace go.
- `scorched` soil is where the bloom spreads **fastest** (`SPREAD_SECONDS_SCORCHED`,
half the normal interval), so burning ground buys the window and sells the
next night.
- Growth runs while the sun is up (first light, day, dusk) at `GROW_SECONDS`
(55 s) from sowing to ripe: sow in a day and lift it in the next.
- **Leavening** marks `sown`, `crop` and `ripe` plots that sit beside a bloom
through a night. The design's table names `crop`/`ripe`; `sown` is included
here so a plot deliberately sown next to a bloom is a real gamble from the
first dusk. The double yield only ever pays at `ripe`, so nothing else
changes.
## The shrine
`FL.state.shrineThreat` climbs by the share of **row 1** carrying bloom, and a
row 1 fully held for `THREAT_SECONDS` (40 s — one night) is 1.0 and a loss.
Clearing row 1 walks it back over `RELIEF_SECONDS` (25 s). The `shrineThreat`
event fires when the value has moved `THREAT_STEP` (0.02), and always at the
ends, so a HUD meter can follow it without a per-frame event.
## The opening hillside
`M.OPENING` dresses rows 1..6 at `start()`: crop, **ripe**, crop, sown, bare,
bare, uphill first. The hill reads as a farm from the first frame, the ripe row
teaches harvest before the first night ends, and the bare lower terraces are
where the zyme arrives. Seeds start at 6 (`FL.resetState`).
## Determinism
The well-up and the spread draw from a small LCG seeded to a fixed value in
`start()`, so two runs of the same inputs produce the same hillside. A
reproduction is a reproduction.
## Measured
- A full-grown bloom under a full-power first-light beam: **2.02 s**.
- One untouched night leaves **9 blooms** on the lower terraces.
- A player who never acts loses at **224 s**, in the day of the second dawn.
- A player who burns the nearest bloom each first light and works the soil each
day wins at **550.4 s** with 83 harvests.
# Hud
The read-out the Facer works by, as one widget tree.
```luau
local Hud = require("game.ui.Hud")
ui.updateScreen("fl_hud", Hud.tree(FL.state, {
power = Sim.beamPowerFor(FL.state.phase, FL.state.phaseT),
progress = Sim.phaseProgress(),
caption = { speaker = "The Almanac", text = "...", missing = false, serial = 3 },
banner = { phase = "firstlight", serial = 2 },
}))
```
`Hud.tree(state, view)` takes `FL.state` plus the four things the component
knows that the state does not: the plate's power this frame, how far through
the phase the clock is, the caption showing now, and the phase announcing
itself.
Five clusters, each on a screen edge at a proportional inset, so the middle of
the frame — where the hillside and the dawn are — stays clear:
| | |
|---|---|
| top left | which dawn, the phase, and the clock running out of it |
| top right | the sun's track, and what the plate can do with it |
| left, centre | how far the bloom has climbed toward the shrine |
| bottom left | seed in hand |
| bottom, centre | what the Almanac is saying |
| upper third | a phase announcing itself, then gone |
The whole thing is a full-height flex column — a top row of two corners, a
banner strip, a `flex = 1` middle that pushes the rest down, and a bottom band
— so nothing carries a coordinate chosen for one screen. The bottom-right
corner is left empty on purpose: the engine's touch overlay draws the Face, Sow
and Pause buttons into it.
# Cards
The five authored screens of *First Light: Resown* — the title, the how-to, the
pause card and the two endings — each a printed page of the previous keeper's
almanac laid over a dimmed, frosted hillside.
```luau
local Cards = require("game.ui.Cards")
local tree = Cards.tree("title", { focus = "begin", device = "kbm", portrait = true }, FL.state)
```
- `Cards.tree(screen, view, state)` returns the tree for `title`, `howto`,
`paused`, `won`, `lost` — and `nil` for `playing`, where there is no card.
- `Cards.ROWS[screen]` is the ordered ring the stick walks; hand it to
`game.ui.Focus`. `Cards.ROW_LABEL` is what each row reads.
- `view` carries `focus` (the lit row id), `device` (`kbm` / `gamepad` /
`touch`, which decides the hint under the rows) and `portrait`.
Row ids are `begin`, `howto_open`, `howto_back`, `resume`, `restart`,
`to_title`. Each row is a real tap target (its own `onClick`), a real click
target, and a focus stop drawn with a bronze ring when it is lit — so a finger,
a mouse, a keyboard and a controller all reach the same thing.
## Shape
Every measure is a fraction of the live screen. Two rules do the work:
- **`pageWidth(short, wide)`** — on a tall frame the page is a fraction of the
short edge and nearly fills it; on a wide one it is a fraction of the width
and becomes a centred column with the hillside showing either side.
- **The how-to lays itself out by aspect.** Stacked down a tall frame; split
into two columns on a wide one, where height is the scarce axis instead of
width. Its control rows stack their keycaps under the action name in the
narrow column and put them beside it in the wide one, so a label and three
caps never run into each other.
A wrapping label breaks against its container, never against itself, so every
run of prose on a card goes through `para()`, which hands it a full-width box.
# FL
The shared constants, geometry and contracts of *First Light: Resown*. Every slice of the game requires this and nothing else of each other.
- **Hillside geometry** — `COLS x ROWS` plots on terraces that run down the hill along +Z; `plotCenter`, `pathCenter`, `rowZ`, `groundY`, `floorAt`, `rowAtZ`, `colAtX`, `shrineCenter`.
- **The day** — `PHASES`, `PHASE_LEN`, `CYCLE`, `DAWNS_TO_WIN`, `sunHeightFor(phase, t)`.
- **Shared state** — `FL.state` (screen, phase, plots, seeds, score, shrine threat, sun height); the rules slice writes it, everyone reads it.
- **Events** — `FL.events.on / emit / clear`, with the event names every slice agrees on listed in the source.
- **The look** — `PALETTE`, and `sz(fraction)` / `isPortrait()` for UI sizes proportional to the live screen.
Consumed by every component and module under `/zero/source/game/`.