diff --git a/connect4-ui/app/layout.tsx b/connect4-ui/app/layout.tsx index 5a0de8d..1db4ada 100644 --- a/connect4-ui/app/layout.tsx +++ b/connect4-ui/app/layout.tsx @@ -3,6 +3,7 @@ import "./globals.css"; import Celebration from "@/components/Celebration"; import Nav from "@/components/Nav"; import { ConnectionProvider } from "@/lib/connection"; +import { CHIP_DROP_SOUND_PATHS } from "@/lib/sfx"; export const metadata: Metadata = { title: "Connect4 Moderator", @@ -16,6 +17,17 @@ export default function RootLayout({ }) { return ( + + {CHIP_DROP_SOUND_PATHS.map((path) => ( + + ))} + diff --git a/connect4-ui/components/Board.tsx b/connect4-ui/components/Board.tsx index 9ae69a3..678bd2d 100644 --- a/connect4-ui/components/Board.tsx +++ b/connect4-ui/components/Board.tsx @@ -3,6 +3,8 @@ import type { CSSProperties } from "react"; import { useEffect, useRef, useState } from "react"; import type { BoardState } from "@/lib/protocol"; +import { CHIP_DROP_SOUND_PATHS } from "@/lib/sfx"; +const CHIP_DROP_ANIMATION_MS = 450; interface BoardProps { board: BoardState; @@ -30,10 +32,27 @@ export default function Board({ } | null>(null); const isFirstRender = useRef(true); const animationTimeoutRef = useRef(null); + const chipDropSoundsRef = useRef([]); const canInteract = !disabled && !!onColumnClick; const lastMoveColumn = lastMove?.column ?? null; const lastMoveRow = lastMove?.row ?? null; + useEffect(() => { + chipDropSoundsRef.current = CHIP_DROP_SOUND_PATHS.map((path) => { + const sound = new Audio(path); + sound.preload = "auto"; + return sound; + }); + + return () => { + chipDropSoundsRef.current.forEach((sound) => { + sound.pause(); + sound.src = ""; + }); + chipDropSoundsRef.current = []; + }; + }, []); + useEffect(() => { if (isFirstRender.current) { isFirstRender.current = false; @@ -49,11 +68,26 @@ export default function Board({ return; } + const prefersReducedMotion = window.matchMedia( + "(prefers-reduced-motion: reduce)", + ).matches; + const dropDurationMs = prefersReducedMotion ? 0 : CHIP_DROP_ANIMATION_MS; + setAnimatingMove({ column: lastMoveColumn, row: lastMoveRow }); animationTimeoutRef.current = window.setTimeout(() => { + const sounds = chipDropSoundsRef.current; + const sound = sounds[Math.floor(Math.random() * sounds.length)]; + + if (sound) { + sound.currentTime = 0; + void sound.play().catch(() => { + // Ignore autoplay failures until the user has interacted. + }); + } + setAnimatingMove(null); animationTimeoutRef.current = null; - }, 450); + }, dropDurationMs); return () => { if (animationTimeoutRef.current !== null) { @@ -192,7 +226,8 @@ export default function Board({