From 1f7a482e75f36c0cca7d86b454822dd2dfff061a Mon Sep 17 00:00:00 2001 From: Joshua Higgins Date: Wed, 15 Apr 2026 14:37:42 -0400 Subject: [PATCH] feat: chip sound effects --- connect4-ui/app/layout.tsx | 12 +++++++ connect4-ui/components/Board.tsx | 39 ++++++++++++++++++++-- connect4-ui/lib/sfx.ts | 4 +++ connect4-ui/public/sfx/chip_collide_1.ogg | Bin 0 -> 15444 bytes connect4-ui/public/sfx/chip_collide_2.ogg | Bin 0 -> 18651 bytes connect4-ui/public/sfx/chip_collide_3.ogg | Bin 0 -> 18065 bytes connect4-ui/public/sfx/chip_collide_4.ogg | Bin 0 -> 10949 bytes connect4-ui/public/sfx/chip_collide_5.ogg | Bin 0 -> 10212 bytes connect4-ui/public/sfx/chip_collide_6.ogg | Bin 0 -> 16447 bytes connect4-ui/public/sfx/chip_collide_7.ogg | Bin 0 -> 20301 bytes 10 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 connect4-ui/lib/sfx.ts create mode 100644 connect4-ui/public/sfx/chip_collide_1.ogg create mode 100644 connect4-ui/public/sfx/chip_collide_2.ogg create mode 100644 connect4-ui/public/sfx/chip_collide_3.ogg create mode 100644 connect4-ui/public/sfx/chip_collide_4.ogg create mode 100644 connect4-ui/public/sfx/chip_collide_5.ogg create mode 100644 connect4-ui/public/sfx/chip_collide_6.ogg create mode 100644 connect4-ui/public/sfx/chip_collide_7.ogg 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({