# a_single_low__solemn_resonant_tone__like
Managed audio asset. Edit the `.metadata` `settings` block to change compression.
# firstlight.legacy
What the world remembers of a survivor. `require("firstlight.legacy")`.
- `Legacy.note(text)` — add a line to the current day's journal entry
(deduplicated). Any slice may call it for a notable event.
- `Legacy.journalLine(day)` — the line for `day`: clock, weather words,
stove, stores, frostbite, the bear, the notes.
- `Legacy.recordDay(day)` — append that line to `Rules.state.journal`.
- `Legacy.die(cause, day?, emitted?)` — end the run: `run.alive = false`,
`run.deaths += 1`, the last journal line, the cache (a quarter of every
store, on `Rules.state.run.cache`), the save. Emits `death { cause, day }`
unless `emitted` says the event is already on the bus. Returns false when
the run was already over.
- `Legacy.newRun()` — the next run from the death screen. Resets body, clock
and stores to the start values plus the cache; keeps journal, seed, deaths,
`hut.gaps` and `stores.holeDrilled`. Emits `newRun { deaths, cache }`.
- `Legacy.cache()` — the cache the last survivor left, or nil.
# a_single_soft__muted_UI_click__like_a_br
Managed audio asset. Edit the `.metadata` `settings` block to change compression.
# hud
<!-- zero:scaffolded-readme -->
<!-- Replace the line below with what this module provides and who
consumes it, then delete both comment lines. -->
TODO: describe this module.
# firstlight.interact
What the player is looking at and can use. A registry of entities that answer
to the interact key, a per-frame raycast from the player's camera that finds
which one is under the reticle, and `current()` for the HUD to draw the prompt.
```lua
local Interact = require("firstlight.interact")
```
## API
| Call | What it does |
|---|---|
| `Interact.register(entity, { label, onUse, key? })` | Make an entity usable. `entity` is a proxy or an id. `label` is a string, or a function returning the label for this moment — or `nil` when there is nothing to do right now (an empty crate, a trap already set), which hides the prompt. `onUse` runs on the interact key. `key` overrides the legend (default `"E"`). |
| `Interact.unregister(entity)` | Forget it. |
| `Interact.current()` | `{ label, key, entity, distance }` for the target under the reticle within reach, or `nil`. The HUD draws the reticle prompt from this. |
| `Interact.use()` | Use the current target. Returns whether something was used. |
| `Interact.scan(camera, playerRoot)` | The per-frame raycast. `firstlight.sites` calls it every play frame from the local player's camera; nothing else needs to. |
| `Interact.isRegistered(entity)` / `Interact.targets()` | Introspection. |
| `Interact.REACH_M` | Reach in metres (3). |
A hit on any descendant of a registered entity counts as a hit on it, so a
bundle's mesh children and a collider child resolve to the prop. Targets need
a collider for the ray to find them; `firstlight.sites` fits an invisible
`interact_hit` box to anything it registers that has none.
## Registering from another slice
```lua
local stove = entity.find("stove")
Interact.register(stove, {
label = function()
return if Rules.state.hut.stoveOn then "Stoke stove" else "Light stove"
end,
onUse = function() stove.component.get("firstlight.stove"):light() end,
})
```
The interact key is the `interact` control of `@builtin::inputMaps.default`
(E on a keyboard, the west face button on a pad, an on-screen button on a
phone). `firstlight.sites` owns that subscription and calls `Interact.use()`.
# bus
<!-- zero:scaffolded-readme -->
<!-- Replace the line below with what this module provides and who
consumes it, then delete both comment lines. -->
TODO: describe this module.
# firstlight.rules
The one module every part of First Light requires: `require("firstlight.rules")`.
It holds the game's numbers (day length, temperatures, calorie burn, stove fuel
rates, bear ranges, starting stores), the layout of the station and the sites
around it, the shared blackboard `M.state`, the event bus `M.events`, and the
clock.
## The clock
`M.tick(dt)` is called once per play frame by the scene entrypoint. It advances
`M.state.run.elapsed`, derives `M.state.clock` (day, fraction of day, hour, sun
elevation, twilight openness) and emits `day`, `noon` and `firstLight`.
`M.sleepToMorning()` jumps to 07:00 next day; `M.restore(elapsed)` rebuilds the
clock from a persisted run.
Pure helpers: `sunElevation(day, t)`, `twilightSeconds(day)`, `airC(day, t)`,
`windChill(airC, windMs)`, `progress(day)`, `rng(seed, salt)`, `dist2d`.
## The blackboard
`M.state` has one section per slice. A slice writes its own section and reads
the others: `run`, `clock`, `weather`, `body`, `hut`, `stores`, `bear`,
`journal`.
## Events
`M.events.on(name, fn)` returns an unsubscribe function; `M.events.emit(name,
payload)` calls every listener. The names and payloads are listed at the top of
`init.luau`. `M.say(text, seconds)` emits `say` for the HUD.
# firstlight.forecast
The thirty-day forecast of First Light, as a pure function of the world's
forecast seed (`Rules.state.run.seed`). The same seed gives the same thirty
days on every load, so a journal line about the weather is true for the next
run.
```luau
local Forecast = require("firstlight.forecast")
```
## For the HUD and the journal
```luau
-- Tomorrow's barometer reading
local clock = Rules.state.clock
local tomorrow = Forecast.forDay(clock.day + 1)
-- tomorrow.storm 0..1 on the storm scale
-- tomorrow.windMs m/s
-- tomorrow.airC °C at noon, before wind chill
-- tomorrow.aurora 0..1
-- tomorrow.band "calm" | "wind" | "storm" | "blizzard"
-- tomorrow.blizzard true on one of the trapped days
Forecast.words(tomorrow.band) -- "Calm" | "Wind" | "Storm" | "Blizzard"
```
Days clamp to 1..30, so `forDay(31)` is day 30. `Forecast.blizzardStart()`
names the first trapped day; the blizzard runs `Rules.BLIZZARD_DAYS` days from
there, between `Rules.BLIZZARD_EARLIEST_DAY` and `Rules.BLIZZARD_LATEST_DAY`.
## The live weather
What the air is doing right now is on the blackboard, written every frame by
the weather component: `Rules.state.weather` carries `storm`, `windMs`,
`windDir`, `visibilityM`, `airC`, `chillC`, `snowing` and `aurora`. Read it
from there rather than recomputing it.
`Forecast.at(day, t)` is the function behind that: neighbouring days blended
across the day, gusts added from a seed-fixed phase, and the whole moment
derived. `Forecast.moment(storm, day, t)` derives the same record from one
storm value.
## The scale
| band | storm | wind | visibility |
|---|---|---|---|
| calm | below 0.175 | a breath | the horizon |
| wind | 0.175 .. 0.525 | 6 .. 12 m/s | hundreds of metres |
| storm | 0.525 .. 0.85 | 12 .. 22 m/s | tens of metres |
| blizzard | 0.85 and up | up to `Rules.WIND_MAX_MS` | a few paces |
`Forecast.band(storm)`, `Forecast.windMs(storm)`, `Forecast.visibilityM(storm)`
and `Forecast.snowing(storm)` are the curves behind the table.
The wind on the plain always blows toward `Forecast.WIND_TOWARD` (east-north-east
in xz), the way the snowfield's sastrugi and spindrift stream; `Forecast.WIND_DIR`
is that as radians.
# screens
<!-- zero:scaffolded-readme -->
<!-- Replace the line below with what this module provides and who
consumes it, then delete both comment lines. -->
TODO: describe this module.