feat: chip sound effects
This commit is contained in:
@@ -3,6 +3,7 @@ import "./globals.css";
|
|||||||
import Celebration from "@/components/Celebration";
|
import Celebration from "@/components/Celebration";
|
||||||
import Nav from "@/components/Nav";
|
import Nav from "@/components/Nav";
|
||||||
import { ConnectionProvider } from "@/lib/connection";
|
import { ConnectionProvider } from "@/lib/connection";
|
||||||
|
import { CHIP_DROP_SOUND_PATHS } from "@/lib/sfx";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Connect4 Moderator",
|
title: "Connect4 Moderator",
|
||||||
@@ -16,6 +17,17 @@ export default function RootLayout({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
{CHIP_DROP_SOUND_PATHS.map((path) => (
|
||||||
|
<link
|
||||||
|
key={path}
|
||||||
|
rel="preload"
|
||||||
|
href={path}
|
||||||
|
as="audio"
|
||||||
|
type="audio/ogg"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</head>
|
||||||
<body className="min-h-screen bg-gray-950 text-gray-100">
|
<body className="min-h-screen bg-gray-950 text-gray-100">
|
||||||
<ConnectionProvider>
|
<ConnectionProvider>
|
||||||
<Celebration />
|
<Celebration />
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
import type { CSSProperties } from "react";
|
import type { CSSProperties } from "react";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import type { BoardState } from "@/lib/protocol";
|
import type { BoardState } from "@/lib/protocol";
|
||||||
|
import { CHIP_DROP_SOUND_PATHS } from "@/lib/sfx";
|
||||||
|
const CHIP_DROP_ANIMATION_MS = 450;
|
||||||
|
|
||||||
interface BoardProps {
|
interface BoardProps {
|
||||||
board: BoardState;
|
board: BoardState;
|
||||||
@@ -30,10 +32,27 @@ export default function Board({
|
|||||||
} | null>(null);
|
} | null>(null);
|
||||||
const isFirstRender = useRef(true);
|
const isFirstRender = useRef(true);
|
||||||
const animationTimeoutRef = useRef<number | null>(null);
|
const animationTimeoutRef = useRef<number | null>(null);
|
||||||
|
const chipDropSoundsRef = useRef<HTMLAudioElement[]>([]);
|
||||||
const canInteract = !disabled && !!onColumnClick;
|
const canInteract = !disabled && !!onColumnClick;
|
||||||
const lastMoveColumn = lastMove?.column ?? null;
|
const lastMoveColumn = lastMove?.column ?? null;
|
||||||
const lastMoveRow = lastMove?.row ?? 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(() => {
|
useEffect(() => {
|
||||||
if (isFirstRender.current) {
|
if (isFirstRender.current) {
|
||||||
isFirstRender.current = false;
|
isFirstRender.current = false;
|
||||||
@@ -49,11 +68,26 @@ export default function Board({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const prefersReducedMotion = window.matchMedia(
|
||||||
|
"(prefers-reduced-motion: reduce)",
|
||||||
|
).matches;
|
||||||
|
const dropDurationMs = prefersReducedMotion ? 0 : CHIP_DROP_ANIMATION_MS;
|
||||||
|
|
||||||
setAnimatingMove({ column: lastMoveColumn, row: lastMoveRow });
|
setAnimatingMove({ column: lastMoveColumn, row: lastMoveRow });
|
||||||
animationTimeoutRef.current = window.setTimeout(() => {
|
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);
|
setAnimatingMove(null);
|
||||||
animationTimeoutRef.current = null;
|
animationTimeoutRef.current = null;
|
||||||
}, 450);
|
}, dropDurationMs);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (animationTimeoutRef.current !== null) {
|
if (animationTimeoutRef.current !== null) {
|
||||||
@@ -192,7 +226,8 @@ export default function Board({
|
|||||||
|
|
||||||
<style jsx>{`
|
<style jsx>{`
|
||||||
.chip-drop {
|
.chip-drop {
|
||||||
animation: chip-drop 450ms cubic-bezier(0.22, 1, 0.36, 1) forwards;
|
animation: chip-drop ${CHIP_DROP_ANIMATION_MS}ms cubic-bezier(0.22, 1, 0.36, 1)
|
||||||
|
forwards;
|
||||||
will-change: transform;
|
will-change: transform;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export const CHIP_DROP_SOUND_PATHS = Array.from(
|
||||||
|
{ length: 7 },
|
||||||
|
(_, index) => `/sfx/chip_collide_${index + 1}.ogg`,
|
||||||
|
);
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user