# 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).
# core
<!-- 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.
# Henter
The player's body on the hill. Rides on the PlayerPrototype's `body` beside
`game.hero.HenterBody`, which is the figure you see.
## The one verb
The hent is a **lunge**, not a snapshot. Pressing it carries the Henter
`Core.HENT.lunge` metres forward and holds the lantern's cone open for the
length of that lunge, so what the snatch sweeps through is what it catches —
without that, a hent aimed at a mote a step away lands behind it. The cone
itself (`Core.HENT.arc` degrees, `Core.HENT.range` metres, measured from
`hentReach` in front of the body) is judged by `game.rules.GlewField`; this
component decides only WHEN and WHERE, and announces it with the `hent` event
so the hoggies can work out whether they were the thing that got snatched.
`hent` carries `caught` — the motes the first frame of the lunge took. Motes
swept up over the rest of it are added to the lantern the same way, each with
its own `glew` event, so the HUD's count is always the true one.
A hent that lands calls `HenterBody:playHent()`, and any change in the lantern
calls `HenterBody:setGlow(lantern / Core.QUOTA.gold)` — both only when those
methods exist, so the body can be a placeholder without breaking the round.
## Taking a butt
On the `butt` event this body loses `lost` motes out of the lantern (emitting
a `glew` with a negative delta), drops whatever lunge is in flight, and sets
`staggerUntil` to `roundTime + Core.HENT.stagger` — for those seconds it moves
at 45% speed. `game.audio.sfx` plays `game.rules.clips.lantern_spill` where it
happened.
## Controls
Everything comes from `game.henter.inputMap`: `move`, `hent`, `pause`. Keyboard,
gamepad and the on-screen touch controls are the same three control names, so
nothing here reads a raw key and any family drives the whole game. Only the
owning peer's input moves a body (`entity:isLocal()`), so a second player on
the hill drives their own Henter and not yours.
## Movement
Position comes from `Core.ground(x, z)` — nobody raycasts. Heading straight
upslope costs up to `Core.MOVE.uphillPenalty` of the speed, which is what makes
climbing toward the light a decision rather than a formality. The body holds
still while `Core.state.cutscene` is true.
# 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).
# game.audio.sfx
Play a one-shot from anywhere on the hill.
```lua
local Sfx = require("game.audio.sfx")
Sfx.play(clipRef) -- flat, non-spatial
Sfx.play(clipRef, { pos = { x, y, z }, volume = 0.7 }) -- out on the hill
Sfx.play(clipRef, { pitch = 1, jitter = 0.12 }) -- a run that is not a machine
Sfx.ui("confirm") -- the menu's own set
```
It spawns a temporary entity carrying an `Audio` component, lets it sound, and
despawns it after `ttl` seconds. Pass `pos` for a spatial source, heard from
the `AudioListener` on the CameraRig. Resolve your clip through a **literal**
identity so the reference is pinned when this world is installed elsewhere.
## Three things every caller gets
**A voice budget.** Below a reserve of free voices a low-priority one-shot is
dropped rather than stealing the narrator's. `priority` 0 is set dressing, 1
is the default, 2 survives a crowded mix, 3 always plays.
**A collapse window.** Ten motes caught in one frame is one sound, not ten
stacked copies 6 dB louder than anything else in the mix. The same clip asked
for again inside `collapse` seconds (35 ms by default) is dropped. Pass a
longer window for a sound that should never double up, or `priority = 3` to
opt out.
**Pitch jitter.** `jitter` spreads repeats of one clip either side of their
pitch, so a run of them reads as a hill rather than a loop.
## Options
`pos`, `volume`, `pitch`, `jitter`, `maxDistance`, `channel`, `ttl`, `name`,
`priority`, `collapse`, `looping`.
`channel` is one of `sfx` / `ui` / `ambient` / `music` / `voice` — the names
the mix knows. A source naming anything else plays at unity, which is a sound
nobody can turn down, so an unknown name falls back to `sfx`.
## The menu's set
`Sfx.ui(name)` plays one of `click` / `move` / `confirm` / `back` / `deny` —
small struck-glass sounds, because the lantern is glass and brass and so is
the menu. Non-spatial, on the `ui` channel, and high enough priority to get
through a crowded mix.
`Sfx.stopAll()` drops every one-shot still sounding, so a hent that started a
frame before the round ended does not carry into the end card.
# 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).
# hoggyart
The hoggy as a thing on screen.
One hoggy is a generated quilled body under a rig of authored parts: a ridge of
blunt spine quills that gives the silhouette at the gameplay camera, a belly
that fills with the glew it has eaten, a snout and nose carried past the body's
outline so the creature reads as aimed, two eyes that catch the light, and a
warm point light inside the belly that puts the hoard on the grass around it.
The generated body carries the fine detail — snout, ears, pale underside, the
fuzz of small quills. The authored parts carry the read from twenty metres up
the hill, where that fuzz is below a pixel.
```lua
local Art = require("game.hoggy.hoggyart")
local parts = Art.build(rootEntity, seed) -- seed 0..1, per-creature variation
Art.setBelly(parts, 0.6) -- 0 empty .. 1 full
Art.setAlarm(parts, 1) -- 0 calm .. 1 about to charge
Art.pose(parts, dt, { gait = "waddle", speed = 2.4 })
Art.destroy(parts)
```
`build` returns a plain table of live entity proxies; every other call takes it
back. Everything it spawns is **temporary**, so nothing here is saved into the
scene.
## The two reads
`setBelly` is what the player crosses the hill for: the belly swells and lights
the grass, and the body warms from slate toward amber. `setAlarm` is the tell
they need a moment later: the per-instance outline goes amber while the hoggy
is worth henting and ember while it has decided to come at them, and the ridge
lifts and splays as it rouses.
Neither touches a material, so five hoggies at five different fullnesses share
one set of materials — the fullness lives in per-instance tint, outline, scale
and light intensity.
## The gait
A hoggy is round and short-legged, so it does not walk, it rocks. `pose` takes
a `gait`: `waddle` while it grazes, `charge` when it is coming for the lantern,
`eat` with the snout down, `stun` for the wobble after it has hit something,
and `idle` when it is standing about.
## Where the body comes from
`generated.meshes.A_stylised_low_poly_folk_tale_creature` — a `.bundle`, about
1.9 m long about its own origin with the snout along `+Z`, scaled down to
`Art.LENGTH` and turned to face the way the creature walks. With that bundle
absent the module blocks out a round body instead, so the ridge, the belly and
the face still read.
# GlewField
Every mote of glew on the hill, and the hent that takes them. One instance,
on the scene's `mount_glew`.
Glew gathers in **drifts** — three thick patches on the dark side of the hill.
A drift slides downhill at a little under half the speed the light-line walks
down from the summit, so the sun steadily closes on it: the glew worth having
is always the glew nearest the light. When the light-line reaches a drift the
motes standing in it burn off over `Core.GLEW.burnSeconds`, and a new drift
gathers lower in whatever is still dark. Reading where the drifts are, and
judging how long the sun will leave them, is the first half of the round.
A mote is a temporary entity named `glew_<n>` carrying `game.rules.GlewMote`
and a Model of the emissive `game.materials.glew_mote` sphere with a small
warm point light. When `game.hero.GlewLook` is in the world the field adds it
to every mote as well, and the look owns the mote's appearance from there — it
may hide or replace the sphere. The field keeps writing `burn` on both the
mark and the look, and calls `look:caught()` when a mote flies to the lantern.
## What other slices call
Take the ref off the mount:
```lua
local field = entity.find("mount_glew").component.get("game.rules.GlewField")
```
| Call | Answers |
|---|---|
| `field:nearest(x, z, radius)` | entity ids of the motes within `radius` metres of `(x, z)`, **nearest first**. A mote already flying to the lantern is not offered. |
| `field:take(entityId)` | `true` when that mote was there to take, and it is gone — a hoggy ate it. |
| `field:spawnBurst(x, y, z, n)` | Fountains `n` motes out around a point: what a full hoggy spills when it is hented. |
| `field:hentInto(x, y, z, facing)` | Runs a hent from `(x, y, z)` facing `facing`: everything inside `Core.HENT.arc` within `Core.HENT.range` flies to the lantern. Returns how many were caught, adds them to `Core.state.lantern` (capped at `Core.LANTERN_CAP`) and emits `glew`. |
| `field:count()` | Motes alive right now. |
| `field:coldCount()` | Motes alive and not yet burning. |
A hoggy that bursts may call `spawnBurst` **and** emit `burst`; the field
honours whichever arrives and ignores the second when both name the same
burst, so the motes are spilled exactly once.
## The round
The field runs while the phase is `playing`, and keeps running through `won`
and `lost` without spawning anything new — so at first light the last motes
are still burning off and the resolution looks at a hill going clean rather
than at one frozen mid-burn. Entering `playing` gathers fresh drifts and
stands most of the population up at once, with the first drift within a few
steps of the foot so the round opens with glew the Henter can reach.
## Sound
The catch (`game.rules.clips.glew_catch`) and the fizzle of a mote taken by
the sun (`game.rules.clips.glew_burn`) are played through `game.audio.sfx` at
the mote's own position, so both are spatial and heard from the CameraRig's
listener.
# 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).