Lobby: dice sounds

Clatter per die and a result cue for checks, synthesized with the Web
Audio API; mute button and volume slider next to the dice.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HJhXuLD2Z6hv3Wqeq9UL9K
This commit is contained in:
Claude
2026-10-23 18:42:46 +00:00
parent 82e4ac9458
commit 2195ecce94
4 changed files with 99 additions and 0 deletions
+9
View File
@@ -1,5 +1,6 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import PaperSheet, { ThemeSwitch } from './PaperSheet.jsx'
import { sound, playRoll } from './sound.js'
import { MapView, MapLibrary, TokenForm, WallLayer } from './MapView.jsx'
const J = { 'Content-Type': 'application/json' }
@@ -303,6 +304,8 @@ function DieIcon({ sides }) {
const MODES = [['mystery', 'Mystery', 'Players see grey dice, but not the result'], ['hidden', 'Hidden', 'Only you see the roll'], ['public', 'Public', 'Everybody sees the roll']]
function DiceBar({ to, me, isDm, count, setCount, mode, setMode, own, setOwn, onRoll, busy }) {
const [snd, setSnd] = useState(sound.enabled)
const [vol, setVol] = useState(sound.volume)
return (
<div className="dicebar" role="toolbar" aria-label="Dice">
<label className="small muted dicecount">Dice
@@ -318,6 +321,10 @@ function DiceBar({ to, me, isDm, count, setCount, mode, setMode, own, setOwn, on
<label className="small ownswitch" title="You rolled real dice: type in the result instead of rolling here">
<input type="checkbox" checked={own} onChange={(e) => setOwn(e.target.checked)} style={{ width: 'auto' }} /> Own dice
</label>
<span className="soundctl small" title="Dice sounds">
<button className="ghost" aria-pressed={snd} aria-label={snd ? 'Mute dice sounds' : 'Turn dice sounds on'} onClick={() => { sound.setEnabled(!snd); setSnd(!snd); if (!snd) playRoll(1, 'success') }}>{snd ? '🔊' : '🔇'}</button>
{snd && <input type="range" min="0" max="1" step="0.05" value={vol} aria-label="Dice sound volume" onChange={(e) => { setVol(+e.target.value); sound.setVolume(+e.target.value) }} onPointerUp={() => playRoll(1)} />}
</span>
{isDm && !to && (
<span className="seg small" role="group" aria-label="Who sees the DM's rolls">
{MODES.map(([k, label, tip]) => <button key={k} className={mode === k ? 'on' : 'ghost'} title={tip} onClick={() => setMode(k)}>{label}</button>)}
@@ -599,6 +606,8 @@ function LobbyView({ id, rules, user }) {
const onRoll = useCallback((m) => {
const key = `${m.id}`
if (m.kind === 'check') playRoll(1, m.check.outcome)
else if (m.kind === 'roll') playRoll(m.roll.n)
setTray((t) => [...t.slice(-2), { ...m, key }])
setTimeout(() => setTray((t) => t.filter((x) => x.key !== key)), 20000)
}, [])