diff --git a/src/core/course.js b/src/core/course.js index e7f32b7..d23860c 100644 --- a/src/core/course.js +++ b/src/core/course.js @@ -163,7 +163,7 @@ export function createCourse({ seed = 1, difficultyRun = DIFFICULTY_RUN } = {}) const roll = rng(); if (roll < 0.18) runway(); else if (roll < 0.42) slalom(d); - else if (roll < 0.62) chasm(d); + else if (roll < 0.62) chasm(); else if (roll < 0.78) catwalk(d); else if (roll < 0.9) stack(d); else gauntlet(d); diff --git a/src/main.js b/src/main.js index d05e712..7e480e1 100644 --- a/src/main.js +++ b/src/main.js @@ -74,7 +74,9 @@ mute.addEventListener('click', (event) => { setMuted(!sound.muted); }); addEventListener('keydown', (event) => { - if (event.code === 'KeyM') setMuted(!sound.muted); + /** A held key repeats, and a mute that flips thirty times a second is a + * mute nobody can aim. */ + if (event.code === 'KeyM' && !event.repeat) setMuted(!sound.muted); }); /** diff --git a/src/render/props.js b/src/render/props.js index 4b9fa9e..deb5bab 100644 --- a/src/render/props.js +++ b/src/render/props.js @@ -76,9 +76,12 @@ export function createHazard() { wire(THEME.agent, 0.55), ); core.position.y = HAZARD.height / 2; - core.name = 'core'; group.add(core); + /** Handed over rather than looked up: the renderer turns this every frame + * for every agent in shot, and a search of the group is a search. */ + group.userData.core = core; + return group; } diff --git a/src/render/view.js b/src/render/view.js index 2f265b6..a9e1f93 100644 --- a/src/render/view.js +++ b/src/render/view.js @@ -89,7 +89,7 @@ export function createView({ scene, camera, studio }) { if (hazard.z > player.z + DRAW.ahead) break; const mesh = agents.take(); mesh.position.set(hazard.x, hazard.y, hazard.z); - mesh.getObjectByName('core').rotation.set(time * 0.9, time * 1.4, 0); + mesh.userData.core.rotation.set(time * 0.9, time * 1.4, 0); } agents.end(); diff --git a/src/ui/hud.js b/src/ui/hud.js index 1284d5d..540afec 100644 --- a/src/ui/hud.js +++ b/src/ui/hud.js @@ -5,8 +5,14 @@ const LEAD = { over: 'Connection terminated. The shell was never the problem.', }; +/** HTML-safe: every figure on this panel is one of our own numbers, but a + * panel that writes unescaped strings is a habit worth not having. */ +function safe(text) { + return String(text).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); +} + function stat(label, value) { - return `
${label}
${value}
`; + return `
${safe(label)}
${safe(value)}
`; } function metres(value) { diff --git a/src/ui/sound.js b/src/ui/sound.js index 34ea7ce..a230e63 100644 --- a/src/ui/sound.js +++ b/src/ui/sound.js @@ -31,7 +31,9 @@ export function createSound() { if (!voice || muted) return; const audio = context(); if (!audio) return; - if (audio.state === 'suspended') audio.resume(); + /** A resume that is refused is a run with no sound in it, not an + * unhandled rejection in everybody's console. */ + if (audio.state === 'suspended') audio.resume()?.catch(() => {}); const now = audio.currentTime; const osc = audio.createOscillator(); @@ -44,6 +46,13 @@ export function createSound() { osc.connect(gain).connect(audio.destination); osc.start(now); osc.stop(now + voice.time + 0.02); + /** Off the graph once it has been heard. A run plays hundreds of + * these, and every one of them stays wired to the destination + * until it is let go of. */ + osc.onended = () => { + osc.disconnect(); + gain.disconnect(); + }; }, }; }