From ede1d6c74dcb058b8f609d1c4e81c7a8006b50c6 Mon Sep 17 00:00:00 2001 From: Valentin ROBIN Date: Tue, 25 Aug 2026 17:42:04 +0200 Subject: [PATCH] Ajoute un son de fusion dans La Corne d'Abondance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pas de fichier audio dans le projet, donc un petit "pop" synthétisé via Web Audio (src/lib/melon/sound.ts), joué à chaque fusion. La hauteur baisse avec le palier de la pièce obtenue, pour que fusionner deux grosses pièces sonne différemment de deux petites. Co-Authored-By: Claude Sonnet 5 --- src/app/corne/melon-game.tsx | 3 +++ src/lib/melon/sound.ts | 50 ++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 src/lib/melon/sound.ts diff --git a/src/app/corne/melon-game.tsx b/src/app/corne/melon-game.tsx index c651384..bbfcfe7 100644 --- a/src/app/corne/melon-game.tsx +++ b/src/app/corne/melon-game.tsx @@ -16,6 +16,7 @@ import { MAX_FRAME_DT_MS, WALL_THICKNESS, } from "@/lib/melon/constants"; +import { playMergeSound } from "@/lib/melon/sound"; import type { MemberRow } from "./page"; type Status = "playing" | "dead"; @@ -329,10 +330,12 @@ export function MelonGame({ const newTier = merge.tier + 1; scoreRef.current += newTier; spawned.push(spawnPiece(newTier, merge.x, merge.y)); + playMergeSound(newTier, maxTier); } else { // Palier maximum déjà atteint : pas de pièce plus grande, juste // un bonus de score pour la fusion finale. scoreRef.current += maxTier * 2; + playMergeSound(maxTier, maxTier); } } diff --git a/src/lib/melon/sound.ts b/src/lib/melon/sound.ts new file mode 100644 index 0000000..43bd7f1 --- /dev/null +++ b/src/lib/melon/sound.ts @@ -0,0 +1,50 @@ +// Petit "pop" synthétisé via Web Audio (pas de fichier audio dans le +// projet, cohérent avec le reste de l'app qui dessine tout plutôt que de +// dépendre d'assets externes — voir les ornements SVG/canvas ailleurs). +// AudioContext créé paresseusement au premier appel : le construire avant +// une interaction utilisateur déclenche des avertissements navigateur +// (autoplay policy) — ici le premier merge n'arrive jamais avant un +// premier lâcher, donc toujours après un vrai geste utilisateur. +let audioContext: AudioContext | null = null; + +function getAudioContext(): AudioContext | null { + if (typeof window === "undefined") return null; + if (!audioContext) { + const Ctor = + window.AudioContext ?? + (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext; + if (!Ctor) return null; + audioContext = new Ctor(); + } + if (audioContext.state === "suspended") { + audioContext.resume().catch(() => {}); + } + return audioContext; +} + +// Fréquence plus grave à mesure que le palier de la pièce issue de la +// fusion monte : deux petites pièces qui fusionnent ne doit pas "sonner" +// comme deux grosses. +export function playMergeSound(tier: number, maxTier: number): void { + const ctx = getAudioContext(); + if (!ctx) return; + + const progress = maxTier > 0 ? Math.min(1, Math.max(0, tier / maxTier)) : 0; + const baseFreq = 720 - progress * 380; + const now = ctx.currentTime; + + const oscillator = ctx.createOscillator(); + const gain = ctx.createGain(); + oscillator.type = "sine"; + oscillator.frequency.setValueAtTime(baseFreq, now); + oscillator.frequency.exponentialRampToValueAtTime(baseFreq * 1.6, now + 0.08); + + gain.gain.setValueAtTime(0.0001, now); + gain.gain.exponentialRampToValueAtTime(0.25, now + 0.01); + gain.gain.exponentialRampToValueAtTime(0.0001, now + 0.18); + + oscillator.connect(gain); + gain.connect(ctx.destination); + oscillator.start(now); + oscillator.stop(now + 0.2); +}