diff --git a/frontend/public/sounds/check-box-se.mp3 b/frontend/public/sounds/check-box-se.mp3 new file mode 100644 index 0000000..4f921b6 Binary files /dev/null and b/frontend/public/sounds/check-box-se.mp3 differ diff --git a/frontend/public/sounds/game2-bgm.mp3 b/frontend/public/sounds/game2-bgm.mp3 new file mode 100644 index 0000000..15f57f5 Binary files /dev/null and b/frontend/public/sounds/game2-bgm.mp3 differ diff --git a/frontend/public/sounds/game3-bgm.mp3 b/frontend/public/sounds/game3-bgm.mp3 new file mode 100644 index 0000000..6ab2ef2 Binary files /dev/null and b/frontend/public/sounds/game3-bgm.mp3 differ diff --git a/frontend/public/sounds/general-button-se.mp3 b/frontend/public/sounds/general-button-se.mp3 new file mode 100644 index 0000000..f064c97 Binary files /dev/null and b/frontend/public/sounds/general-button-se.mp3 differ diff --git a/frontend/public/sounds/result-bgm.mp3 b/frontend/public/sounds/result-bgm.mp3 new file mode 100644 index 0000000..bfb3779 Binary files /dev/null and b/frontend/public/sounds/result-bgm.mp3 differ diff --git a/frontend/public/sounds/start-bgm.mp3 b/frontend/public/sounds/start-bgm.mp3 new file mode 100644 index 0000000..01527d1 Binary files /dev/null and b/frontend/public/sounds/start-bgm.mp3 differ diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index daba27e..acc370a 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -2,10 +2,10 @@ import Image from 'next/image'; import { useRouter } from 'next/navigation'; -import { useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import type { CSSProperties } from 'react'; -//まる爆発アニメーションコンポーネント +// --- まる爆発アニメーションコンポーネント --- const SparklesExplosion = () => { const count = 20; const colors = ['#e9eb7c', '#ee7ee6', '#6eb8ca', '#e17a78', '#91ec77']; @@ -56,16 +56,51 @@ const SparklesExplosion = () => { ); }; +// --- メインのトップページコンポーネント --- export default function TopPage() { const router = useRouter(); const [showExplosion, setShowExplosion] = useState(false); + // BGMを保持するための Ref + const bgmRef = useRef(null); + + // BGMの初期化と再生管理 + useEffect(() => { + // パスは public/sounds/start-bgm.mp3 を想定 + const bgm = new Audio('/sounds/start-bgm.mp3'); + bgm.loop = true; + bgm.volume = 0.4; + bgmRef.current = bgm; + + const playBGM = () => { + bgm.play().catch(() => { + // 自動再生制限がかかった場合は何もしない + }); + // 一度クリックされたらイベントリスナーを削除 + window.removeEventListener('click', playBGM); + }; + + window.addEventListener('click', playBGM); + + // クリーンアップ + return () => { + bgm.pause(); + window.removeEventListener('click', playBGM); + }; + }, []); + const handleStartClick = () => { setShowExplosion(true); + // SEの再生(パスを修正) const audio = new Audio('/sounds/start-se.mp3'); audio.play().catch(() => {}); + // ボタン押下時にBGMを停止 + if (bgmRef.current) { + bgmRef.current.pause(); + } + setTimeout(() => { router.push('/games/terms'); }, 800); @@ -76,7 +111,18 @@ export default function TopPage() { }; return ( -
+
(null); + + const playSE = useCallback((path: string) => { + const audio = new Audio(path); + audio.volume = 0.5; + audio.play().catch(() => {}); + }, []); + + // BGMの初期化と再生管理 + useEffect(() => { + const bgm = new Audio('/sounds/start-bgm.mp3'); + bgm.loop = true; + bgm.volume = 0.4; + bgmRef.current = bgm; + + const playBGM = () => { + bgm.play().catch(() => {}); + window.removeEventListener('click', playBGM); + }; + + window.addEventListener('click', playBGM); + playBGM(); + + return () => { + bgm.pause(); + window.removeEventListener('click', playBGM); + }; + }, []); + const [currentIndex, setCurrentIndex] = useState(0); const [answers, setAnswers] = useState< Partial> @@ -66,6 +95,7 @@ export default function BaselineSurvey() { const handleAnswer = (value: AnswerOption) => { const newAnswers = { ...answers, [currentQuestion.key]: value }; setAnswers(newAnswers); + playSE('/sounds/general-button-se.mp3'); if (currentIndex < totalQuestions - 1) { setCurrentIndex((prev) => prev + 1); @@ -75,6 +105,7 @@ export default function BaselineSurvey() { }; const handleRetry = () => { + playSE('/sounds/general-button-se.mp3'); submitToApi(answers as BaselineAnswers); }; diff --git a/frontend/src/features/diagnosis/components/MbtiSelect.tsx b/frontend/src/features/diagnosis/components/MbtiSelect.tsx index ea3b42e..cfcb406 100644 --- a/frontend/src/features/diagnosis/components/MbtiSelect.tsx +++ b/frontend/src/features/diagnosis/components/MbtiSelect.tsx @@ -1,7 +1,7 @@ 'use client'; import Image from 'next/image'; -import { useState, useCallback } from 'react'; +import { useState, useCallback, useEffect, useRef } from 'react'; import { flushSync } from 'react-dom'; import { motion, AnimatePresence } from 'framer-motion'; import { useSetAtom } from 'jotai'; @@ -39,6 +39,36 @@ export default function MbtiSelect() { const [transitionVia, setTransitionVia] = useState< 'tab' | 'arrow-left' | 'arrow-right' >('tab'); + const bgmRef = useRef(null); + + const playSE = useCallback((path: string) => { + const audio = new Audio(path); + audio.volume = 0.5; + audio.play().catch(() => {}); + }, []); + + // BGMの初期化と再生管理 + useEffect(() => { + // TopPageと同じ start-bgm を使用 + const bgm = new Audio('/sounds/start-bgm.mp3'); + bgm.loop = true; + bgm.volume = 0.4; + bgmRef.current = bgm; + + const playBGM = () => { + bgm.play().catch(() => {}); + window.removeEventListener('click', playBGM); + }; + + window.addEventListener('click', playBGM); + // すでに他のページでインタラクションがあれば即再生される + playBGM(); + + return () => { + bgm.pause(); + window.removeEventListener('click', playBGM); + }; + }, []); const currentGroup = MBTI_GROUPS[groupIndex]; const currentTypes = getTypesByGroup(currentGroup); @@ -54,33 +84,39 @@ export default function MbtiSelect() { flushSync(() => setTransitionVia('tab')); setGroupIndex(index); setSelected(null); + playSE('/sounds/general-button-se.mp3'); }, - [groupIndex] + [groupIndex, playSE] ); const handleArrowPrev = useCallback(() => { setTransitionVia('arrow-right'); // コンテンツは右から入る setGroupIndex((i) => (i - 1 + MBTI_GROUPS.length) % MBTI_GROUPS.length); setSelected(null); - }, []); + playSE('/sounds/general-button-se.mp3'); + }, [playSE]); const handleArrowNext = useCallback(() => { setTransitionVia('arrow-left'); // コンテンツは左から入る setGroupIndex((i) => (i + 1) % MBTI_GROUPS.length); setSelected(null); - }, []); + playSE('/sounds/general-button-se.mp3'); + }, [playSE]); const handleConfirm = () => { + playSE('/sounds/general-button-se.mp3'); if (!selected) return; setMbti(selected); setStep('quiz'); }; const handleReselect = () => { + playSE('/sounds/general-button-se.mp3'); setSelected(null); }; const handleSkip = () => { + playSE('/sounds/general-button-se.mp3'); setMbti(null); setStep('quiz'); }; @@ -184,7 +220,10 @@ export default function MbtiSelect() { setSelected(type.code)} + onClick={() => { + setSelected(type.code); + playSE('/sounds/general-button-se.mp3'); + }} className="relative z-0 flex flex-1 basis-0 flex-col cursor-pointer items-center justify-center overflow-hidden rounded-4xl border-2 border-gray-800 bg-white/80 outline-none ring-0 hover:z-50" whileHover={{ scale: 1.2, diff --git a/frontend/src/features/games/group-chat/components/GroupChatGameFlow.tsx b/frontend/src/features/games/group-chat/components/GroupChatGameFlow.tsx index 1f94864..4b747ac 100644 --- a/frontend/src/features/games/group-chat/components/GroupChatGameFlow.tsx +++ b/frontend/src/features/games/group-chat/components/GroupChatGameFlow.tsx @@ -24,6 +24,36 @@ export default function GroupChatGameFlow() { const chatEndRef = useRef(null); const [submitStatus, setSubmitStatus] = useState('loading'); const pendingDataRef = useRef(null); + const bgmRef = useRef(null); + + const playSE = useCallback((path: string) => { + const audio = new Audio(path); + audio.volume = 0.5; + audio.play().catch(() => {}); + }, []); + + // BGMの初期化と再生管理 + useEffect(() => { + // 指示はgame2でしたが、Game3の画面のためgame3-bgm.mp3を適用します + const bgm = new Audio('/sounds/game3-bgm.mp3'); + bgm.loop = true; + bgm.volume = 0.3; + bgmRef.current = bgm; + + const playBGM = () => { + bgm.play().catch(() => {}); + window.removeEventListener('click', playBGM); + }; + + window.addEventListener('click', playBGM); + // 前の画面から継続している場合は即再生 + playBGM(); + + return () => { + bgm.pause(); + window.removeEventListener('click', playBGM); + }; + }, []); const submitGame3 = useCallback(async (data: Game3Data) => { const userId = localStorage.getItem('user_id'); @@ -43,6 +73,7 @@ export default function GroupChatGameFlow() { try { await submitGame3(data); setSubmitStatus('success'); + bgmRef.current?.pause(); setTimeout(() => { router.push('/result'); }, 2000); @@ -54,19 +85,21 @@ export default function GroupChatGameFlow() { ); const handleRetry = useCallback(async () => { + playSE('/sounds/general-button-se.mp3'); // リトライ音 const data = pendingDataRef.current; if (!data) return; setSubmitStatus('loading'); try { await submitGame3(data); setSubmitStatus('success'); + bgmRef.current?.pause(); setTimeout(() => { router.push('/result'); }, 2000); } catch { setSubmitStatus('error'); } - }, [router, submitGame3]); + }, [router, submitGame3, playSE]); const { gamePhase, @@ -76,14 +109,28 @@ export default function GroupChatGameFlow() { remainingTimeMs, isTypingIndicatorVisible, typingBotName, - startGame, - selectOption, + startGame: originalStartGame, // 名前を変更してラップ + selectOption: originalSelectOption, // 名前を変更してラップ handleOptionHover, stageTimeLimitMs, groupName, groupMemberCount, } = useGroupChatGame({ onComplete: handleComplete }); + // サウンドを鳴らすようにラップ + const startGame = useCallback(() => { + playSE('/sounds/general-button-se.mp3'); + originalStartGame(); + }, [originalStartGame, playSE]); + + const selectOption = useCallback( + (optionId: number) => { + playSE('/sounds/general-button-se.mp3'); + originalSelectOption(optionId); + }, + [originalSelectOption, playSE] + ); + useEffect(() => { chatEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [chatMessages, isTypingIndicatorVisible, gamePhase]); diff --git a/frontend/src/features/games/helpdesk/components/HelpdeskGameFlow.tsx b/frontend/src/features/games/helpdesk/components/HelpdeskGameFlow.tsx index b1f8aad..4494fd7 100644 --- a/frontend/src/features/games/helpdesk/components/HelpdeskGameFlow.tsx +++ b/frontend/src/features/games/helpdesk/components/HelpdeskGameFlow.tsx @@ -1,7 +1,7 @@ 'use client'; import { useRouter } from 'next/navigation'; -import { useCallback, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import type { Game2Data } from '@/features/games/types'; import { submitGame } from '@/lib/api'; import { useHelpdeskGame } from '../hooks/useHelpdeskGame'; @@ -16,6 +16,36 @@ export default function HelpdeskGameFlow() { const [submitStatus, setSubmitStatus] = useState('loading'); const pendingDataRef = useRef(null); + // --- サウンド管理用のRefとヘルパー --- + const bgmRef = useRef(null); + + const playSE = useCallback((path: string) => { + const audio = new Audio(path); + audio.volume = 0.5; + audio.play().catch(() => {}); + }, []); + + // BGMの初期化と再生管理 + useEffect(() => { + const bgm = new Audio('/sounds/game2-bgm.mp3'); + bgm.loop = true; + bgm.volume = 0.2; + bgmRef.current = bgm; + + const playBGM = () => { + bgm.play().catch(() => {}); + window.removeEventListener('click', playBGM); + }; + + window.addEventListener('click', playBGM); + playBGM(); // 前の画面(規約)から遷移した場合は、既にユーザー操作済みなので即再生される可能性が高い + + return () => { + bgm.pause(); + window.removeEventListener('click', playBGM); + }; + }, []); + const submitGame2 = useCallback(async (data: Game2Data) => { const userId = localStorage.getItem('user_id'); if (!userId) throw new Error('user_id が見つかりません'); @@ -31,6 +61,11 @@ export default function HelpdeskGameFlow() { pendingDataRef.current = data; setSubmitStatus('loading'); + // 終了時にBGMを停止 + if (bgmRef.current) { + bgmRef.current.pause(); + } + try { await submitGame2(data); setSubmitStatus('success'); @@ -44,21 +79,6 @@ export default function HelpdeskGameFlow() { [router, submitGame2] ); - const handleRetry = useCallback(async () => { - const data = pendingDataRef.current; - if (!data) return; - setSubmitStatus('loading'); - try { - await submitGame2(data); - setSubmitStatus('success'); - setTimeout(() => { - router.push('/games/group-chat'); - }, 2000); - } catch { - setSubmitStatus('error'); - } - }, [router, submitGame2]); - const { instructionText, inputMethod, @@ -66,36 +86,77 @@ export default function HelpdeskGameFlow() { gamePhase, remainingTimeMs, speech, - startGame, - endVoiceTurnManually, + startGame: originalStartGame, + endVoiceTurnManually: originalEndVoiceTurnManually, submitTextTurn, - switchToText, + switchToText: originalSwitchToText, onKeyDown, resetTyping, voiceApiRetrying, - retryVoiceApi, - startInstruction, + retryVoiceApi: originalRetryVoiceApi, + startInstruction: originalStartInstruction, gameTopic, hints, } = useHelpdeskGame({ onComplete: handleComplete }); - const [hintIndex, setHintIndex] = useState(0); - const [prevHints, setPrevHints] = useState(hints); + // --- アクションをラップしてSEを追加 --- + const startInstruction = useCallback(() => { + playSE('/sounds/general-button-se.mp3'); + originalStartInstruction(); + }, [originalStartInstruction, playSE]); - // ヒント内容が更新されたら(AIの応答に基づき)、インデックスを0(おすすめ)にリセットする - if (hints !== prevHints) { - setPrevHints(hints); - setHintIndex(0); - } + const startGame = useCallback(() => { + playSE('/sounds/general-button-se.mp3'); + originalStartGame(); + }, [originalStartGame, playSE]); const handleTextSubmit = useCallback(() => { if (!textInput.trim()) return; + playSE('/sounds/general-button-se.mp3'); submitTextTurn(textInput); setTextInput(''); resetTyping(); - }, [textInput, submitTextTurn, resetTyping]); + }, [textInput, submitTextTurn, resetTyping, playSE]); + + const endVoiceTurnManually = useCallback(() => { + playSE('/sounds/general-button-se.mp3'); + originalEndVoiceTurnManually(); + }, [originalEndVoiceTurnManually, playSE]); + + const switchToText = useCallback(() => { + playSE('/sounds/general-button-se.mp3'); + originalSwitchToText(); + }, [originalSwitchToText, playSE]); + + const retryVoiceApi = useCallback(() => { + playSE('/sounds/general-button-se.mp3'); + originalRetryVoiceApi(); + }, [originalRetryVoiceApi, playSE]); + + const handleRetry = useCallback(async () => { + playSE('/sounds/general-button-se.mp3'); + const data = pendingDataRef.current; + if (!data) return; + setSubmitStatus('loading'); + try { + await submitGame2(data); + setSubmitStatus('success'); + setTimeout(() => { + router.push('/games/group-chat'); + }, 2000); + } catch { + setSubmitStatus('error'); + } + }, [submitGame2, playSE, router]); + + const [hintIndex, setHintIndex] = useState(0); + const [prevHints, setPrevHints] = useState(hints); + + if (hints !== prevHints) { + setPrevHints(hints); + setHintIndex(0); + } - // --- チャット画面 (ゲームメイン画面) --- return (
- {/* 閉じるボタン */} -

ルール

-
{instructionText}
@@ -208,7 +266,6 @@ export default function HelpdeskGameFlow() { {/* --- 画面下部の会話エリア --- */}
- {/* AI 思考中 */} {gamePhase === 'awaiting-api' && (
@@ -220,7 +277,6 @@ export default function HelpdeskGameFlow() {
)} - {/* AI 発話中 */} {gamePhase === 'support-speaking' && (
@@ -231,7 +287,6 @@ export default function HelpdeskGameFlow() { ? chatHistory[chatHistory.length - 1].text : ''}

- {/* ▼ 進むアイコン風 */}
)} - {/* ユーザー入力中(ストーリーモード風 2段) */} {gamePhase === 'user-input' && ( <> - {/* 上段:AIの直前の発言履歴 */}
AI @@ -263,7 +316,6 @@ export default function HelpdeskGameFlow() {

- {/* 下段:あなたの現在の入力 */}
あなた @@ -281,9 +333,10 @@ export default function HelpdeskGameFlow() {