# 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.
# 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/`.