# SoundClip
Managed audio asset. `data.zaud` is the engine-native ZAUD payload the runtime decodes and plays; compression settings live in the `.metadata` `settings` block (edit to re-encode).
# 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.
# Sfx
The sounds the Facer and the plate make, computed from raw samples.
`Sfx.ensure()` bakes anything not already registered and is safe to call on
every load. `Sfx.report()` reads each clip's header back — sample rate,
channels, frames, duration — which is how a clip that came out empty or at the
wrong channel count is caught before it ships as silence.
| clip | what it is |
|---|---|
| `fl_plate_raise` | the bronze ring as the plate comes up: inharmonic partials over a struck envelope, each decaying faster than the one below, with a short breath of strike noise at the front so the hit has an edge before the tone takes over |
| `fl_plate_lower` | the same body, damped and lower: the plate coming down |
| `fl_beam_hum` | the sustained note of the beam. Every partial is a whole multiple of one over the clip's length, so the waveform arrives back where it started and the loop has no seam |
| `fl_step_soil` | a footfall on dry terrace soil: a noise burst through two lowpasses over a short body thump |
The hum is stored uncompressed so its first milliseconds survive intact and the
seam stays silent; the one-shots are compressed, which they do not care about.
# SoundClip
Managed audio asset. `data.zaud` is the engine-native ZAUD payload the runtime decodes and plays; compression settings live in the `.metadata` `settings` block (edit to re-encode).
# SoundClip
Managed audio asset. `data.zaud` is the engine-native ZAUD payload the runtime decodes and plays; compression settings live in the `.metadata` `settings` block (edit to re-encode).
# Look
The Facer's materials, geometry and body.
`Look.ensureAssets()` mints every mesh and material the figure is made of that
is not registered yet, and is safe to call on every load.
`Look.rebuildAssets()` re-writes the geometry of all of them — the authoring
pass, run after changing a profile in this file. `Look.build(body)` hangs the
figure under a body entity and hands back the handles the Facer component poses
and lights. `Look.pose(rig, t)` puts the figure between carrying the plate at
the chest (`t = 0`) and holding it up into the dawn (`t = 1`).
## The figure
A robe that hangs in folds, a mantle over the shoulders, a cowl whose peak
leans back, and two jointed arms. Every part is modelled facing the entity's own
forward, so a pose is stated as points — the elbow is *here*, the hand is
*there* — and the rotation comes from aiming at them. Re-proportioning the
figure therefore needs no angle re-derived.
## The plate
A dished bronze lens with a raised rim, twelve rays alternating long and short,
a domed boss, sixteen rivets, an embossed face on the front, and a cross-brace
and haft behind. Four grades of bronze carry it: the weathered body, the
polished rim and rays the keeper keeps bright, the green patina in the recesses
and on the face, and the leather of the haft.
## The light
Three surfaces answer the beam: the shaft itself, two halo cards in the plate's
own plane so light spills round it whichever side the camera is on, and a pool
where the beam lands. All three are additive, so they add light to the hill and
never darken it. A point light rides on the plate and is what actually falls on
the soil.
`FL.PALETTE` supplies the colours, so the plate's amber and the game's first
light are the same value.
# 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/`.
# SoundClip
Managed audio asset. `data.zaud` is the engine-native ZAUD payload the runtime decodes and plays; compression settings live in the `.metadata` `settings` block (edit to re-encode).