# firstlight.hut
The living hut as a place to stay alive in. Attached to `living_hut` by the
scene build. At awake it spawns the interior as temporary children under
`fl_hut_interior`: the bunk along the west wall, the table and stool against
the east wall with the barometer and thermometer above them, two rafters
under the roof, the lamp bracket and lamp on the north wall — and, once a
survivor has died here, the roof cache (`fl_roof_cache`), a canvas bundle
roped to the east rafter holding a quarter of their stores.
## What it runs
`Rules.state.hut.insideC` settles toward
airC + (shelterC + stoveHeat * stoveC) / (1 + gaps * (0.35 + 0.02 * windMs))
capped at `maxInsideC`, with the lag of a timber room. Unpatched wind gaps
(`Rules.state.hut.gaps`, starting at `Rules.HUT_GAPS_START`) are what let
the stove's warmth out; a patched gap outlasts the run.
## Methods (call with a colon on `entity.find("living_hut").component.get("hut")`)
| Method | Does | Returns |
|---|---|---|
| `hut:sleep()` | Sleep in the bunk until 07:00 tomorrow (`Rules.sleepToMorning()`). Refused while the hut is below -25 C with the stove cold. The night burns fuel and warms or chills the body by the hut's warmth. | HUD line (string) |
| `hut:patchGap()` | Patches one wind gap; persists across runs. | gaps still open (number) |
| `hut:state()` | `{ insideC, gaps, targetC, cache }` | table |
| `hut:bunkPosition()` | Hut-local position of the bunk, for the prompt. | `{ x, y, z }` |
## Fields
- `shelterC` (12) — degrees the hut rises above the air on shelter alone.
- `stoveC` (75) — degrees a high burn adds in a tight hut before gap losses.
- `maxInsideC` (26) — the warmest the hut gets.
# a_steel_fox_trap_snapping_shut__spring_a
Managed audio asset. Edit the `.metadata` `settings` block to change compression.
# a_fishing_line_pulled_up_through_a_hole
Managed audio asset. Edit the `.metadata` `settings` block to change compression.
# firstlight.body
The survivor's body, and the record of the run. Attached to `GameDirector`
by the scene build.
## What it runs, every play frame
- Where the local player stands: `Rules.state.body.indoors` is true inside
the living hut's footprint; `nearFire` (0..1) is the stove's warmth
reaching the body, full within a metre of the firebox and gone at
`fireReachM`.
- `coreC`: loses `lossPerDegree` per second per degree the felt temperature
sits below `comfortC` — the hut's `insideC` indoors, the weather's `chillC`
outdoors — multiplied by `1 + 2.5 * wet`; gains `fireGain * nearFire`. At
-40 C in still air a warm body dies in about 15 real minutes (1.5 in-game
days); by a high stove it is back to normal in three.
- `kcal`: `Rules.KCAL_PER_DAY_REST` per day at rest indoors, toward
`Rules.KCAL_PER_DAY_WORK` moving outdoors.
- `wet`: snow and storm soak clothes outdoors; the fire dries them.
- `frostbite` below `Rules.CORE_C_FROSTBITE`: the avatar's
`CharacterController.moveSpeed` is scaled by `frostbiteSpeed` until the core
recovers.
- Death at `Rules.CORE_C_DEATH` or `Rules.KCAL_DEATH`, through
`firstlight.legacy`: the run ends, the last journal line is written, the
cache is stashed, the save is written and `death { cause, day }` is emitted.
A `death` another slice emits (the bear) is handled the same way.
- Sleep (`sleep` event): eight hours of rest at the hut's warmth.
- The journal: a line for the day that ended on every `day` event.
- The save: `firstlight.persist` writes `run`, `body`, `hut`, `stores` and
`journal` on every day, sleep, death, patch and (throttled) store change,
and on leaving play; it reads them back at awake so a reload resumes the
same morning. The forecast seed is chosen once per world at the first load.
## Methods (call with a colon on `entity.find("GameDirector").component.get("body")`)
| Method | Does | Returns |
|---|---|---|
| `body:eat(kcal)` | Adds calories (a tin is about 800). | calories held (number) |
| `body:warm(degrees)` | Warms the core directly (a hot meal). | core C (number) |
| `body:newRun()` | Starts the next run from the death screen: body, clock and stores reset to the start values plus the cache; journal, seed, deaths, patched gaps and drilled hole stay. Emits `newRun { deaths, cache }`. | — |
| `body:save()` | Writes the save now. | — |
| `body:state()` | `{ coreC, kcal, wet, frostbite, indoors, nearFire, alive, day }` | table |
# firstlight.stove
The cast-iron box stove in the living hut. Attached to `living_hut` by the
scene build. At awake it spawns the stove (hearth, legs, firebox, door with
mica window and draught vent, top plate and rings, flue through the roof,
poker, the wood stack) as temporary children under `fl_stove`, with a
flickering amber point light and the crackle loop
(`firstlight.body.stove_crackle`).
## What it runs
- Three levels: 0 out, 1 low (`Rules.STOVE_FUEL_PER_HOUR_LOW` kg/h), 2 high
(`Rules.STOVE_FUEL_PER_HOUR_HIGH` kg/h). Fuel burns from the firebox
(`Rules.state.hut.fuelKg`, up to `fireboxKg`), which is loaded from
`Rules.state.stores.fuel`.
- Embers: a fire damped down or burned out keeps embers for
`Rules.STOVE_EMBER_HOURS`; lighting from embers costs no match, otherwise a
match comes off `Rules.state.stores.matches`.
- `Rules.state.hut.stoveHeat` (0..1) follows the burn with the lag of iron;
the light, the window glow, the crackle volume and the hut's warmth
(firstlight.hut) all read it.
- The night: on `sleep` the stove burns the hours slept.
- Emits `stove` `{ level, heat }` on every change of level or on/off.
## Methods (call with a colon on `entity.find("living_hut").component.get("stove")`)
| Method | Does | Returns |
|---|---|---|
| `stove:light()` | Lights the fire at low. Free from embers, otherwise strikes a match. Loads 1.5 kg from the store if the firebox is empty. | HUD line (string) |
| `stove:setLevel(n)` | `0` damps down to embers, `1` low burn, `2` high burn. Lights the stove first if it is out. | HUD line (string) |
| `stove:addFuel(kg?)` | Moves wood (default 1.5 kg) from `stores.fuel` into the firebox. | kg moved (number) |
| `stove:state()` | `{ on, level, heat, embers, fuelKg, matches }` | table |
| `stove:firePosition()` | Hut-local position of the firebox, for a prompt or a bear. | `{ x, y, z }` |
Every method also speaks its line through `Rules.say`.
## Fields
- `fireboxKg` (5) — the most wood the firebox holds.
- `lightIntensity` (9), `lightRadius` (8) — the fire light at a high burn.
# a_small_plastic_headlamp_switch_clicking
Managed audio asset. Edit the `.metadata` `settings` block to change compression.
# 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()`.
# a_hand_ice_auger_drilling_through_thick
Managed audio asset. Edit the `.metadata` `settings` block to change compression.
# picking_up_a_dented_tin_can_from_a_woode
Managed audio asset. Edit the `.metadata` `settings` block to change compression.
# 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.
# gathering_firewood__two_heavy_logs_knock
Managed audio asset. Edit the `.metadata` `settings` block to change compression.
# firstlight.stores
What the survivor has. The numbers live on `Rules.state.stores`; this module
is the one door every change goes through, so every change clamps at zero and
emits the `stores` event.
```lua
local Stores = require("firstlight.stores")
Stores.add("tins", 1) --> total, and emits stores { key = "tins", delta = 1, total = 11 }
Stores.take("matches", 1) --> true when there was one to take
Stores.has("batteries") --> true when at least one
Stores.food() --> meals in hand: tins + fish + meat
```
## API
| Call | What it does |
|---|---|
| `Stores.add(key, delta)` | Change a store; negative spends. Never below zero. Returns the total, emits `stores`. |
| `Stores.set(key, value)` | Set outright. Emits `stores`. |
| `Stores.get(key)` | Read a number (0 when unset). |
| `Stores.has(key, n?)` / `Stores.take(key, n?)` | Check for, or spend, `n` (default 1). |
| `Stores.food()` / `Stores.foodKg()` | Meals in hand, and their weight in kg. |
| `Stores.cacheOutside()` / `Stores.bringInside()` | Move raw fish and meat to the cache outside the hut door and back. `foodOutside` is what the bear smells. |
| `Stores.lootLeft(id, initial)` / `Stores.lootTake(id, initial)` | Units left at a scavenge point, seeded on first ask. |
| `Stores.visitedToday(key)` / `Stores.markVisited(key)` | Once-a-day gathering sites. |
| `Stores.ensure()` | Seed the fields below on the blackboard. `firstlight.sites` calls it at awake. |
## Keys on `Rules.state.stores`
Seeded by `firstlight.rules`: `tins`, `fuel` (kg wood-equivalent, what the
stove burns), `lampOil` (litres), `batteries`, `matches`, `fish`, `meat`,
`foodOutside` (kg), `headlampOn`, `holeDrilled`.
Added by this slice:
| Key | Meaning |
|---|---|
| `timber` | scrap planks for repairs |
| `wire` | copper wire from the generator shed |
| `drill` | the ice drill is in hand |
| `trapSet`, `trapSetDay` | the trap line is armed, and the day it was |
| `lampLit` | the hut lamp is burning (drains `lampOil`) |
| `headlampCharge` | 0..1 of the battery in the headlamp; a fresh battery is taken from `batteries` when it runs out |
| `wreckLooted`, `caribouTaken`, `caribouDay` | the one-time hauls, and the day the carcass appears |
| `loot` | scavenge point id -> units left |
| `lastVisitDay` | gathering site -> last day gathered |
| `cachedFish`, `cachedMeat` | what is in the outdoor cache |
# striking_a_wooden_match_and_a_kerosene_l
Managed audio asset. Edit the `.metadata` `settings` block to change compression.
# Mesh
Mesh asset. `data.zmsh` is the engine-native geometry payload (`renderer.mesh.encode`); consumers reference the asset's guid.
# Texture
Managed texture asset. `data.ztex` is the encoded payload the renderer uploads; compression settings live in the `.metadata` `settings` block (edit to re-encode).