World sounds are heard in a radius around the bot, not the view

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-23 23:40:08 -04:00
co-authored by Claude Opus 5.5
parent af9759f9df
commit dd8fc3d866
5 changed files with 36 additions and 18 deletions
+7 -6
View File
@@ -178,12 +178,13 @@ above, including:
- **Fewer, smaller messages.** Molds show only a small percentage, red when blocked or spoiled and gold when a casting waits for F; details are in the hover panel. Bars dropping, blocked molds and set castings no longer pop up cards or toasts. Fan heat is a red marker on the fan; melted tubes and machines get a short red label where they were. Status lines are one short sentence. The owner found the messages too large and too many. - **Fewer, smaller messages.** Molds show only a small percentage, red when blocked or spoiled and gold when a casting waits for F; details are in the hover panel. Bars dropping, blocked molds and set castings no longer pop up cards or toasts. Fan heat is a red marker on the fan; melted tubes and machines get a short red label where they were. Status lines are one short sentence. The owner found the messages too large and too many.
- **World sounds are local.** Sounds from the world (water sizzling on hot metal or lava, and - **World sounds are local.** Sounds from the world (water sizzling on hot metal or lava, and
any other event with a place) fade with distance from the view and pan left or right; they go any other event with a place) are heard around the bot, whatever the zoom: full volume within
silent one screen width off-screen. The owner heard a constant snapping that followed the bot 24 grains, fading to silent at 96, panned left or right. The owner heard a constant snapping
in an older world but not in a new one. It was the sizzle: in the owner's world, water that that followed the bot in an older world but not in a new one. It was the sizzle: in the
had found its way down to hot basalt and granite (480-800°C, about 500 grains below the owner's world, water that had found its way down to hot basalt and granite (480-800°C, about
surface) boiled about 48 times a minute, and every sizzle played at full volume. 500 grains below the surface) boiled about 48 times a minute, every one at full volume. The
first fix faded sounds with distance from the view; zoomed out they were still heard, so the
owner asked for a radius around the bot instead.
- **The silo has pixel art in the pixel look**, drawn grain by grain like the rest of the world; - **The silo has pixel art in the pixel look**, drawn grain by grain like the rest of the world;
the smooth look keeps the illustrated silo. The owner noticed it looked the same in both. the smooth look keeps the illustrated silo. The owner noticed it looked the same in both.
+2 -2
View File
@@ -354,10 +354,10 @@ Smooth look: a WebGL2 pass that turns the per-grain buffers drawWorld fills (col
- functions: smCompile, smoothAvailable, smUpload, smoothDraw, smoothPace - functions: smCompile, smoothAvailable, smUpload, smoothDraw, smoothPace
- variables: SM, SM_VS, SM_FS, smBytes - variables: SM, SM_VS, SM_FS, smBytes
### src/ui/audio.js (228 lines) ### src/ui/audio.js (230 lines)
Audio: chiptune effects and generative music that changes with depth. Everything is synthesized with WebAudio, no samples. Audio: chiptune effects and generative music that changes with depth. Everything is synthesized with WebAudio, no samples.
- functions: auInit, auVolumes, rateOk, tone, noiseBurst, sfxPlace, playSfx, auAmbient, mtof, zoneForDepth, scaleNote, musicScheduler - functions: auInit, auVolumes, rateOk, tone, noiseBurst, sfxPlace, playSfx, auAmbient, mtof, zoneForDepth, scaleNote, musicScheduler
- variables: AU, SFX, ZONES - variables: AU, SFX, SFX_NEAR, SFX_FAR, ZONES
### src/ui/state.js (67 lines) ### src/ui/state.js (67 lines)
Game shell (src/ui/): browser only. This file: settings, the GAME state object, DOM helpers, toasts and the status line. Game shell (src/ui/): browser only. This file: settings, the GAME state object, DOM helpers, toasts and the status line.
+12
View File
@@ -4,6 +4,18 @@ What agents have done, newest first. Add an entry for every piece of work: the d
note or request it was for, what changed, the files touched, the tests run, and anything left note or request it was for, what changed, the files touched, the tests run, and anything left
open or not verified. Keep each entry short; the git history has the details. open or not verified. Keep each entry short; the git history has the details.
## 2026-10-23: world sounds are heard around the bot, not the view (Claude Code)
Note: "the sound effect is still heard when the player's zoom level is farther out… sounds
shouldnt be heard based on the viewport, but rather in a radius from the player."
- `src/ui/audio.js` `sfxPlace`: gain from the distance to the bot's centre (full within 24
grains, silent at 96), pan from the horizontal offset; zoom no longer matters.
- `test/play.py`: the audio checks use points relative to the bot; a new check zooms out to 2
and plays a visible sizzle 200 grains away: silent (it played before). `play.py` and all 11
headless suites pass. `docs/DESIGN.md`.
- Open: nothing.
## 2026-10-23: F on an ingot mold does nothing and says nothing (Claude Code) ## 2026-10-23: F on an ingot mold does nothing and says nothing (Claude Code)
Note: "now that ingot molds empty themselves, F still makes a message…" Note: "now that ingot molds empty themselves, F still makes a message…"
+5 -3
View File
@@ -118,10 +118,12 @@ const SFX = {
sale() { if (!AU.ctx) return; const t = AU.ctx.currentTime; [784, 988, 1175, 1568].forEach((f, k) => tone(f, 0.09, { wave: 'p25', vol: 0.05, at: t + k * 0.06 })); }, sale() { if (!AU.ctx) return; const t = AU.ctx.currentTime; [784, 988, 1175, 1568].forEach((f, k) => tone(f, 0.09, { wave: 'p25', vol: 0.05, at: t + k * 0.06 })); },
discover() { if (!rateOk('disc', 500)) return; const t = AU.ctx && AU.ctx.currentTime; tone(880, 0.06, { wave: 'p25', vol: 0.04, at: t }); tone(1175, 0.1, { wave: 'p25', vol: 0.04, at: t + 0.06 }); }, discover() { if (!rateOk('disc', 500)) return; const t = AU.ctx && AU.ctx.currentTime; tone(880, 0.06, { wave: 'p25', vol: 0.04, at: t }); tone(1175, 0.1, { wave: 'p25', vol: 0.04, at: t + 0.06 }); },
}; };
// World sounds are heard around the bot, whatever the zoom: full within 24 grains, silent at 96
const SFX_NEAR = 24, SFX_FAR = 96;
function sfxPlace(x, y) { function sfxPlace(x, y) {
if (typeof R === 'undefined' || !R.bw) return { gain: 1, pan: 0 }; if (typeof bot === 'undefined' || !bot.w) return { gain: 1, pan: 0 };
const dx = Math.max(R.vx0 - x, 0, x - R.vx0 - R.bw), dy = Math.max(R.vy0 - y, 0, y - R.vy0 - R.bh); const dx = x - (bot.x + bot.w / 2), dy = y - (bot.y + bot.h / 2), d = Math.hypot(dx, dy);
return { gain: Math.max(0, 1 - Math.hypot(dx, dy) / R.bw), pan: Math.max(-1, Math.min(1, (x - R.vx0 - R.bw / 2) / (R.bw / 2))) * 0.6 }; return { gain: Math.max(0, Math.min(1, (SFX_FAR - d) / (SFX_FAR - SFX_NEAR))), pan: Math.max(-1, Math.min(1, dx / SFX_FAR)) * 0.6 };
} }
function playSfx(name, arg, x, y) { function playSfx(name, arg, x, y) {
if (!AU.ctx || !SFX[name]) return; if (!AU.ctx || !SFX[name]) return;
+10 -7
View File
@@ -26,15 +26,18 @@ with sync_playwright() as p:
pg.wait_for_timeout(500) pg.wait_for_timeout(500)
ev = pg.evaluate ev = pg.evaluate
ev("window._maxStatus = ''; const _ss = setStatus; setStatus = function (s, k) { if (s && s.length > _maxStatus.length) _maxStatus = s; return _ss(s, k); }; 0") ev("window._maxStatus = ''; const _ss = setStatus; setStatus = function (s, k) { if (s && s.length > _maxStatus.length) _maxStatus = s; return _ss(s, k); }; 0")
# World effects use the view, and inaudible ones must not throttle nearby sounds. # World sounds are heard around the bot at any zoom, and inaudible ones must not throttle nearby sounds.
ev("auInit(); window._noiseCalls = 0; window._noiseBurst = noiseBurst; noiseBurst = function (...args) { _noiseCalls++; return _noiseBurst(...args); }; 0") ev("auInit(); window._noiseCalls = 0; window._noiseBurst = noiseBurst; noiseBurst = function (...args) { _noiseCalls++; return _noiseBurst(...args); }; 0")
ev("window._bc = (dx, dy) => [bot.x + bot.w / 2 + dx, bot.y + bot.h / 2 + dy]; 0")
try: try:
far = ev("(() => { const n = _noiseCalls; AU.last.sizzle = 0; const x = R.vx0 + R.bw * 3, y = R.vy0 + R.bh / 2; playSfx('sizzle', 0, x, y); return [_noiseCalls - n, sfxPlace(x, y).gain, AU.last.sizzle]; })()") far = ev("(() => { const n = _noiseCalls; AU.last.sizzle = 0; const [x, y] = _bc(200, 0); playSfx('sizzle', 0, x, y); return [_noiseCalls - n, sfxPlace(x, y).gain, AU.last.sizzle]; })()")
check('a sound far off-screen is silent', far == [0, 0, 0], far) check('a sound 200 grains from the bot is silent', far == [0, 0, 0], far)
near = ev("(() => { const n = _noiseCalls; AU.last.sizzle = 0; const x = R.vx0 + R.bw / 2, y = R.vy0 + R.bh / 2; playSfx('sizzle', 0, x, y); return [_noiseCalls - n, sfxPlace(x, y).gain, AU.bus == null]; })()") near = ev("(() => { const n = _noiseCalls; AU.last.sizzle = 0; const [x, y] = _bc(10, 0); playSfx('sizzle', 0, x, y); return [_noiseCalls - n, sfxPlace(x, y).gain, AU.bus == null]; })()")
check('a sound on screen plays', near == [1, 1, True], near) check('a sound next to the bot plays at full volume', near == [1, 1, True], near)
quiet = ev('sfxPlace(R.vx0 + R.bw * 1.5, R.vy0 + R.bh / 2)') quiet = ev('sfxPlace(..._bc(60, 0))')
check('a sound just off-screen is quieter', 0 < quiet['gain'] < 1 and quiet['pan'] > 0, quiet) check('a sound 60 grains to the right is quieter and panned right', 0 < quiet['gain'] < 1 and quiet['pan'] > 0, quiet)
zoomed = ev("(() => { const z = SET.zoom; SET.zoom = 2; resizeRender(); const n = _noiseCalls; AU.last.sizzle = 0; const [x, y] = _bc(200, 0); const vis = x < R.vx0 + R.bw; playSfx('sizzle', 0, x, y); SET.zoom = z; resizeRender(); return [vis, _noiseCalls - n]; })()")
check('zoomed far out, a visible sound 200 grains away stays silent', zoomed == [True, 0], zoomed)
finally: finally:
ev('noiseBurst = _noiseBurst; delete window._noiseBurst; delete window._noiseCalls; 0') ev('noiseBurst = _noiseBurst; delete window._noiseBurst; delete window._noiseCalls; 0')
look = sys.argv[2] if len(sys.argv) > 2 else 'pixel' look = sys.argv[2] if len(sys.argv) > 2 else 'pixel'