diff --git a/src/components/GameMode.tsx b/src/components/GameMode.tsx index 9652789..0f6a950 100644 --- a/src/components/GameMode.tsx +++ b/src/components/GameMode.tsx @@ -1,5 +1,6 @@ import { useState, useEffect, useRef, useCallback } from 'react' import styles from '../styles/game.module.css' +import { playKeyClick, playWordComplete, playCombo, playMiss, playGameOver } from '../sounds' const WORD_POOL = [ 'the', 'and', 'for', 'are', 'but', 'not', 'you', 'all', 'can', 'her', @@ -152,6 +153,7 @@ export function GameMode({ highScore, onGameOver }: Props) { } } if (lostLife) { + playMiss() setLives(l => { const nl = l - 1 if (nl <= 0) { @@ -176,6 +178,7 @@ export function GameMode({ highScore, onGameOver }: Props) { // Fire onGameOver useEffect(() => { if (gameOver && started) { + playGameOver() onGameOver(score) } }, [gameOver, started, score, onGameOver]) @@ -204,6 +207,7 @@ export function GameMode({ highScore, onGameOver }: Props) { if (e.key.length !== 1) return e.preventDefault() + playKeyClick() const newInput = inputRef.current + e.key @@ -214,6 +218,7 @@ export function GameMode({ highScore, onGameOver }: Props) { const points = completed.text.length * 10 setScore(s => s + points) updateInput('') + playWordComplete() // Spawn explosion at word position const now = Date.now() @@ -231,7 +236,10 @@ export function GameMode({ highScore, onGameOver }: Props) { // Combo tracking setCombo(c => { const next = c + 1 - if (next >= 3) setShowCombo(now) + if (next >= 3) { + setShowCombo(now) + playCombo() + } return next }) clearTimeout(comboTimerRef.current) diff --git a/src/sounds.ts b/src/sounds.ts new file mode 100644 index 0000000..b9fabc0 --- /dev/null +++ b/src/sounds.ts @@ -0,0 +1,126 @@ +// Synthesized game sounds using Web Audio API — no audio files needed + +let ctx: AudioContext | null = null + +function getCtx(): AudioContext { + if (!ctx) ctx = new AudioContext() + if (ctx.state === 'suspended') ctx.resume() + return ctx +} + +export function playKeyClick() { + const c = getCtx() + const osc = c.createOscillator() + const gain = c.createGain() + osc.type = 'square' + osc.frequency.setValueAtTime(800, c.currentTime) + osc.frequency.exponentialRampToValueAtTime(600, c.currentTime + 0.04) + gain.gain.setValueAtTime(0.06, c.currentTime) + gain.gain.exponentialRampToValueAtTime(0.001, c.currentTime + 0.04) + osc.connect(gain).connect(c.destination) + osc.start() + osc.stop(c.currentTime + 0.04) +} + +export function playWordComplete() { + const c = getCtx() + const t = c.currentTime + + // Bright rising arpeggio + const notes = [523, 659, 784, 1047] // C5 E5 G5 C6 + notes.forEach((freq, i) => { + const osc = c.createOscillator() + const gain = c.createGain() + osc.type = 'sine' + osc.frequency.setValueAtTime(freq, t + i * 0.06) + gain.gain.setValueAtTime(0, t) + gain.gain.linearRampToValueAtTime(0.15, t + i * 0.06) + gain.gain.exponentialRampToValueAtTime(0.001, t + i * 0.06 + 0.2) + osc.connect(gain).connect(c.destination) + osc.start(t + i * 0.06) + osc.stop(t + i * 0.06 + 0.2) + }) +} + +export function playCombo() { + const c = getCtx() + const t = c.currentTime + + // Power-up sweep + const osc = c.createOscillator() + const gain = c.createGain() + osc.type = 'sawtooth' + osc.frequency.setValueAtTime(300, t) + osc.frequency.exponentialRampToValueAtTime(1200, t + 0.15) + osc.frequency.exponentialRampToValueAtTime(800, t + 0.3) + gain.gain.setValueAtTime(0.1, t) + gain.gain.linearRampToValueAtTime(0.15, t + 0.1) + gain.gain.exponentialRampToValueAtTime(0.001, t + 0.35) + osc.connect(gain).connect(c.destination) + osc.start(t) + osc.stop(t + 0.35) + + // Sparkle overtone + const osc2 = c.createOscillator() + const gain2 = c.createGain() + osc2.type = 'sine' + osc2.frequency.setValueAtTime(1600, t + 0.05) + osc2.frequency.exponentialRampToValueAtTime(2400, t + 0.2) + gain2.gain.setValueAtTime(0.08, t + 0.05) + gain2.gain.exponentialRampToValueAtTime(0.001, t + 0.35) + osc2.connect(gain2).connect(c.destination) + osc2.start(t + 0.05) + osc2.stop(t + 0.35) +} + +export function playMiss() { + const c = getCtx() + const t = c.currentTime + + // Low thud + const osc = c.createOscillator() + const gain = c.createGain() + osc.type = 'sine' + osc.frequency.setValueAtTime(150, t) + osc.frequency.exponentialRampToValueAtTime(60, t + 0.2) + gain.gain.setValueAtTime(0.2, t) + gain.gain.exponentialRampToValueAtTime(0.001, t + 0.25) + osc.connect(gain).connect(c.destination) + osc.start(t) + osc.stop(t + 0.25) + + // Noise burst + const bufferSize = c.sampleRate * 0.1 + const buffer = c.createBuffer(1, bufferSize, c.sampleRate) + const data = buffer.getChannelData(0) + for (let i = 0; i < bufferSize; i++) { + data[i] = (Math.random() * 2 - 1) * (1 - i / bufferSize) + } + const noise = c.createBufferSource() + const noiseGain = c.createGain() + noise.buffer = buffer + noiseGain.gain.setValueAtTime(0.08, t) + noiseGain.gain.exponentialRampToValueAtTime(0.001, t + 0.1) + noise.connect(noiseGain).connect(c.destination) + noise.start(t) +} + +export function playGameOver() { + const c = getCtx() + const t = c.currentTime + + // Descending minor arpeggio + const notes = [440, 349, 294, 220] // A4 F4 D4 A3 + notes.forEach((freq, i) => { + const osc = c.createOscillator() + const gain = c.createGain() + osc.type = 'triangle' + osc.frequency.setValueAtTime(freq, t + i * 0.15) + gain.gain.setValueAtTime(0, t) + gain.gain.linearRampToValueAtTime(0.15, t + i * 0.15) + gain.gain.exponentialRampToValueAtTime(0.001, t + i * 0.15 + 0.4) + osc.connect(gain).connect(c.destination) + osc.start(t + i * 0.15) + osc.stop(t + i * 0.15 + 0.4) + }) +}