Music mute everywhere + card-draw SFX
- MusicToggle: global floating button (enable/disable music from any screen; hidden on the table, which has its own audio control in its HUD). Uses sound-store toggleMusic. - Card sounds now use a synthesized card-draw "swish" (filtered noise burst with a downward sweep) for cardPlay (+ soft landing tap) and deal (a flurry), replacing the old beep tones. Verified: tsc + next build pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,7 @@ import { DailyRewardModal } from "@/components/online/DailyRewardModal";
|
||||
import { NotificationToaster } from "@/components/online/NotificationToaster";
|
||||
import { ResumeGameBar } from "@/components/online/ResumeGameBar";
|
||||
import { CelebrationOverlay } from "@/components/online/CelebrationOverlay";
|
||||
import { MusicToggle } from "@/components/online/MusicToggle";
|
||||
import { PublicProfileModal } from "@/components/online/PublicProfileModal";
|
||||
import { CapacitorBack } from "@/components/CapacitorBack";
|
||||
import { useSessionStore } from "@/lib/session-store";
|
||||
@@ -171,6 +172,7 @@ export default function Page() {
|
||||
<NotificationToaster />
|
||||
<ResumeGameBar />
|
||||
<CelebrationOverlay />
|
||||
<MusicToggle />
|
||||
<PublicProfileModal />
|
||||
<CapacitorBack />
|
||||
{loading && null}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
"use client";
|
||||
|
||||
import { Music } from "lucide-react";
|
||||
import { useSoundStore } from "@/lib/sound-store";
|
||||
import { useUIStore } from "@/lib/ui-store";
|
||||
import { useI18n } from "@/lib/i18n";
|
||||
|
||||
/**
|
||||
* Always-available music mute toggle (enable/disable from anywhere). Floats in a
|
||||
* corner on every screen except the game table, which has its own audio control
|
||||
* in its HUD.
|
||||
*/
|
||||
export function MusicToggle() {
|
||||
const { t } = useI18n();
|
||||
const music = useSoundStore((s) => s.music);
|
||||
const toggleMusic = useSoundStore((s) => s.toggleMusic);
|
||||
const screen = useUIStore((s) => s.screen);
|
||||
|
||||
if (screen === "game") return null;
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={toggleMusic}
|
||||
title={music ? t("settings.music") : t("settings.music")}
|
||||
aria-label={t("settings.music")}
|
||||
className="fixed z-[55] bottom-[max(0.75rem,env(safe-area-inset-bottom))] ltr:left-3 rtl:right-3 glass rounded-full min-h-11 min-w-11 grid place-items-center hover:bg-navy-800/80 transition"
|
||||
>
|
||||
{music ? (
|
||||
<Music className="size-4 text-gold-400" />
|
||||
) : (
|
||||
<span className="relative grid place-items-center">
|
||||
<Music className="size-4 text-cream/40" />
|
||||
<span className="absolute block h-0.5 w-5 rotate-45 rounded bg-rose-400" />
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
+31
-2
@@ -115,6 +115,32 @@ class SoundManager {
|
||||
notes.forEach(([freq, dur], i) => this.tone(freq, t0 + i * gap, dur, opts));
|
||||
}
|
||||
|
||||
/** Filtered noise burst with a downward sweep — a card "swish" / draw sound. */
|
||||
private swish(start: number, dur = 0.13, opts: { gain?: number; from?: number; to?: number } = {}) {
|
||||
if (!this.ctx || !this.master) return;
|
||||
const ctx = this.ctx;
|
||||
const buf = ctx.createBuffer(1, Math.ceil(ctx.sampleRate * dur), ctx.sampleRate);
|
||||
const data = buf.getChannelData(0);
|
||||
for (let i = 0; i < data.length; i++) data[i] = Math.random() * 2 - 1;
|
||||
const src = ctx.createBufferSource();
|
||||
src.buffer = buf;
|
||||
const bp = ctx.createBiquadFilter();
|
||||
bp.type = "bandpass";
|
||||
bp.Q.value = 0.9;
|
||||
bp.frequency.setValueAtTime(opts.from ?? 3200, start);
|
||||
bp.frequency.exponentialRampToValueAtTime(opts.to ?? 900, start + dur);
|
||||
const g = ctx.createGain();
|
||||
const peak = opts.gain ?? 0.28;
|
||||
g.gain.setValueAtTime(0.0001, start);
|
||||
g.gain.exponentialRampToValueAtTime(peak, start + 0.008);
|
||||
g.gain.exponentialRampToValueAtTime(0.0001, start + dur);
|
||||
src.connect(bp);
|
||||
bp.connect(g);
|
||||
g.connect(this.master);
|
||||
src.start(start);
|
||||
src.stop(start + dur + 0.02);
|
||||
}
|
||||
|
||||
play(name: Sfx) {
|
||||
if (!this.sfxEnabled) return;
|
||||
this.init();
|
||||
@@ -125,11 +151,14 @@ class SoundManager {
|
||||
this.tone(520, t, 0.06, { type: "square", gain: 0.12 });
|
||||
break;
|
||||
case "cardPlay":
|
||||
this.tone(360, t, 0.09, { type: "triangle", gain: 0.18, to: 220 });
|
||||
// Draw-card swish + a soft low tap as it lands on the felt.
|
||||
this.swish(t, 0.13, { gain: 0.3, from: 3200, to: 800 });
|
||||
this.tone(150, t + 0.08, 0.06, { type: "sine", gain: 0.12, to: 90 });
|
||||
break;
|
||||
case "deal":
|
||||
// A flurry of card-draw swishes (dealing).
|
||||
for (let i = 0; i < 4; i++)
|
||||
this.tone(320 + i * 20, t + i * 0.08, 0.07, { type: "triangle", gain: 0.14, to: 200 });
|
||||
this.swish(t + i * 0.1, 0.1, { gain: 0.22, from: 3400, to: 1000 });
|
||||
break;
|
||||
case "trump":
|
||||
this.seq([[440, 0.12], [660, 0.18]], 0.1, { type: "sine", gain: 0.25 });
|
||||
|
||||
Reference in New Issue
Block a user