import { useState, useEffect, useRef, useCallback } from 'react' import styles from '../styles/game.module.css' const WORD_POOL = [ 'the', 'and', 'for', 'are', 'but', 'not', 'you', 'all', 'can', 'her', 'was', 'one', 'our', 'out', 'day', 'get', 'has', 'him', 'his', 'how', 'its', 'may', 'new', 'now', 'old', 'see', 'way', 'who', 'did', 'got', 'let', 'say', 'she', 'too', 'use', 'big', 'cat', 'dog', 'fun', 'hat', 'jump', 'kick', 'love', 'make', 'nice', 'open', 'play', 'quiz', 'run', 'star', 'time', 'up', 'very', 'walk', 'xray', 'year', 'zero', 'code', 'type', 'fast', 'hero', 'epic', 'cool', 'fire', 'bolt', 'zoom', 'dash', 'pixel', 'blaze', 'storm', 'power', 'quest', 'magic', 'super', 'turbo', 'cyber', 'ultra', 'royal', 'brave', 'swift', 'light', 'dream', 'shine', ] type FallingWord = { id: number text: string x: number y: number speed: number matched: number // how many chars matched } type Explosion = { id: number text: string x: number y: number points: number } type Particle = { id: number x: number y: number tx: number ty: number color: string duration: number } const PARTICLE_COLORS = ['#2ecc71', '#27ae60', '#7c6ff7', '#f1c40f', '#e84393', '#3498db', '#00d2d3'] function spawnParticles(x: number, y: number, count: number): Particle[] { return Array.from({ length: count }, (_, i) => { const angle = (Math.PI * 2 * i) / count + (Math.random() - 0.5) * 0.5 const dist = 40 + Math.random() * 80 return { id: Date.now() + i, x, y, tx: Math.cos(angle) * dist, ty: Math.sin(angle) * dist, color: PARTICLE_COLORS[Math.floor(Math.random() * PARTICLE_COLORS.length)], duration: 0.4 + Math.random() * 0.3, } }) } type Props = { highScore: number onGameOver: (score: number) => void } export function GameMode({ highScore, onGameOver }: Props) { const [words, setWords] = useState([]) const [input, setInput] = useState('') const inputRef = useRef('') const [score, setScore] = useState(0) const [lives, setLives] = useState(3) const [gameOver, setGameOver] = useState(false) const [started, setStarted] = useState(false) const [focused, setFocused] = useState(false) const [explosions, setExplosions] = useState([]) const [particles, setParticles] = useState([]) const [flash, setFlash] = useState(0) const [combo, setCombo] = useState(0) const [showCombo, setShowCombo] = useState(0) const comboTimerRef = useRef>(undefined) const containerRef = useRef(null) const nextIdRef = useRef(0) const frameRef = useRef(0) const lastSpawnRef = useRef(0) const difficultyRef = useRef(1) const livesRef = useRef(3) const gameOverRef = useRef(false) livesRef.current = lives gameOverRef.current = gameOver const spawnWord = useCallback(() => { const difficulty = difficultyRef.current const maxLen = Math.min(3 + Math.floor(difficulty / 3), 7) const pool = WORD_POOL.filter(w => w.length <= maxLen) const text = pool[Math.floor(Math.random() * pool.length)] const word: FallingWord = { id: nextIdRef.current++, text, x: Math.random() * 70 + 5, // 5%-75% from left y: -5, speed: 0.3 + difficulty * 0.08, matched: 0, } setWords(prev => [...prev, word]) }, []) const startGame = () => { setWords([]) updateInput('') setScore(0) setLives(3) setGameOver(false) setStarted(true) setExplosions([]) setParticles([]) setCombo(0) difficultyRef.current = 1 lastSpawnRef.current = 0 nextIdRef.current = 0 containerRef.current?.focus() } // Game loop useEffect(() => { if (!started || gameOver) return let lastTime = performance.now() const tick = (now: number) => { if (gameOverRef.current) return const dt = (now - lastTime) / 16.67 // normalize to ~60fps lastTime = now // Spawn const spawnInterval = Math.max(1500 - difficultyRef.current * 80, 600) if (now - lastSpawnRef.current > spawnInterval) { spawnWord() lastSpawnRef.current = now } // Move words setWords(prev => { const next: FallingWord[] = [] let lostLife = false for (const w of prev) { const ny = w.y + w.speed * dt if (ny > 100) { lostLife = true } else { next.push({ ...w, y: ny }) } } if (lostLife) { setLives(l => { const nl = l - 1 if (nl <= 0) { setGameOver(true) } return nl }) } return next }) // Increase difficulty difficultyRef.current = 1 + score / 50 frameRef.current = requestAnimationFrame(tick) } frameRef.current = requestAnimationFrame(tick) return () => cancelAnimationFrame(frameRef.current) }, [started, gameOver, spawnWord, score]) // Fire onGameOver useEffect(() => { if (gameOver && started) { onGameOver(score) } }, [gameOver, started, score, onGameOver]) const updateInput = (val: string) => { inputRef.current = val setInput(val) } const handleKeyDown = (e: React.KeyboardEvent) => { if (gameOver) return if (!started) { startGame() return } if (e.key === 'Backspace') { const newInput = inputRef.current.slice(0, -1) updateInput(newInput) setWords(prev => prev.map(w => ({ ...w, matched: w.text.startsWith(newInput) && newInput.length > 0 ? newInput.length : 0, }))) return } if (e.key.length !== 1) return e.preventDefault() const newInput = inputRef.current + e.key // Do matching + removal in one atomic setWords call setWords(prev => { const completed = prev.find(w => w.text === newInput) if (completed) { const points = completed.text.length * 10 setScore(s => s + points) updateInput('') // Spawn explosion at word position const now = Date.now() setExplosions(ex => [...ex, { id: now, text: completed.text, x: completed.x, y: completed.y, points, }]) // Spawn particles setParticles(p => [...p, ...spawnParticles(completed.x, completed.y, 12)]) // Screen flash setFlash(now) // Combo tracking setCombo(c => { const next = c + 1 if (next >= 3) setShowCombo(now) return next }) clearTimeout(comboTimerRef.current) comboTimerRef.current = setTimeout(() => setCombo(0), 2000) // Auto-clean effects after animation setTimeout(() => { setExplosions(ex => ex.filter(e => e.id !== now)) }, 500) setTimeout(() => { setParticles(p => p.slice(-24)) }, 800) return prev.filter(w => w.id !== completed.id) } // No completion — update partial matches updateInput(newInput) return prev.map(w => ({ ...w, matched: w.text.startsWith(newInput) ? newInput.length : 0, })) }) } return (

Falling Words

High Score: {highScore} {started && !gameOver && ( )}
setFocused(true)} onBlur={() => setFocused(false)} >
Score {score}
Lives {'❤️'.repeat(Math.max(0, lives))}{'🖤'.repeat(Math.max(0, 3 - lives))}
{words.map(w => (
0 ? styles.wordMatched : ''}`} style={{ left: `${w.x}%`, top: `${w.y}%` }} > {w.text.split('').map((ch, i) => ( {ch} ))}
))} {/* Exploding words */} {explosions.map(ex => (
{ex.text}
))} {/* Particles */} {particles.map(p => (
))} {/* Score popups */} {explosions.map(ex => (
+{ex.points}
))} {/* Screen flash */} {flash > 0 &&
} {/* Combo text */} {showCombo > 0 && combo >= 3 && (
{combo}x COMBO!
)}
{started && !gameOver && (
{input || type a word...}
)} {!started && (
Falling Words
Type the words before they fall!
)} {gameOver && (
Game Over!
Score: {score}
{score > highScore && score > 0 && (
New High Score!
)}
)}
) }