Three things a game could not say, each of which had been worked around. Audio.play_at(id, gain:, pitch:, pan:) fires a one-shot with its own gain, pitch and stereo position. Audio.volume and Audio.pitch are global - they are the options screen - so a game placing a sound in the world was fighting them, and distance attenuation was simply not expressible. The backend already took volume and rate per call; this adds setPan: alongside them and stops routing through the master state. ludic.render3d gains outline_model(model, mat, width, r, g, b): a rim around something that is not an Actor. The outline pass walked the actor list and stopped, so instanced scatter - a forest - could not be highlighted at all. It is a queue flushed by the same pass, which is what gets the depth test right when the caller does not control pass order. And terrain_coast(cx, cz, margin, fall), the other way to make an island: the sea around the survey's own edge rather than cut out of the middle of it. terrain_island measures a radius from a centre, which drowns two thirds of a real survey to make an island of the rest; this measures inward from the boundary, so everything the data covers stays land and the coast is where the data runs out. Both modes gained a strand - the last few metres of height either side of the water line compressed, which stretches a cliff plunge out into beach and shallows. 132 regression tests and the self-host fixpoints pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1.5 KiB
| id | name | category | kind | tokens | sig | tip | order | ns | member |
|---|---|---|---|---|---|---|---|---|---|
| audio-play_at | Audio.play_at | audio | namespace-method | Audio.play_at | Audio.play_at(id, gain, pitch, pan) -> void | Fire a one-shot with its own gain, pitch and stereo position. | 2 | Audio | play_at |
Plays the sound id from the start as a one-shot, with its own gain, pitch
and stereo position, leaving the master settings alone.
Audio.volume and Audio.pitch are global: they exist so a player
can turn the game down, and a game that used them to place a sound in the world would be
fighting its own options screen. This is the per-voice version, and it is what distance
attenuation is made of — work out how far away a sound is and which side it is on, and say
so here.
gain— 0.0 to 1.0, multiplied by the master volume, so the options screen still wins.pitch— 1.0 is as recorded; 0.5 an octave down, 2.0 an octave up. Clamped to the backend's 0.25 .. 4.0.pan— -1.0 hard left, 0.0 centred, 1.0 hard right.
program Demo {
entry {
let call = Audio.load("elk.wav")
# an elk eighty metres off, over your left shoulder
Audio.play_at(id: call, gain: 0.3, pitch: 0.96, pan: -0.55)
}
}
One honest limitation, and it is the backend's: a loaded sound is one player, so firing the same handle again restarts it rather than layering a second copy over the first. Load a handle per variant when several need to overlap — which is what a game does anyway, to stop a repeated sound machine-gunning.