World sounds are local: fade with distance and pan (distant sizzles snapped constantly)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -177,6 +177,12 @@ 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
|
||||||
|
any other event with a place) fade with distance from the view and pan left or right; they go
|
||||||
|
silent one screen width off-screen. The owner heard a constant snapping that followed the bot
|
||||||
|
in an older world but not in a new one. Most likely the sizzle: in a test, a lava and water
|
||||||
|
contact far from the bot sizzled about 580 times a minute, all played at full volume.
|
||||||
|
|
||||||
## Ideas not built yet
|
## Ideas not built yet
|
||||||
|
|
||||||
- **Concrete**, as a stronger wall material from rubble.
|
- **Concrete**, as a stronger wall material from rubble.
|
||||||
|
|||||||
+2
-2
@@ -354,9 +354,9 @@ 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 (209 lines)
|
### src/ui/audio.js (228 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, 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, ZONES
|
||||||
|
|
||||||
### src/ui/state.js (67 lines)
|
### src/ui/state.js (67 lines)
|
||||||
|
|||||||
@@ -4,6 +4,23 @@ 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 local (Claude Code, with Codex)
|
||||||
|
|
||||||
|
Note: "there is a sound effect that seems to happen randomly but constantly… maybe the sound
|
||||||
|
effects are not local?" The owner then confirmed a new world is quiet.
|
||||||
|
|
||||||
|
- Measured headlessly over 5 game minutes: a fresh world emits no sounds; a lava and water
|
||||||
|
contact 720+ grains from the bot emitted 2,889 sizzles (about 580 a minute), and `events.js`
|
||||||
|
played every one at full volume. The sizzle (`heat.js` boiling and quenching) is the only sim
|
||||||
|
sound with a position.
|
||||||
|
- `src/ui/audio.js`: `sfxPlace(x, y)` gives gain (1 in view, 0 one view width beyond it) and
|
||||||
|
pan; `playSfx(name, arg, x, y)` skips silent ones before the rate limit and plays the rest
|
||||||
|
through a gain and stereo panner (`AU.bus`). `src/ui/events.js` passes positions.
|
||||||
|
`docs/DESIGN.md`, `docs/PROJECT_MAP.md`.
|
||||||
|
- `test/play.py`: far off-screen is silent, on screen plays, just off-screen is quieter and
|
||||||
|
panned. All 11 headless suites, `play.py` and `t_smooth.py` pass.
|
||||||
|
- Open: not tested against the owner's own save; the owner checks by ear.
|
||||||
|
|
||||||
## 2026-10-23: fewer, smaller messages (Claude Code, with Codex)
|
## 2026-10-23: fewer, smaller messages (Claude Code, with Codex)
|
||||||
|
|
||||||
Note: "lets change the engot mold and other status messages. they are way too large and just too
|
Note: "lets change the engot mold and other status messages. they are way too large and just too
|
||||||
|
|||||||
+22
-3
@@ -75,7 +75,7 @@ function tone(freq, dur, opts = {}) {
|
|||||||
g.gain.exponentialRampToValueAtTime(v, t + a);
|
g.gain.exponentialRampToValueAtTime(v, t + a);
|
||||||
g.gain.setValueAtTime(v, t + Math.max(a, dur - rel));
|
g.gain.setValueAtTime(v, t + Math.max(a, dur - rel));
|
||||||
g.gain.exponentialRampToValueAtTime(0.0001, t + dur);
|
g.gain.exponentialRampToValueAtTime(0.0001, t + dur);
|
||||||
o.connect(g); g.connect(opts.bus || AU.sfx);
|
o.connect(g); g.connect(opts.bus || AU.bus || AU.sfx);
|
||||||
o.start(t); o.stop(t + dur + 0.02);
|
o.start(t); o.stop(t + dur + 0.02);
|
||||||
}
|
}
|
||||||
function noiseBurst(dur, opts = {}) {
|
function noiseBurst(dur, opts = {}) {
|
||||||
@@ -87,7 +87,7 @@ function noiseBurst(dur, opts = {}) {
|
|||||||
if (opts.sweep) f.frequency.exponentialRampToValueAtTime(opts.sweep, t + dur);
|
if (opts.sweep) f.frequency.exponentialRampToValueAtTime(opts.sweep, t + dur);
|
||||||
const g = ctx.createGain(); const v = opts.vol || 0.08;
|
const g = ctx.createGain(); const v = opts.vol || 0.08;
|
||||||
g.gain.setValueAtTime(v, t); g.gain.exponentialRampToValueAtTime(0.0001, t + dur);
|
g.gain.setValueAtTime(v, t); g.gain.exponentialRampToValueAtTime(0.0001, t + dur);
|
||||||
s.connect(f); f.connect(g); g.connect(opts.bus || AU.sfx);
|
s.connect(f); f.connect(g); g.connect(opts.bus || AU.bus || AU.sfx);
|
||||||
s.start(t, Math.random() * 1.5); s.stop(t + dur + 0.02);
|
s.start(t, Math.random() * 1.5); s.stop(t + dur + 0.02);
|
||||||
}
|
}
|
||||||
const SFX = {
|
const SFX = {
|
||||||
@@ -118,7 +118,26 @@ 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 }); },
|
||||||
};
|
};
|
||||||
function playSfx(name, arg) { if (!AU.ctx || !SFX[name]) return; try { SFX[name](arg); } catch (e) { /* ignore */ } }
|
function sfxPlace(x, y) {
|
||||||
|
if (typeof R === 'undefined' || !R.bw) 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);
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
function playSfx(name, arg, x, y) {
|
||||||
|
if (!AU.ctx || !SFX[name]) return;
|
||||||
|
const place = typeof x === 'number' && typeof y === 'number' ? sfxPlace(x, y) : null;
|
||||||
|
if (place && place.gain < 0.02) return;
|
||||||
|
const prev = AU.bus; let gain, pan;
|
||||||
|
try {
|
||||||
|
if (place) {
|
||||||
|
gain = AU.ctx.createGain(); pan = AU.ctx.createStereoPanner();
|
||||||
|
gain.gain.value = place.gain; pan.pan.value = place.pan;
|
||||||
|
gain.connect(pan); pan.connect(AU.sfx); AU.bus = gain;
|
||||||
|
}
|
||||||
|
SFX[name](arg);
|
||||||
|
} catch (e) { /* ignore */ }
|
||||||
|
finally { AU.bus = prev; if (gain || pan) setTimeout(() => { if (gain) gain.disconnect(); if (pan) pan.disconnect(); }, 1500); }
|
||||||
|
}
|
||||||
|
|
||||||
// Ambient layer driven by what is on screen.
|
// Ambient layer driven by what is on screen.
|
||||||
function auAmbient(info) {
|
function auAmbient(info) {
|
||||||
|
|||||||
+2
-2
@@ -11,7 +11,7 @@ function drainEvents() {
|
|||||||
const m = MILESTONES.find(x => x.id === e.id);
|
const m = MILESTONES.find(x => x.id === e.id);
|
||||||
if (m && !['bit1', 'bit2', 'bit3', 'tank'].includes(m.id)) { showCard({ kind: 'Milestone', title: m.title, body: [m.msg, m.reward], accent: 'milestone' }); playSfx('milestone'); }
|
if (m && !['bit1', 'bit2', 'bit3', 'tank'].includes(m.id)) { showCard({ kind: 'Milestone', title: m.title, body: [m.msg, m.reward], accent: 'milestone' }); playSfx('milestone'); }
|
||||||
} else if (e.t === 'cast_ready') {
|
} else if (e.t === 'cast_ready') {
|
||||||
playSfx('cast');
|
playSfx('cast', undefined, e.x, e.y);
|
||||||
} else if (e.t === 'upgrade') {
|
} else if (e.t === 'upgrade') {
|
||||||
playSfx('upgrade'); if (SET.shake) R.shake = 1.4;
|
playSfx('upgrade'); if (SET.shake) R.shake = 1.4;
|
||||||
showCard({ kind: e.kind === 'bit' ? 'Drill bit upgraded' : 'Bag upgraded', title: e.kind === 'bit' ? BIT_TIERS[e.tier].name : TANK_TIERS[e.tier].name, accent: 'milestone',
|
showCard({ kind: e.kind === 'bit' ? 'Drill bit upgraded' : 'Bag upgraded', title: e.kind === 'bit' ? BIT_TIERS[e.tier].name : TANK_TIERS[e.tier].name, accent: 'milestone',
|
||||||
@@ -36,7 +36,7 @@ function drainEvents() {
|
|||||||
}
|
}
|
||||||
else if (e.t === 'claim_paid') { showCard({ kind: 'Claim', title: 'The land is yours', accent: 'milestone', hold: 1.5, body: ['You have paid off the claim on this land.', 'Keep filling orders: bigger buyers bring bigger ones. The ledger has bigger goals too.'] }); playSfx('milestone'); }
|
else if (e.t === 'claim_paid') { showCard({ kind: 'Claim', title: 'The land is yours', accent: 'milestone', hold: 1.5, body: ['You have paid off the claim on this land.', 'Keep filling orders: bigger buyers bring bigger ones. The ledger has bigger goals too.'] }); playSfx('milestone'); }
|
||||||
else if (e.t === 'sprout') { showCard({ kind: 'Discovery', title: 'A sapling', swatch: ACORN, body: ['An acorn on soil under open sky has sprouted.', 'It grows into a tree in about a minute and a half. Keep the space above it clear.'] }); playSfx('discover'); }
|
else if (e.t === 'sprout') { showCard({ kind: 'Discovery', title: 'A sapling', swatch: ACORN, body: ['An acorn on soil under open sky has sprouted.', 'It grows into a tree in about a minute and a half. Keep the space above it clear.'] }); playSfx('discover'); }
|
||||||
else if (e.t === 'sfx') { if (e.s) playSfx(e.s, e.a); }
|
else if (e.t === 'sfx') { if (e.s) playSfx(e.s, e.a, e.x, e.y); }
|
||||||
else if (e.t === 'toast') { if (e.text) toast(e.text); }
|
else if (e.t === 'toast') { if (e.text) toast(e.text); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,17 @@ 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.
|
||||||
|
ev("auInit(); window._noiseCalls = 0; window._noiseBurst = noiseBurst; noiseBurst = function (...args) { _noiseCalls++; return _noiseBurst(...args); }; 0")
|
||||||
|
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]; })()")
|
||||||
|
check('a sound far off-screen 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]; })()")
|
||||||
|
check('a sound on screen plays', near == [1, 1, True], near)
|
||||||
|
quiet = ev('sfxPlace(R.vx0 + R.bw * 1.5, R.vy0 + R.bh / 2)')
|
||||||
|
check('a sound just off-screen is quieter', 0 < quiet['gain'] < 1 and quiet['pan'] > 0, quiet)
|
||||||
|
finally:
|
||||||
|
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'
|
||||||
check('a new game starts fully zoomed in', ev('SET.zoom') == 12, ev('SET.zoom'))
|
check('a new game starts fully zoomed in', ev('SET.zoom') == 12, ev('SET.zoom'))
|
||||||
# Ctrl+W closes the tab; with a world open the browser must ask first
|
# Ctrl+W closes the tab; with a world open the browser must ask first
|
||||||
|
|||||||
Reference in New Issue
Block a user