550 lines
25 KiB
JavaScript

import "./styles.sass";
import {SocketContext} from "@/common/contexts/SocketContext";
import {useContext, useState, useEffect, useRef, useCallback} from "react";
import MusicSlider from "@/pages/Game/components/MusicSlider";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {
faMessage,
faMusic,
faHeadphones,
faClock,
faCrown,
faPaperPlane,
faCheckCircle,
faVolumeUp,
faStepForward
} from "@fortawesome/free-solid-svg-icons";
import { fetchPlaylistSongs } from "@/services/youtubeService.js";
import YouTubePlayer from "../../components/YouTubePlayer/YouTubePlayer";
export const Game = () => {
const {send, on, socket, connected, connect} = useContext(SocketContext);
useEffect(() => {
if (!connected) {
connect();
}
}, [connected, connect]);
const [role, setRole] = useState(null);
const [round, setRound] = useState(1);
const [phase, setPhase] = useState("waiting");
const [timeLeft, setTimeLeft] = useState(30);
const [frequency, setFrequency] = useState(440);
const [currentSong, setCurrentSong] = useState(null);
const [songOptions, setSongOptions] = useState([]);
const [scores, setScores] = useState({});
const [selectedSong, setSelectedSong] = useState(null);
const [guessResult, setGuessResult] = useState(null);
const [isHost, setIsHost] = useState(false);
const [hasGuessed, setHasGuessed] = useState(false);
const [messages, setMessages] = useState([]);
const [inputValue, setInputValue] = useState("");
const [connectedUsers, setConnectedUsers] = useState([]);
const [username, setUsername] = useState("");
const messageEndRef = useRef(null);
const timerIntervalRef = useRef(null);
const [allSongs, setAllSongs] = useState([]);
const [songsLoading, setSongsLoading] = useState(false);
const [composerIsPlaying, setComposerIsPlaying] = useState(false);
useEffect(() => {
if (!connected) return;
const eventHandlers = {
"roles-assigned": (roles) => {
const myRole = roles[socket?.id];
if (myRole) {
setRole(myRole);
setMessages(prev => [...prev, {
system: true,
text: myRole === "composer"
? "Du bist der Komponist! Spiele den Song mit dem Tonregler."
: "Du bist ein Rater! Höre die Frequenzen und versuche, den Song zu erkennen."
}]);
}
},
"song-selected": setCurrentSong,
"round-started": (data) => {
setRound(data.round);
setPhase("composing");
setTimeLeft(data.timeRemaining);
},
"guessing-phase-started": (data) => {
console.log("Guessing phase started:", data);
setPhase("guessing");
setTimeLeft(data.timeRemaining);
setSongOptions(data.songOptions || []);
},
"guess-result": (result) => {
setGuessResult(result.isCorrect);
setCurrentSong(result.correctSong);
},
"round-results": (results) => {
setPhase("results");
const scoresWithNames = {};
Object.entries(results.scores).forEach(([userId, score]) => {
const user = connectedUsers.find(u => u.id === userId);
scoresWithNames[userId] = {
score: score,
name: user?.name || results.playerNames?.[userId] || "Player"
};
});
setScores(scoresWithNames);
if (!currentSong) {
setCurrentSong(results.selectedSong);
}
},
"room-users-update": (users) => {
setConnectedUsers(users);
const currentUser = users.find(u => u.id === socket?.id);
if (currentUser) setUsername(currentUser.name);
},
"host-status": (status) => setIsHost(status.isHost),
"chat-message": (msg) => setMessages(prev => [...prev, msg]),
"user-connected": (userData) => {
setMessages(prev => [...prev, {
system: true,
text: `${userData.name} ist beigetreten`
}]);
setConnectedUsers(prev => [...prev, userData]);
},
"user-disconnected": (userId) => {
setConnectedUsers(prev => {
const user = prev.find(u => u.id === userId);
if (user) {
setMessages(prevMsgs => [...prevMsgs, {
system: true,
text: `${user.name} hat den Raum verlassen`
}]);
}
return prev.filter(u => u.id !== userId);
});
},
"frequency-update": (data) => {
if (phase === "composing") {
console.log("Received frequency update:", data.frequency);
setFrequency(data.frequency);
setComposerIsPlaying(data.isPlaying); // Make sure isPlaying is handled
}
},
"phase-changed": (data) => {
console.log("Phase changed:", data);
setPhase(data.phase);
if (data.timeRemaining) {
setTimeLeft(data.timeRemaining);
}
},
"chat-message-confirmation": (msg) => {
setMessages(prev => prev.map(prevMsg => {
if (!prevMsg.system && prevMsg.text === msg.text && prevMsg.sender !== msg.sender) {
console.log(`Updating message sender from "${prevMsg.sender}" to "${msg.sender}"`);
return { ...prevMsg, sender: msg.sender };
}
return prevMsg;
}));
},
};
const cleanupFunctions = Object.entries(eventHandlers).map(
([event, handler]) => on(event, handler)
);
if (connected) {
send("get-room-users");
send("check-host-status");
}
return () => cleanupFunctions.forEach(cleanup => cleanup());
}, [socket, on, send, role, currentSong, phase, connected, connectedUsers]);
useEffect(() => {
if (timerIntervalRef.current) clearInterval(timerIntervalRef.current);
if (phase !== "waiting" && phase !== "results") {
timerIntervalRef.current = setInterval(() => {
setTimeLeft(prev => Math.max(0, prev - 1));
}, 1000);
}
return () => {
if (timerIntervalRef.current) clearInterval(timerIntervalRef.current);
};
}, [phase]);
useEffect(() => {
messageEndRef.current?.scrollIntoView({behavior: "smooth"});
}, [messages]);
useEffect(() => {
if (!connected || !socket) return;
const loadSongs = async () => {
try {
setSongsLoading(true);
console.log("Fetching songs with active socket:", socket?.id);
const songs = await fetchPlaylistSongs(socket);
console.log(`Successfully loaded ${songs.length} songs`);
setAllSongs(songs);
} catch (error) {
console.error("Error loading songs:", error);
} finally {
setSongsLoading(false);
}
};
loadSongs();
}, [socket, connected]);
useEffect(() => {
if (!allSongs.length || !currentSong || !currentSong.id) return;
const enhancedSong = allSongs.find(song => song.id === currentSong.id);
if (enhancedSong && enhancedSong !== currentSong) {
setCurrentSong(enhancedSong);
}
}, [allSongs, currentSong]);
useEffect(() => {
if (!allSongs.length || !songOptions.length) return;
const enhancedOptions = songOptions.map(option => {
if (option.id && !option.title) {
return allSongs.find(song => song.id === option.id) || option;
}
return option;
});
if (JSON.stringify(enhancedOptions) !== JSON.stringify(songOptions)) {
setSongOptions(enhancedOptions);
}
}, [allSongs, songOptions]);
const handleFrequencyChange = useCallback((newFrequency, isPlaying) => {
setFrequency(newFrequency);
setComposerIsPlaying(isPlaying);
if (role === "composer") {
send("submit-frequency", { frequency: newFrequency, isPlaying });
}
}, [role, send]);
const handleSendMessage = useCallback(() => {
if (inputValue.trim()) {
const currentUser = connectedUsers.find(u => u.id === socket?.id);
const senderName = currentUser?.name || username || "Player";
const messageData = { text: inputValue, sender: senderName };
send("send-message", messageData);
setMessages(prev => [...prev, messageData]);
setInputValue("");
}
}, [inputValue, send, socket?.id, connectedUsers, username]);
const handleSongSelect = useCallback((song) => {
setSelectedSong(song);
}, []);
const handleSubmitGuess = useCallback(() => {
if (!selectedSong || phase !== 'guessing') return;
console.log("Submitting guess:", selectedSong.id);
send("submit-guess", { songId: selectedSong.id });
setMessages(prev => [...prev, {
system: true,
text: `Du hast "${selectedSong.title}" von ${selectedSong.artist} gewählt.`
}]);
setHasGuessed(true);
}, [selectedSong, send, phase]);
const handleNextRound = useCallback(() => {
send("next-round");
setSelectedSong(null);
setGuessResult(null);
setTimeLeft(0);
}, [send]);
const handlePlayerReady = useCallback(() => {
console.log("Player ready");
}, []);
const renderPhaseContent = () => {
switch (phase) {
case "waiting":
return (
<div className="waiting-phase">
<h3>Warten auf Spielstart...</h3>
</div>
);
case "composing":
return (
<div className="composing-phase">
<div className="phase-header">
<h3>Runde {round}: {role === "composer" ? "Spielen" : "Zuhören"}</h3>
<div className="timer">
<FontAwesomeIcon icon={faClock} /> {timeLeft}s
</div>
</div>
{role === "composer" && currentSong && (
<div className="song-display">
<div className="song-card">
<img src={currentSong.coverUrl} alt={currentSong.title} />
<div className="song-info">
<div className="song-names">{currentSong.title}</div>
<div className="song-description">von {currentSong.artist}</div>
</div>
</div>
<div className="song-player-container">
<YouTubePlayer
videoId={currentSong.youtubeId}
autoplay={true}
startTime={currentSong.refrainTime || 45}
onReady={handlePlayerReady}
className="song-embedded-player"
/>
</div>
<div className="music-controls">
<p className="instruction">Use the player above to listen, and the tone slider to play!</p>
</div>
</div>
)}
{role === "guesser" && (
<div className="listening-display">
<div className="listening-icon">
<FontAwesomeIcon icon={faHeadphones} size="4x" />
</div>
<p className="instruction">Höre genau zu und versuche, den Song zu erkennen!</p>
</div>
)}
</div>
);
case "guessing":
return (
<div className="guessing-phase">
<div className="phase-header">
<h3>Runde {round}: Song erraten</h3>
<div className="timer">
<FontAwesomeIcon icon={faClock} /> {timeLeft}s
</div>
</div>
{role === "composer" ? (
<div className="waiting-for-guessers">
<p>Die Rater versuchen nun, deinen Song zu erraten...</p>
</div>
) : (
<div className="song-selection">
<p className="instruction">Welchen Song hat der Komponist gespielt?</p>
{songOptions.length === 0 ? (
<div className="loading-songs">
<p>Lade Songoptionen...</p>
</div>
) : (
<>
<div className="song-grid">
{songOptions.map(song => (
<div
key={song.id}
className={`song-option ${selectedSong?.id === song.id ? 'selected' : ''} ${hasGuessed ? 'disabled' : ''}`}
onClick={() => !hasGuessed && handleSongSelect(song)}
>
<div className="song-image">
<img src={song.coverUrl} alt={song.title} />
{selectedSong?.id === song.id && (
<div className="selection-indicator">
<FontAwesomeIcon icon={faCheckCircle} />
</div>
)}
</div>
<div className="song-details">
<div className="song-title">{song.title}</div>
<div className="song-artist">{song.artist}</div>
</div>
</div>
))}
</div>
<div className="guess-actions">
{hasGuessed ? (
<p className="guess-submitted">Deine Antwort wurde eingereicht!</p>
) : (
<button
className={`submit-guess-button ${!selectedSong ? 'disabled' : ''}`}
onClick={handleSubmitGuess}
disabled={!selectedSong}
>
<FontAwesomeIcon icon={faPaperPlane} />
Antwort einreichen
</button>
)}
</div>
</>
)}
</div>
)}
</div>
);
case "results":
return (
<div className="results-phase">
<h3>Runde {round}: Ergebnisse</h3>
<div className="round-results">
{role === "composer" ? (
<div className="composer-results">
<p>Die Rater haben versucht, deinen Song zu erraten.</p>
</div>
) : (
<div className="guesser-results">
{currentSong && (
<div className="correct-song">
<p>Der richtige Song war:</p>
<div className="song-card highlight">
<img src={currentSong.coverUrl} alt={currentSong.title} />
<div className="song-info">
<div className="song-names">{currentSong.title}</div>
<div className="song-description">von {currentSong.artist}</div>
</div>
</div>
</div>
)}
{guessResult !== null && (
<div className={`guess-result ${guessResult ? 'correct' : 'incorrect'}`}>
{guessResult
? 'Richtig! Du erhälst 10 Punkte.'
: 'Leider falsch. Kein Punkt.'}
</div>
)}
</div>
)}
<div className="scoreboard">
<h4>Punktestand</h4>
<div className="scores">
{Object.entries(scores).map(([userId, scoreData]) => {
const isCurrentUser = userId === socket?.id;
const isHost = connectedUsers.find(u => u.id === "user1")?.id === userId;
const playerName = typeof scoreData === 'object' ?
scoreData.name :
(connectedUsers.find(u => u.id === userId)?.name || "Player");
const playerScore = typeof scoreData === 'object' ? scoreData.score : scoreData;
return (
<div key={userId} className={`score-entry ${isCurrentUser ? 'current-user' : ''}`}>
<span className="player-name">
{isCurrentUser && "👉 "}
{playerName}
{isHost && (
<FontAwesomeIcon icon={faCrown} className="host-icon" />
)}
</span>
<span className="player-score">{playerScore}</span>
</div>
);
})}
</div>
</div>
{isHost ? (
<button className="next-round-button" onClick={handleNextRound}>
Nächste Runde
</button>
) : (
<p className="waiting-message">Warten auf Rundenwechsel durch Host...</p>
)}
</div>
</div>
);
default:
return <div>Unknown phase</div>;
}
};
return (
<div className={`game-page ${phase === "composing" ? "with-otamatone" : ""}`}>
<div className="background-overlay">
<div className="rotating-gradient"></div>
</div>
<div className="game-header">
<div className="game-role">
<FontAwesomeIcon icon={role === "composer" ? faMusic : faHeadphones} />
<span>{role === "composer" ? "Komponist" : "Rater"}</span>
</div>
<h2>ToneGuessr</h2>
<div className="game-round">Runde {round}</div>
</div>
<div className="game-layout">
<div className="main-content">
{renderPhaseContent()}
</div>
<div className="chat-panel">
<div className="chat-header">
<FontAwesomeIcon icon={faMessage} />
<div className="chat-title">Chat</div>
</div>
<div className="chat-messages">
{messages.map((message, index) => (
<div key={index} className={`message ${message.system ? 'system-message' : ''}`}>
{message.system ? (
<span className="message-text system">{message.text}</span>
) : (
<>
<span className="message-sender">{message.sender}:</span>
<span className="message-text">{message.text}</span>
</>
)}
</div>
))}
<div ref={messageEndRef}></div>
</div>
<div className="chat-input">
<input type="text" value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleSendMessage()}
placeholder="Nachricht..."
/>
<button onClick={handleSendMessage} className="send-button">
<FontAwesomeIcon icon={faPaperPlane} />
</button>
</div>
</div>
</div>
{phase === "composing" && (
<MusicSlider
isReadOnly={role !== "composer"}
onFrequencyChange={handleFrequencyChange}
frequency={frequency}
composerIsPlaying={composerIsPlaying}
/>
)}
{songsLoading && (
<div className="songs-loading-indicator">
Loading songs...
</div>
)}
</div>
);
};
export default Game;