2025-03-01 00:51:34 +01:00

436 lines
20 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} from "@fortawesome/free-solid-svg-icons";
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);
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");
setScores(results.scores);
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);
}
},
"phase-changed": (data) => {
console.log("Phase changed:", data);
setPhase(data.phase);
if (data.timeRemaining) {
setTimeLeft(data.timeRemaining);
}
}
};
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]);
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]);
const handleFrequencyChange = useCallback((newFrequency) => {
setFrequency(newFrequency);
if (role === "composer") {
send("submit-frequency", { frequency: newFrequency });
}
}, [role, send]);
const handleSendMessage = useCallback(() => {
if (inputValue.trim()) {
const messageData = { text: inputValue, sender: username };
send("send-message", messageData);
setMessages(prev => [...prev, messageData]);
setInputValue("");
}
}, [inputValue, username, send]);
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]);
// Phase-specific content rendering
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>
<p className="instruction">Spiele diesen Song mit dem Tonregler!</p>
</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="fa-solid fa-check-circle" />
</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="fa-solid fa-paper-plane" />
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, score]) => {
const user = connectedUsers.find(u => u.id === userId) || { name: 'Unbekannt' };
return (
<div key={userId} className="score-entry">
<span className="player-name">
{user.id === (socket?.id || "you") && "👉 "}
{user.name}
{userId === connectedUsers.find(u => u.id === "user1")?.id && (
<FontAwesomeIcon icon={faCrown} className="host-icon" />
)}
</span>
<span className="player-score">{score}</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">
<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}
/>
)}
</div>
);
};
export default Game;