Compare commits
6 Commits
0a4a9a9d0e
...
f00ca9ba7c
Author | SHA1 | Date | |
---|---|---|---|
f00ca9ba7c | |||
6e8a235629 | |||
184aa09fcf | |||
a3daab2f84 | |||
87fc9a2f39 | |||
7d7dd263fe |
168
client/src/common/contexts/SocketContext.jsx
Normal file
168
client/src/common/contexts/SocketContext.jsx
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
import { createContext, useState, useEffect, useCallback, useRef } from 'react';
|
||||||
|
import { io } from 'socket.io-client';
|
||||||
|
|
||||||
|
export const SocketContext = createContext();
|
||||||
|
|
||||||
|
export const SocketProvider = ({ children }) => {
|
||||||
|
const [socket, setSocket] = useState(null);
|
||||||
|
const [connected, setConnected] = useState(false);
|
||||||
|
const connectAttempts = useRef(0);
|
||||||
|
const maxAttempts = 3;
|
||||||
|
const pendingEventHandlers = useRef({});
|
||||||
|
const isConnecting = useRef(false);
|
||||||
|
|
||||||
|
const connect = useCallback(() => {
|
||||||
|
if (socket || isConnecting.current) return;
|
||||||
|
|
||||||
|
isConnecting.current = true;
|
||||||
|
console.log("Connecting to socket server...");
|
||||||
|
|
||||||
|
const serverUrl = process.env.NODE_ENV === 'production'
|
||||||
|
? window.location.origin
|
||||||
|
: 'http://localhost:5287';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newSocket = io(serverUrl, {
|
||||||
|
reconnectionAttempts: 3,
|
||||||
|
timeout: 10000,
|
||||||
|
reconnectionDelay: 1000,
|
||||||
|
autoConnect: true
|
||||||
|
});
|
||||||
|
|
||||||
|
newSocket.on('connect', () => {
|
||||||
|
console.log('Socket connected with ID:', newSocket.id);
|
||||||
|
setConnected(true);
|
||||||
|
connectAttempts.current = 0;
|
||||||
|
isConnecting.current = false;
|
||||||
|
|
||||||
|
Object.entries(pendingEventHandlers.current).forEach(([event, handlers]) => {
|
||||||
|
handlers.forEach(handler => {
|
||||||
|
console.log(`Registering pending handler for event: ${event}`);
|
||||||
|
newSocket.on(event, handler);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
localStorage.setItem('playerId', newSocket.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
newSocket.on('disconnect', (reason) => {
|
||||||
|
console.log('Socket disconnected:', reason);
|
||||||
|
setConnected(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
newSocket.on('connect_error', (error) => {
|
||||||
|
console.error('Connection error:', error);
|
||||||
|
connectAttempts.current += 1;
|
||||||
|
isConnecting.current = false;
|
||||||
|
|
||||||
|
if (connectAttempts.current >= maxAttempts) {
|
||||||
|
console.error('Max connection attempts reached, giving up');
|
||||||
|
newSocket.disconnect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setSocket(newSocket);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error creating socket connection:', err);
|
||||||
|
isConnecting.current = false;
|
||||||
|
}
|
||||||
|
}, [socket]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!socket && !isConnecting.current) {
|
||||||
|
connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
pendingEventHandlers.current = {};
|
||||||
|
};
|
||||||
|
}, [connect, socket]);
|
||||||
|
|
||||||
|
const disconnect = useCallback(() => {
|
||||||
|
if (socket) {
|
||||||
|
socket.disconnect();
|
||||||
|
setSocket(null);
|
||||||
|
setConnected(false);
|
||||||
|
console.log('Socket manually disconnected');
|
||||||
|
pendingEventHandlers.current = {};
|
||||||
|
}
|
||||||
|
}, [socket]);
|
||||||
|
|
||||||
|
const send = useCallback((event, data) => {
|
||||||
|
if (!socket) {
|
||||||
|
console.warn(`Cannot send event "${event}": socket not connected. Auto-connecting...`);
|
||||||
|
connect();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!connected) {
|
||||||
|
console.warn(`Socket exists but not connected when sending "${event}". Waiting for connection...`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
socket.emit(event, data);
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Error sending event "${event}":`, err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}, [socket, connected, connect]);
|
||||||
|
|
||||||
|
const on = useCallback((event, callback) => {
|
||||||
|
if (!socket) {
|
||||||
|
console.log(`Deferring registration for event "${event}" until socket is ready`);
|
||||||
|
|
||||||
|
if (!pendingEventHandlers.current[event]) {
|
||||||
|
pendingEventHandlers.current[event] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingEventHandlers.current[event].push(callback);
|
||||||
|
|
||||||
|
if (!isConnecting.current) {
|
||||||
|
connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (pendingEventHandlers.current[event]) {
|
||||||
|
const index = pendingEventHandlers.current[event].indexOf(callback);
|
||||||
|
if (index > -1) {
|
||||||
|
pendingEventHandlers.current[event].splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.on(event, callback);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (socket) {
|
||||||
|
socket.off(event, callback);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [socket, connect]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (socket) {
|
||||||
|
socket.disconnect();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [socket]);
|
||||||
|
|
||||||
|
const contextValue = {
|
||||||
|
socket,
|
||||||
|
connected,
|
||||||
|
connect,
|
||||||
|
disconnect,
|
||||||
|
send,
|
||||||
|
on,
|
||||||
|
isConnecting: isConnecting.current
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SocketContext.Provider value={contextValue}>
|
||||||
|
{children}
|
||||||
|
</SocketContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
@ -1,101 +1,395 @@
|
|||||||
import "./styles.sass";
|
import "./styles.sass";
|
||||||
import {StateContext} from "@/common/contexts/StateContext";
|
|
||||||
import {SocketContext} from "@/common/contexts/SocketContext";
|
import {SocketContext} from "@/common/contexts/SocketContext";
|
||||||
import {useContext, useState, useEffect, useRef} from "react";
|
import {useContext, useState, useEffect, useRef, useCallback} from "react";
|
||||||
import MusicSlider from "@/pages/Game/components/MusicSlider";
|
import MusicSlider from "@/pages/Game/components/MusicSlider";
|
||||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||||
import {faMessage} from "@fortawesome/free-solid-svg-icons";
|
import {faMessage, faMusic, faHeadphones, faClock, faCrown, faPaperPlane} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
export const Game = () => {
|
export const Game = () => {
|
||||||
const {setCurrentState} = useContext(StateContext);
|
const {send, on, socket, connected, connect} = useContext(SocketContext);
|
||||||
const {send, on, socket} = 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 [messages, setMessages] = useState([]);
|
||||||
const [inputValue, setInputValue] = useState("");
|
const [inputValue, setInputValue] = useState("");
|
||||||
const [connectedUsers, setConnectedUsers] = useState([]);
|
const [connectedUsers, setConnectedUsers] = useState([]);
|
||||||
const [username, setUsername] = useState("");
|
const [username, setUsername] = useState("");
|
||||||
const messageEndRef = useRef(null);
|
const messageEndRef = useRef(null);
|
||||||
|
const timerIntervalRef = useRef(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleChatMessage = (messageData) => {
|
if (!connected) return;
|
||||||
setMessages(prev => [...prev, messageData]);
|
|
||||||
};
|
const eventHandlers = {
|
||||||
|
"roles-assigned": (roles) => {
|
||||||
const handleUserConnected = (userData) => {
|
const myRole = roles[socket?.id];
|
||||||
setMessages(prev => [...prev, {
|
if (myRole) {
|
||||||
system: true,
|
setRole(myRole);
|
||||||
text: `${userData.name} ist beigetreten`
|
setMessages(prev => [...prev, {
|
||||||
}]);
|
|
||||||
setConnectedUsers(prev => [...prev, userData]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUserDisconnected = (userId) => {
|
|
||||||
setConnectedUsers(prev => {
|
|
||||||
const user = prev.find(u => u.id === userId);
|
|
||||||
if (user) {
|
|
||||||
setMessages(prevMsgs => [...prevMsgs, {
|
|
||||||
system: true,
|
system: true,
|
||||||
text: `${user.name} hat den Raum verlassen`
|
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."
|
||||||
}]);
|
}]);
|
||||||
}
|
}
|
||||||
return prev.filter(u => u.id !== userId);
|
},
|
||||||
});
|
"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);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (socket && socket.id) {
|
const cleanupFunctions = Object.entries(eventHandlers).map(
|
||||||
send("get-user-info");
|
([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);
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleUserInfo = (userInfo) => {
|
|
||||||
setUsername(userInfo.name);
|
|
||||||
};
|
|
||||||
|
|
||||||
const cleanupChatMessage = on("chat-message", handleChatMessage);
|
|
||||||
const cleanupUserConnected = on("user-connected", handleUserConnected);
|
|
||||||
const cleanupUserDisconnected = on("user-disconnected", handleUserDisconnected);
|
|
||||||
const cleanupUserInfo = on("user-info", handleUserInfo);
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cleanupChatMessage();
|
if (timerIntervalRef.current) clearInterval(timerIntervalRef.current);
|
||||||
cleanupUserConnected();
|
|
||||||
cleanupUserDisconnected();
|
|
||||||
cleanupUserInfo();
|
|
||||||
};
|
};
|
||||||
}, [on, send, socket]);
|
}, [phase]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
messageEndRef.current?.scrollIntoView({behavior: "smooth"});
|
messageEndRef.current?.scrollIntoView({behavior: "smooth"});
|
||||||
}, [messages]);
|
}, [messages]);
|
||||||
|
|
||||||
const handleSendMessage = () => {
|
const handleFrequencyChange = useCallback((newFrequency) => {
|
||||||
|
setFrequency(newFrequency);
|
||||||
|
if (role === "composer") {
|
||||||
|
send("submit-frequency", { frequency: newFrequency });
|
||||||
|
}
|
||||||
|
}, [role, send]);
|
||||||
|
|
||||||
|
const handleSendMessage = useCallback(() => {
|
||||||
if (inputValue.trim()) {
|
if (inputValue.trim()) {
|
||||||
const messageData = {
|
const messageData = { text: inputValue, sender: username };
|
||||||
text: inputValue,
|
|
||||||
sender: username
|
|
||||||
};
|
|
||||||
send("send-message", messageData);
|
send("send-message", messageData);
|
||||||
setMessages(prev => [...prev, messageData]);
|
setMessages(prev => [...prev, messageData]);
|
||||||
setInputValue("");
|
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 (
|
return (
|
||||||
<div className="game-page">
|
<div className={`game-page ${phase === "composing" ? "with-otamatone" : ""}`}>
|
||||||
<div className="background-overlay">
|
<div className="background-overlay">
|
||||||
<div className="rotating-gradient"></div>
|
<div className="rotating-gradient"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="main-content">
|
<div className="game-header">
|
||||||
<div className="song-display">
|
<div className="game-role">
|
||||||
<h2>ToneGuessr</h2>
|
<FontAwesomeIcon icon={role === "composer" ? faMusic : faHeadphones} />
|
||||||
<div className="song-card">
|
<span>{role === "composer" ? "Komponist" : "Rater"}</span>
|
||||||
<img src="https://mir-s3-cdn-cf.behance.net/project_modules/1400/fe529a64193929.5aca8500ba9ab.jpg" alt="Song"/>
|
|
||||||
<div className="song-info">
|
|
||||||
<div className="song-names">Black Steam</div>
|
|
||||||
<div className="song-description">von Carrot Quest GmbH</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="chat-window">
|
<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">
|
<div className="chat-header">
|
||||||
<FontAwesomeIcon icon={faMessage} />
|
<FontAwesomeIcon icon={faMessage} />
|
||||||
<div className="chat-title">Chat</div>
|
<div className="chat-title">Chat</div>
|
||||||
@ -119,13 +413,24 @@ export const Game = () => {
|
|||||||
<input type="text" value={inputValue}
|
<input type="text" value={inputValue}
|
||||||
onChange={(e) => setInputValue(e.target.value)}
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
onKeyPress={(e) => e.key === 'Enter' && handleSendMessage()}
|
onKeyPress={(e) => e.key === 'Enter' && handleSendMessage()}
|
||||||
placeholder="Gib eine Nachricht ein..."
|
placeholder="Nachricht..."
|
||||||
/>
|
/>
|
||||||
<button onClick={handleSendMessage}>Send</button>
|
<button onClick={handleSendMessage} className="send-button">
|
||||||
|
<FontAwesomeIcon icon={faPaperPlane} />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<MusicSlider/>
|
|
||||||
|
{phase === "composing" && (
|
||||||
|
<MusicSlider
|
||||||
|
isReadOnly={role !== "composer"}
|
||||||
|
onFrequencyChange={handleFrequencyChange}
|
||||||
|
frequency={frequency}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default Game;
|
@ -1,32 +1,89 @@
|
|||||||
import {useEffect, useRef, useState} from "react";
|
import {useEffect, useRef, useState} from "react";
|
||||||
import "./styles.sass";
|
import "./styles.sass";
|
||||||
|
|
||||||
export const MusicSlider = () => {
|
export const MusicSlider = ({ isReadOnly = false, onFrequencyChange, frequency: externalFrequency }) => {
|
||||||
const [frequency, setFrequency] = useState(440);
|
const [frequency, setFrequency] = useState(externalFrequency || 440);
|
||||||
const [isPlaying, setIsPlaying] = useState(false);
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
const [dragging, setDragging] = useState(false);
|
const [dragging, setDragging] = useState(false);
|
||||||
const audioContextRef = useRef(null);
|
const audioContextRef = useRef(null);
|
||||||
const oscillatorRef = useRef(null);
|
const oscillatorRef = useRef(null);
|
||||||
const gainNodeRef = useRef(null);
|
const gainNodeRef = useRef(null);
|
||||||
const sliderRef = useRef(null);
|
const sliderRef = useRef(null);
|
||||||
|
const hasInteractedRef = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const initAudioContext = () => {
|
||||||
|
if (!audioContextRef.current && !hasInteractedRef.current) {
|
||||||
|
hasInteractedRef.current = true;
|
||||||
|
audioContextRef.current = new (window.AudioContext || window.webkitAudioContext)();
|
||||||
|
if (!oscillatorRef.current) {
|
||||||
|
startAudio();
|
||||||
|
}
|
||||||
|
document.removeEventListener('click', initAudioContext);
|
||||||
|
document.removeEventListener('touchstart', initAudioContext);
|
||||||
|
document.removeEventListener('keydown', initAudioContext);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('click', initAudioContext);
|
||||||
|
document.addEventListener('touchstart', initAudioContext);
|
||||||
|
document.addEventListener('keydown', initAudioContext);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('click', initAudioContext);
|
||||||
|
document.removeEventListener('touchstart', initAudioContext);
|
||||||
|
document.removeEventListener('keydown', initAudioContext);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (externalFrequency !== undefined && !dragging) {
|
||||||
|
setFrequency(externalFrequency);
|
||||||
|
|
||||||
|
if (audioContextRef.current) {
|
||||||
|
if (!isPlaying) {
|
||||||
|
startAudio();
|
||||||
|
} else if (oscillatorRef.current) {
|
||||||
|
oscillatorRef.current.frequency.setValueAtTime(
|
||||||
|
externalFrequency,
|
||||||
|
audioContextRef.current.currentTime
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [externalFrequency, dragging, isPlaying]);
|
||||||
|
|
||||||
const handleMouseDown = (e) => {
|
const handleMouseDown = (e) => {
|
||||||
|
if (isReadOnly) return;
|
||||||
|
|
||||||
setDragging(true);
|
setDragging(true);
|
||||||
startAudio();
|
|
||||||
handleFrequencyChange(e);
|
handleFrequencyChange(e);
|
||||||
|
|
||||||
|
if (!audioContextRef.current) {
|
||||||
|
audioContextRef.current = new (window.AudioContext || window.webkitAudioContext)();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isPlaying) {
|
||||||
|
startAudio();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMouseUp = () => {
|
const handleMouseUp = () => {
|
||||||
setDragging(false);
|
setDragging(false);
|
||||||
stopAudio();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFrequencyChange = (e) => {
|
const handleFrequencyChange = (e) => {
|
||||||
|
if (isReadOnly) return;
|
||||||
|
|
||||||
const rect = sliderRef.current.getBoundingClientRect();
|
const rect = sliderRef.current.getBoundingClientRect();
|
||||||
const x = e.clientX - rect.left;
|
const x = e.clientX - rect.left;
|
||||||
const clampedX = Math.max(0, Math.min(x, rect.width));
|
const clampedX = Math.max(0, Math.min(x, rect.width));
|
||||||
const newFrequency = 20 + (clampedX / rect.width) * 1980;
|
const newFrequency = 20 + (clampedX / rect.width) * 1980;
|
||||||
setFrequency(newFrequency);
|
setFrequency(newFrequency);
|
||||||
|
|
||||||
|
if (onFrequencyChange) {
|
||||||
|
onFrequencyChange(newFrequency);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -43,46 +100,108 @@ export const MusicSlider = () => {
|
|||||||
}, [dragging]);
|
}, [dragging]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isPlaying) {
|
if (isPlaying && oscillatorRef.current && audioContextRef.current) {
|
||||||
if (oscillatorRef.current) {
|
oscillatorRef.current.frequency.setValueAtTime(
|
||||||
oscillatorRef.current.frequency.setValueAtTime(frequency, audioContextRef.current.currentTime);
|
frequency,
|
||||||
}
|
audioContextRef.current.currentTime
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}, [frequency, isPlaying]);
|
}, [frequency, isPlaying]);
|
||||||
|
|
||||||
const startAudio = () => {
|
const startAudio = () => {
|
||||||
if (!audioContextRef.current) {
|
if (!audioContextRef.current) {
|
||||||
audioContextRef.current = new (window.AudioContext || window.webkitAudioContext)();
|
try {
|
||||||
|
audioContextRef.current = new (window.AudioContext || window.webkitAudioContext)();
|
||||||
|
} catch (e) {
|
||||||
|
console.error("AudioContext could not be created:", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!oscillatorRef.current) {
|
|
||||||
oscillatorRef.current = audioContextRef.current.createOscillator();
|
|
||||||
oscillatorRef.current.type = 'sine';
|
|
||||||
oscillatorRef.current.frequency.setValueAtTime(frequency, audioContextRef.current.currentTime);
|
|
||||||
|
|
||||||
gainNodeRef.current = audioContextRef.current.createGain();
|
if (audioContextRef.current.state === 'suspended') {
|
||||||
gainNodeRef.current.gain.setValueAtTime(0.5, audioContextRef.current.currentTime);
|
audioContextRef.current.resume().catch(err => {
|
||||||
|
console.error("Could not resume AudioContext:", err);
|
||||||
oscillatorRef.current.connect(gainNodeRef.current);
|
});
|
||||||
gainNodeRef.current.connect(audioContextRef.current.destination);
|
}
|
||||||
oscillatorRef.current.start();
|
|
||||||
} else {
|
try {
|
||||||
oscillatorRef.current.frequency.setValueAtTime(frequency, audioContextRef.current.currentTime);
|
if (!oscillatorRef.current) {
|
||||||
|
oscillatorRef.current = audioContextRef.current.createOscillator();
|
||||||
|
oscillatorRef.current.type = 'sine';
|
||||||
|
oscillatorRef.current.frequency.setValueAtTime(
|
||||||
|
frequency,
|
||||||
|
audioContextRef.current.currentTime
|
||||||
|
);
|
||||||
|
|
||||||
|
gainNodeRef.current = audioContextRef.current.createGain();
|
||||||
|
gainNodeRef.current.gain.setValueAtTime(0.5, audioContextRef.current.currentTime);
|
||||||
|
|
||||||
|
oscillatorRef.current.connect(gainNodeRef.current);
|
||||||
|
gainNodeRef.current.connect(audioContextRef.current.destination);
|
||||||
|
oscillatorRef.current.start();
|
||||||
|
console.log("Audio started successfully");
|
||||||
|
} else {
|
||||||
|
oscillatorRef.current.frequency.setValueAtTime(
|
||||||
|
frequency,
|
||||||
|
audioContextRef.current.currentTime
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setIsPlaying(true);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error starting audio:", e);
|
||||||
}
|
}
|
||||||
setIsPlaying(true);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const stopAudio = () => {
|
const stopAudio = () => {
|
||||||
if (oscillatorRef.current) {
|
if (oscillatorRef.current) {
|
||||||
oscillatorRef.current.stop();
|
try {
|
||||||
oscillatorRef.current.disconnect();
|
oscillatorRef.current.stop();
|
||||||
oscillatorRef.current = null;
|
oscillatorRef.current.disconnect();
|
||||||
|
oscillatorRef.current = null;
|
||||||
|
setIsPlaying(false);
|
||||||
|
console.log("Audio stopped");
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error stopping audio:", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
setIsPlaying(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (oscillatorRef.current) {
|
||||||
|
try {
|
||||||
|
oscillatorRef.current.stop();
|
||||||
|
oscillatorRef.current.disconnect();
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error cleaning up oscillator:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (audioContextRef.current && audioContextRef.current.state !== 'closed') {
|
||||||
|
try {
|
||||||
|
audioContextRef.current.close();
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error closing AudioContext:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (isPlaying) {
|
||||||
|
stopAudio();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [isPlaying]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="otamatone-container">
|
<div className={`otamatone-container ${isReadOnly ? 'read-only' : ''}`}>
|
||||||
<div className="otamatone" onMouseDown={handleMouseDown}>
|
<div
|
||||||
|
className="otamatone"
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
style={{ cursor: isReadOnly ? 'default' : 'pointer' }}
|
||||||
|
>
|
||||||
<div className="otamatone-face">
|
<div className="otamatone-face">
|
||||||
<div className="otamatone-mouth" style={{
|
<div className="otamatone-mouth" style={{
|
||||||
height: `${10 + (frequency / 2000) * 40}px`,
|
height: `${10 + (frequency / 2000) * 40}px`,
|
||||||
@ -101,5 +220,5 @@ export const MusicSlider = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
@ -9,17 +9,19 @@
|
|||||||
right: 0
|
right: 0
|
||||||
bottom: 0
|
bottom: 0
|
||||||
padding: 20px 30px 30px 30px
|
padding: 20px 30px 30px 30px
|
||||||
background: rgba(20, 20, 30, 0.75)
|
background: rgba(20, 20, 30, 0.85)
|
||||||
backdrop-filter: blur(15px)
|
backdrop-filter: blur(15px)
|
||||||
border-radius: 30px 30px 0 0
|
border-radius: 30px 30px 0 0
|
||||||
margin: 0
|
margin: 0
|
||||||
z-index: 10
|
z-index: 100
|
||||||
box-shadow: 0 -5px 30px rgba(0, 0, 0, 0.3), 0 -2px 10px rgba(255, 255, 255, 0.1)
|
box-shadow: 0 -5px 30px rgba(0, 0, 0, 0.3), 0 -2px 10px rgba(255, 255, 255, 0.1)
|
||||||
border-top: 1px solid rgba(255, 255, 255, 0.3)
|
border-top: 1px solid rgba(255, 255, 255, 0.3)
|
||||||
border-left: 1px solid rgba(255, 255, 255, 0.2)
|
border-left: 1px solid rgba(255, 255, 255, 0.2)
|
||||||
border-right: 1px solid rgba(255, 255, 255, 0.2)
|
border-right: 1px solid rgba(255, 255, 255, 0.2)
|
||||||
transition: transform 0.3s ease
|
transition: transform 0.3s ease
|
||||||
width: 100%
|
width: 100%
|
||||||
|
height: 100px
|
||||||
|
animation: slide-in-bottom 0.4s ease-out
|
||||||
|
|
||||||
.otamatone
|
.otamatone
|
||||||
display: flex
|
display: flex
|
||||||
@ -171,6 +173,14 @@
|
|||||||
0%
|
0%
|
||||||
transform: translateY(100%)
|
transform: translateY(100%)
|
||||||
opacity: 0
|
opacity: 0
|
||||||
|
100%
|
||||||
|
transform: translateY(0)
|
||||||
|
opacity: 1
|
||||||
|
|
||||||
|
@keyframes slide-in-bottom
|
||||||
|
0%
|
||||||
|
transform: translateY(100%)
|
||||||
|
opacity: 0
|
||||||
100%
|
100%
|
||||||
transform: translateY(0)
|
transform: translateY(0)
|
||||||
opacity: 1
|
opacity: 1
|
@ -1,26 +1,491 @@
|
|||||||
@import "@/common/styles/colors"
|
@import "@/common/styles/colors"
|
||||||
|
|
||||||
|
// Main layout improvements
|
||||||
.game-page
|
.game-page
|
||||||
display: flex
|
display: flex
|
||||||
flex-direction: column
|
flex-direction: column
|
||||||
align-items: center
|
align-items: center
|
||||||
justify-content: center
|
|
||||||
height: 100vh
|
height: 100vh
|
||||||
width: 100vw
|
width: 100vw
|
||||||
padding: 20px
|
|
||||||
position: relative
|
position: relative
|
||||||
animation: page-fade-in 1s ease-in-out
|
animation: page-fade-in 1s ease-in-out
|
||||||
|
overflow: hidden
|
||||||
|
|
||||||
.main-content
|
&.with-otamatone
|
||||||
|
.game-layout
|
||||||
|
height: calc(100vh - 280px)
|
||||||
|
padding-bottom: 20px
|
||||||
|
|
||||||
|
.chat-panel
|
||||||
|
max-height: calc(100vh - 300px)
|
||||||
|
|
||||||
|
.game-layout
|
||||||
display: flex
|
display: flex
|
||||||
flex-direction: row
|
|
||||||
align-items: flex-start
|
|
||||||
justify-content: center
|
|
||||||
width: 100%
|
width: 100%
|
||||||
padding: 20px
|
height: calc(100vh - 180px)
|
||||||
|
justify-content: space-between
|
||||||
|
align-items: stretch
|
||||||
|
padding: 0 20px
|
||||||
z-index: 2
|
z-index: 2
|
||||||
position: relative
|
position: relative
|
||||||
animation: float-up 1.5s ease-out
|
transition: height 0.3s ease
|
||||||
|
|
||||||
|
.main-content
|
||||||
|
flex: 1
|
||||||
|
padding: 20px 30px
|
||||||
|
overflow-y: auto
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
align-items: center
|
||||||
|
min-width: 0
|
||||||
|
margin-right: 20px
|
||||||
|
|
||||||
|
// Redesigned chat panel
|
||||||
|
.chat-panel
|
||||||
|
width: 280px
|
||||||
|
height: 100%
|
||||||
|
max-height: calc(100vh - 200px)
|
||||||
|
background: rgba(255, 255, 255, 0.08)
|
||||||
|
backdrop-filter: blur(10px)
|
||||||
|
border-radius: 20px
|
||||||
|
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.2), 0 0 20px rgba(255, 255, 255, 0.1)
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2)
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
overflow: hidden
|
||||||
|
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275), max-height 0.3s ease
|
||||||
|
animation: slide-in-right 0.5s ease-out
|
||||||
|
|
||||||
|
.chat-header
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
padding: 12px 15px
|
||||||
|
background: rgba(30, 30, 30, 0.5)
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.2)
|
||||||
|
|
||||||
|
svg
|
||||||
|
font-size: 16px
|
||||||
|
color: $white
|
||||||
|
filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.5))
|
||||||
|
animation: icon-pulse 3s infinite alternate ease-in-out
|
||||||
|
|
||||||
|
.chat-title
|
||||||
|
margin-left: 10px
|
||||||
|
font-size: 16px
|
||||||
|
color: $white
|
||||||
|
text-shadow: 0 0 10px rgba(255, 255, 255, 0.3)
|
||||||
|
|
||||||
|
.chat-messages
|
||||||
|
flex: 1
|
||||||
|
padding: 10px
|
||||||
|
overflow-y: auto
|
||||||
|
color: $white
|
||||||
|
font-size: 14px
|
||||||
|
|
||||||
|
&::-webkit-scrollbar
|
||||||
|
width: 4px
|
||||||
|
background: transparent
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb
|
||||||
|
background: rgba(255, 255, 255, 0.2)
|
||||||
|
border-radius: 2px
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
background: rgba(255, 255, 255, 0.3)
|
||||||
|
|
||||||
|
.message
|
||||||
|
margin-bottom: 8px
|
||||||
|
padding: 6px 10px
|
||||||
|
border-radius: 8px
|
||||||
|
background: rgba(255, 255, 255, 0.05)
|
||||||
|
word-break: break-word
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
background: rgba(255, 255, 255, 0.1)
|
||||||
|
|
||||||
|
.message-sender
|
||||||
|
font-weight: bold
|
||||||
|
color: $yellow
|
||||||
|
margin-right: 4px
|
||||||
|
font-size: 13px
|
||||||
|
|
||||||
|
.message-text
|
||||||
|
font-size: 13px
|
||||||
|
|
||||||
|
.message-text.system
|
||||||
|
font-style: italic
|
||||||
|
color: rgba(255, 255, 255, 0.6)
|
||||||
|
|
||||||
|
.system-message
|
||||||
|
text-align: center
|
||||||
|
color: rgba(255, 255, 255, 0.6)
|
||||||
|
padding: 4px 0
|
||||||
|
font-size: 12px
|
||||||
|
|
||||||
|
.chat-input
|
||||||
|
display: flex
|
||||||
|
padding: 10px
|
||||||
|
background: rgba(30, 30, 30, 0.5)
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.2)
|
||||||
|
|
||||||
|
input
|
||||||
|
flex: 1
|
||||||
|
padding: 8px 12px
|
||||||
|
border: none
|
||||||
|
border-radius: 8px
|
||||||
|
outline: none
|
||||||
|
background: rgba(0, 0, 0, 0.3)
|
||||||
|
color: $white
|
||||||
|
font-size: 14px
|
||||||
|
|
||||||
|
&:focus
|
||||||
|
box-shadow: 0 0 15px rgba(255, 255, 255, 0.2)
|
||||||
|
|
||||||
|
.send-button
|
||||||
|
margin-left: 8px
|
||||||
|
width: 36px
|
||||||
|
height: 36px
|
||||||
|
background: linear-gradient(135deg, $purple, $blue)
|
||||||
|
color: $white
|
||||||
|
border: none
|
||||||
|
border-radius: 50%
|
||||||
|
cursor: pointer
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
justify-content: center
|
||||||
|
transition: all 0.2s ease
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
transform: translateY(-2px)
|
||||||
|
box-shadow: 0 4px 15px rgba(102, 204, 255, 0.5)
|
||||||
|
|
||||||
|
&:active
|
||||||
|
transform: translateY(0)
|
||||||
|
|
||||||
|
// Game header improvements
|
||||||
|
.game-header
|
||||||
|
display: flex
|
||||||
|
justify-content: space-between
|
||||||
|
align-items: center
|
||||||
|
width: 100%
|
||||||
|
padding: 15px 30px
|
||||||
|
margin-bottom: 10px
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1)
|
||||||
|
background: rgba(0, 0, 0, 0.2)
|
||||||
|
backdrop-filter: blur(10px)
|
||||||
|
|
||||||
|
h2
|
||||||
|
font-size: 32px
|
||||||
|
background: linear-gradient(135deg, $yellow, $pink)
|
||||||
|
background-clip: text
|
||||||
|
-webkit-background-clip: text
|
||||||
|
color: transparent
|
||||||
|
margin: 0
|
||||||
|
animation: pulse 3s infinite alternate ease-in-out
|
||||||
|
text-shadow: 0 0 15px rgba(255, 255, 255, 0.3)
|
||||||
|
|
||||||
|
.game-role, .game-round
|
||||||
|
background: rgba(255, 255, 255, 0.1)
|
||||||
|
padding: 8px 15px
|
||||||
|
border-radius: 20px
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2)
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
font-size: 14px
|
||||||
|
|
||||||
|
svg
|
||||||
|
margin-right: 8px
|
||||||
|
color: $yellow
|
||||||
|
|
||||||
|
// Improved button styles
|
||||||
|
.submit-guess-button, .next-round-button, button:not(.send-button)
|
||||||
|
background: linear-gradient(135deg, $purple, $blue)
|
||||||
|
color: #fff
|
||||||
|
border: none
|
||||||
|
border-radius: 10px
|
||||||
|
padding: 12px 25px
|
||||||
|
font-size: 16px
|
||||||
|
font-weight: bold
|
||||||
|
cursor: pointer
|
||||||
|
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275)
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3)
|
||||||
|
display: inline-flex
|
||||||
|
align-items: center
|
||||||
|
justify-content: center
|
||||||
|
position: relative
|
||||||
|
overflow: hidden
|
||||||
|
|
||||||
|
&:before
|
||||||
|
content: ""
|
||||||
|
position: absolute
|
||||||
|
top: -50%
|
||||||
|
left: -50%
|
||||||
|
width: 200%
|
||||||
|
height: 200%
|
||||||
|
background: radial-gradient(circle, rgba(255, 255, 255, 0.3) 0%, transparent 70%)
|
||||||
|
opacity: 0
|
||||||
|
transition: opacity 0.5s ease
|
||||||
|
|
||||||
|
svg
|
||||||
|
margin-right: 10px
|
||||||
|
|
||||||
|
&:hover:not(.disabled)
|
||||||
|
transform: translateY(-5px)
|
||||||
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.4), 0 0 20px rgba(102, 204, 255, 0.4)
|
||||||
|
|
||||||
|
&:before
|
||||||
|
opacity: 1
|
||||||
|
|
||||||
|
&:active:not(.disabled)
|
||||||
|
transform: translateY(-2px)
|
||||||
|
|
||||||
|
&.disabled
|
||||||
|
opacity: 0.5
|
||||||
|
cursor: not-allowed
|
||||||
|
background: linear-gradient(135deg, #777, #555)
|
||||||
|
|
||||||
|
// Content component improvements
|
||||||
|
.waiting-phase
|
||||||
|
text-align: center
|
||||||
|
padding: 100px 0
|
||||||
|
animation: fade-in 0.5s ease-out
|
||||||
|
|
||||||
|
h3
|
||||||
|
font-size: 24px
|
||||||
|
color: $white
|
||||||
|
margin-bottom: 20px
|
||||||
|
animation: pulse 2s infinite alternate ease-in-out
|
||||||
|
|
||||||
|
.composing-phase, .guessing-phase, .results-phase
|
||||||
|
width: 100%
|
||||||
|
max-width: 800px
|
||||||
|
margin: 0 auto
|
||||||
|
animation: fade-in 0.5s ease-out
|
||||||
|
|
||||||
|
.song-display
|
||||||
|
text-align: center
|
||||||
|
|
||||||
|
.song-card
|
||||||
|
display: flex
|
||||||
|
flex-direction: row
|
||||||
|
align-items: center
|
||||||
|
background: rgba(255, 255, 255, 0.08)
|
||||||
|
padding: 25px
|
||||||
|
border-radius: 20px
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3)
|
||||||
|
backdrop-filter: blur(10px)
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2)
|
||||||
|
max-width: 500px
|
||||||
|
margin: 20px auto
|
||||||
|
transition: all 0.3s ease
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
transform: translateY(-5px)
|
||||||
|
border-color: rgba(255, 255, 255, 0.4)
|
||||||
|
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.4), 0 0 30px rgba(255, 255, 255, 0.1)
|
||||||
|
|
||||||
|
img
|
||||||
|
width: 120px
|
||||||
|
height: 120px
|
||||||
|
object-fit: cover
|
||||||
|
border-radius: 15px
|
||||||
|
margin-right: 20px
|
||||||
|
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.5)
|
||||||
|
|
||||||
|
.song-info
|
||||||
|
text-align: left
|
||||||
|
|
||||||
|
.song-names
|
||||||
|
font-size: 24px
|
||||||
|
color: $white
|
||||||
|
margin-bottom: 10px
|
||||||
|
|
||||||
|
.song-description
|
||||||
|
font-size: 16px
|
||||||
|
color: rgba(255, 255, 255, 0.7)
|
||||||
|
|
||||||
|
.phase-header
|
||||||
|
display: flex
|
||||||
|
justify-content: space-between
|
||||||
|
align-items: center
|
||||||
|
width: 100%
|
||||||
|
margin-bottom: 30px
|
||||||
|
|
||||||
|
h3
|
||||||
|
font-size: 24px
|
||||||
|
margin: 0
|
||||||
|
color: $white
|
||||||
|
text-shadow: 0 0 10px rgba(255, 255, 255, 0.3)
|
||||||
|
|
||||||
|
.timer
|
||||||
|
background: rgba(255, 255, 255, 0.1)
|
||||||
|
padding: 10px 20px
|
||||||
|
border-radius: 15px
|
||||||
|
font-size: 18px
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2)
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1)
|
||||||
|
|
||||||
|
svg
|
||||||
|
margin-right: 10px
|
||||||
|
color: $yellow
|
||||||
|
|
||||||
|
// Improved song selection grid
|
||||||
|
.song-selection
|
||||||
|
width: 100%
|
||||||
|
|
||||||
|
.instruction
|
||||||
|
text-align: center
|
||||||
|
font-size: 20px
|
||||||
|
margin-bottom: 30px
|
||||||
|
color: $white
|
||||||
|
|
||||||
|
.song-grid
|
||||||
|
display: grid
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr))
|
||||||
|
gap: 20px
|
||||||
|
margin-top: 20px
|
||||||
|
|
||||||
|
.song-option
|
||||||
|
background: rgba(255, 255, 255, 0.1)
|
||||||
|
border-radius: 15px
|
||||||
|
overflow: hidden
|
||||||
|
cursor: pointer
|
||||||
|
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275)
|
||||||
|
border: 2px solid transparent
|
||||||
|
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3)
|
||||||
|
height: 100%
|
||||||
|
|
||||||
|
&:hover:not(.disabled)
|
||||||
|
transform: translateY(-8px) scale(1.03)
|
||||||
|
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.4), 0 0 25px rgba(255, 255, 255, 0.1)
|
||||||
|
background: rgba(255, 255, 255, 0.15)
|
||||||
|
|
||||||
|
&.selected
|
||||||
|
border: 2px solid $yellow
|
||||||
|
box-shadow: 0 0 25px rgba(255, 255, 0, 0.4), 0 10px 30px rgba(0, 0, 0, 0.4)
|
||||||
|
background: rgba(255, 255, 255, 0.15)
|
||||||
|
|
||||||
|
&.disabled
|
||||||
|
opacity: 0.7
|
||||||
|
cursor: default
|
||||||
|
|
||||||
|
.song-image
|
||||||
|
position: relative
|
||||||
|
|
||||||
|
img
|
||||||
|
width: 100%
|
||||||
|
height: 150px
|
||||||
|
object-fit: cover
|
||||||
|
|
||||||
|
.selection-indicator
|
||||||
|
position: absolute
|
||||||
|
top: 10px
|
||||||
|
right: 10px
|
||||||
|
background: $yellow
|
||||||
|
width: 30px
|
||||||
|
height: 30px
|
||||||
|
border-radius: 50%
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
justify-content: center
|
||||||
|
box-shadow: 0 0 15px rgba(255, 255, 0, 0.7)
|
||||||
|
|
||||||
|
.song-details
|
||||||
|
padding: 15px
|
||||||
|
|
||||||
|
.song-title
|
||||||
|
font-weight: bold
|
||||||
|
font-size: 16px
|
||||||
|
color: $white
|
||||||
|
|
||||||
|
.song-artist
|
||||||
|
font-size: 14px
|
||||||
|
opacity: 0.7
|
||||||
|
margin-top: 5px
|
||||||
|
|
||||||
|
// Results phase improvements
|
||||||
|
.results-phase
|
||||||
|
text-align: center
|
||||||
|
|
||||||
|
h3
|
||||||
|
font-size: 28px
|
||||||
|
margin-bottom: 40px
|
||||||
|
|
||||||
|
.round-results
|
||||||
|
background: rgba(255, 255, 255, 0.08)
|
||||||
|
backdrop-filter: blur(10px)
|
||||||
|
border-radius: 25px
|
||||||
|
padding: 30px
|
||||||
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4)
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.15)
|
||||||
|
max-width: 600px
|
||||||
|
margin: 0 auto
|
||||||
|
|
||||||
|
.scoreboard
|
||||||
|
margin-top: 30px
|
||||||
|
padding-top: 20px
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.1)
|
||||||
|
|
||||||
|
h4
|
||||||
|
margin-bottom: 15px
|
||||||
|
font-size: 20px
|
||||||
|
|
||||||
|
.scores
|
||||||
|
display: grid
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr))
|
||||||
|
gap: 10px
|
||||||
|
|
||||||
|
.score-entry
|
||||||
|
background: rgba(255, 255, 255, 0.1)
|
||||||
|
border-radius: 10px
|
||||||
|
padding: 10px 15px
|
||||||
|
display: flex
|
||||||
|
justify-content: space-between
|
||||||
|
align-items: center
|
||||||
|
|
||||||
|
&:nth-child(1)
|
||||||
|
background: linear-gradient(135deg, rgba(255, 215, 0, 0.2), rgba(255, 215, 0, 0.1))
|
||||||
|
box-shadow: 0 0 20px rgba(255, 215, 0, 0.3)
|
||||||
|
|
||||||
|
&:nth-child(2)
|
||||||
|
background: linear-gradient(135deg, rgba(192, 192, 192, 0.2), rgba(192, 192, 192, 0.1))
|
||||||
|
|
||||||
|
&:nth-child(3)
|
||||||
|
background: linear-gradient(135deg, rgba(205, 127, 50, 0.2), rgba(205, 127, 50, 0.1))
|
||||||
|
|
||||||
|
.player-name
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
|
||||||
|
.host-icon
|
||||||
|
color: $yellow
|
||||||
|
margin-left: 5px
|
||||||
|
|
||||||
|
// Responsive adjustments
|
||||||
|
@media (max-width: 900px)
|
||||||
|
.game-layout
|
||||||
|
flex-direction: column
|
||||||
|
|
||||||
|
.main-content
|
||||||
|
margin-right: 0
|
||||||
|
margin-bottom: 20px
|
||||||
|
|
||||||
|
.chat-panel
|
||||||
|
width: 100%
|
||||||
|
max-height: 300px
|
||||||
|
|
||||||
|
// Animations
|
||||||
|
@keyframes slide-in-right
|
||||||
|
0%
|
||||||
|
transform: translateX(30px)
|
||||||
|
opacity: 0
|
||||||
|
100%
|
||||||
|
transform: translateX(0)
|
||||||
|
opacity: 1
|
||||||
|
|
||||||
|
// ...existing animations...
|
||||||
|
|
||||||
.song-display
|
.song-display
|
||||||
display: flex
|
display: flex
|
||||||
@ -281,6 +746,149 @@
|
|||||||
opacity: 1
|
opacity: 1
|
||||||
animation: rotate-background 5s linear infinite
|
animation: rotate-background 5s linear infinite
|
||||||
|
|
||||||
|
.phase-header
|
||||||
|
display: flex
|
||||||
|
justify-content: space-between
|
||||||
|
align-items: center
|
||||||
|
width: 100%
|
||||||
|
margin-bottom: 20px
|
||||||
|
|
||||||
|
.timer
|
||||||
|
background: rgba(255, 255, 255, 0.1)
|
||||||
|
padding: 10px 15px
|
||||||
|
border-radius: 20px
|
||||||
|
font-size: 18px
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
|
||||||
|
svg
|
||||||
|
margin-right: 8px
|
||||||
|
color: $yellow
|
||||||
|
|
||||||
|
.song-grid
|
||||||
|
display: grid
|
||||||
|
grid-template-columns: repeat(3, 1fr)
|
||||||
|
gap: 20px
|
||||||
|
margin-top: 20px
|
||||||
|
animation: fade-in 0.5s ease-out
|
||||||
|
|
||||||
|
@media (max-width: 768px)
|
||||||
|
grid-template-columns: repeat(2, 1fr)
|
||||||
|
|
||||||
|
@media (max-width: 500px)
|
||||||
|
grid-template-columns: 1fr
|
||||||
|
|
||||||
|
.song-option
|
||||||
|
background: rgba(255, 255, 255, 0.1)
|
||||||
|
border-radius: 15px
|
||||||
|
overflow: hidden
|
||||||
|
cursor: pointer
|
||||||
|
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275)
|
||||||
|
border: 2px solid transparent
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2)
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
transform: translateY(-5px)
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3), 0 0 20px rgba(255, 255, 255, 0.1)
|
||||||
|
background: rgba(255, 255, 255, 0.15)
|
||||||
|
|
||||||
|
&.selected
|
||||||
|
border: 2px solid $yellow
|
||||||
|
box-shadow: 0 0 20px rgba(255, 255, 0, 0.5), 0 10px 30px rgba(0, 0, 0, 0.3)
|
||||||
|
background: rgba(255, 255, 255, 0.2)
|
||||||
|
|
||||||
|
.song-image
|
||||||
|
position: relative
|
||||||
|
overflow: hidden
|
||||||
|
|
||||||
|
img
|
||||||
|
width: 100%
|
||||||
|
height: 150px
|
||||||
|
object-fit: cover
|
||||||
|
transition: transform 0.3s ease
|
||||||
|
|
||||||
|
.selected &
|
||||||
|
transform: scale(1.05)
|
||||||
|
|
||||||
|
.selection-indicator
|
||||||
|
position: absolute
|
||||||
|
top: 10px
|
||||||
|
right: 10px
|
||||||
|
background-color: $yellow
|
||||||
|
color: #000
|
||||||
|
width: 30px
|
||||||
|
height: 30px
|
||||||
|
border-radius: 50%
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
justify-content: center
|
||||||
|
box-shadow: 0 0 15px rgba(255, 255, 0, 0.7)
|
||||||
|
animation: pulse 1.5s infinite ease-in-out
|
||||||
|
|
||||||
|
.song-details
|
||||||
|
padding: 15px
|
||||||
|
|
||||||
|
.song-title
|
||||||
|
font-weight: bold
|
||||||
|
font-size: 18px
|
||||||
|
color: #fff
|
||||||
|
margin-bottom: 5px
|
||||||
|
|
||||||
|
.song-artist
|
||||||
|
font-size: 14px
|
||||||
|
color: rgba(255, 255, 255, 0.8)
|
||||||
|
|
||||||
|
.guess-actions
|
||||||
|
margin-top: 30px
|
||||||
|
text-align: center
|
||||||
|
animation: fade-in 0.5s ease-out 0.2s both
|
||||||
|
|
||||||
|
.submit-guess-button
|
||||||
|
background: linear-gradient(135deg, $purple, $blue)
|
||||||
|
color: #fff
|
||||||
|
border: none
|
||||||
|
border-radius: 10px
|
||||||
|
padding: 12px 25px
|
||||||
|
font-size: 16px
|
||||||
|
font-weight: bold
|
||||||
|
cursor: pointer
|
||||||
|
transition: all 0.3s ease
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3)
|
||||||
|
display: inline-flex
|
||||||
|
align-items: center
|
||||||
|
justify-content: center
|
||||||
|
|
||||||
|
svg
|
||||||
|
margin-right: 8px
|
||||||
|
|
||||||
|
&:hover:not(.disabled)
|
||||||
|
transform: translateY(-3px)
|
||||||
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.4), 0 0 20px rgba(102, 204, 255, 0.4)
|
||||||
|
|
||||||
|
&:active:not(.disabled)
|
||||||
|
transform: translateY(-1px)
|
||||||
|
|
||||||
|
&.disabled
|
||||||
|
opacity: 0.5
|
||||||
|
cursor: not-allowed
|
||||||
|
background: linear-gradient(135deg, #999, #666)
|
||||||
|
|
||||||
|
.selection-hint
|
||||||
|
margin-top: 10px
|
||||||
|
color: rgba(255, 255, 255, 0.7)
|
||||||
|
font-size: 14px
|
||||||
|
font-style: italic
|
||||||
|
|
||||||
|
.loading-songs
|
||||||
|
text-align: center
|
||||||
|
padding: 40px
|
||||||
|
color: $white
|
||||||
|
|
||||||
|
p
|
||||||
|
font-size: 18px
|
||||||
|
opacity: 0.8
|
||||||
|
animation: pulse 1.5s infinite ease-in-out
|
||||||
|
|
||||||
@keyframes subtle-text-glow
|
@keyframes subtle-text-glow
|
||||||
0%, 100%
|
0%, 100%
|
||||||
text-shadow: 0 0 10px rgba(255, 255, 255, 0.4)
|
text-shadow: 0 0 10px rgba(255, 255, 255, 0.4)
|
||||||
@ -337,4 +945,20 @@
|
|||||||
filter: drop-shadow(0 0 15px rgba(255, 255, 255, 0.7))
|
filter: drop-shadow(0 0 15px rgba(255, 255, 255, 0.7))
|
||||||
50%
|
50%
|
||||||
transform: translateY(-10px) scale(1.05) rotate(1deg)
|
transform: translateY(-10px) scale(1.05) rotate(1deg)
|
||||||
filter: drop-shadow(0 0 25px rgba(255, 255, 255, 0.9)) drop-shadow(0 0 50px rgba(102, 204, 255, 0.6))
|
filter: drop-shadow(0 0 25px rgba(255, 255, 255, 0.9)) drop-shadow(0 0 50px rgba(102, 204, 255, 0.6))
|
||||||
|
|
||||||
|
@keyframes fade-in
|
||||||
|
0%
|
||||||
|
opacity: 0
|
||||||
|
transform: translateY(20px)
|
||||||
|
100%
|
||||||
|
opacity: 1
|
||||||
|
transform: translateY(0)
|
||||||
|
|
||||||
|
@keyframes pulse
|
||||||
|
0%, 100%
|
||||||
|
opacity: 0.8
|
||||||
|
transform: scale(1)
|
||||||
|
50%
|
||||||
|
opacity: 1
|
||||||
|
transform: scale(1.05)
|
@ -9,10 +9,16 @@ import {useContext, useState, useEffect} from "react";
|
|||||||
|
|
||||||
export const Home = () => {
|
export const Home = () => {
|
||||||
const {setCurrentState} = useContext(StateContext);
|
const {setCurrentState} = useContext(StateContext);
|
||||||
const {connect, send, on} = useContext(SocketContext);
|
const {connect, send, on, connected} = useContext(SocketContext);
|
||||||
const [homeState, setHomeState] = useState("initial"); // initial, joining, creating
|
const [homeState, setHomeState] = useState("initial"); // initial, joining, creating
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!connected) {
|
||||||
|
connect();
|
||||||
|
}
|
||||||
|
}, [connected, connect]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
connect();
|
connect();
|
||||||
|
|
||||||
|
209
server/controller/game.js
Normal file
209
server/controller/game.js
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
const roomController = require('./room');
|
||||||
|
|
||||||
|
const SONGS = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: "Black Steam",
|
||||||
|
artist: "Carrot Quest GmbH",
|
||||||
|
coverUrl: "https://mir-s3-cdn-cf.behance.net/project_modules/1400/fe529a64193929.5aca8500ba9ab.jpg"
|
||||||
|
},
|
||||||
|
{id: 2, title: "Sunset Dreams", artist: "Ocean Waves", coverUrl: "https://place-hold.it/500x500/"},
|
||||||
|
{id: 3, title: "Neon Nights", artist: "Electric Avenue", coverUrl: "https://place-hold.it/500x500/"},
|
||||||
|
{id: 4, title: "Mountain Echo", artist: "Wild Terrain", coverUrl: "https://place-hold.it/500x500/"},
|
||||||
|
{id: 5, title: "Urban Jungle", artist: "City Dwellers", coverUrl: "https://place-hold.it/500x500/"},
|
||||||
|
{id: 6, title: "Cosmic Journey", artist: "Stargazers", coverUrl: "https://place-hold.it/500x500/"}
|
||||||
|
];
|
||||||
|
|
||||||
|
const gameStates = {};
|
||||||
|
|
||||||
|
const initializeGameState = (roomId) => {
|
||||||
|
if (!gameStates[roomId]) {
|
||||||
|
gameStates[roomId] = {
|
||||||
|
round: 0,
|
||||||
|
phase: 'waiting',
|
||||||
|
roles: {},
|
||||||
|
selectedSong: null,
|
||||||
|
scores: {},
|
||||||
|
songOptions: [],
|
||||||
|
guessResults: {},
|
||||||
|
currentComposer: null,
|
||||||
|
roundStartTime: null,
|
||||||
|
phaseTimeLimit: {
|
||||||
|
composing: 30,
|
||||||
|
guessing: 30
|
||||||
|
},
|
||||||
|
lastFrequency: 440,
|
||||||
|
};
|
||||||
|
|
||||||
|
const users = roomController.getRoomUsers(roomId);
|
||||||
|
users.forEach(user => {
|
||||||
|
gameStates[roomId].scores[user.id] = 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return gameStates[roomId];
|
||||||
|
};
|
||||||
|
|
||||||
|
const startNewRound = (roomId) => {
|
||||||
|
const gameState = gameStates[roomId];
|
||||||
|
if (!gameState) return false;
|
||||||
|
|
||||||
|
const users = roomController.getRoomUsers(roomId);
|
||||||
|
if (users.length < 2) return false;
|
||||||
|
|
||||||
|
gameState.round += 1;
|
||||||
|
gameState.phase = 'composing';
|
||||||
|
gameState.guessResults = {};
|
||||||
|
gameState.roundStartTime = Date.now();
|
||||||
|
gameState.roles = {};
|
||||||
|
|
||||||
|
gameState.currentComposer = determineNextComposer(roomId, gameState, users);
|
||||||
|
|
||||||
|
users.forEach(user => {
|
||||||
|
gameState.roles[user.id] = user.id === gameState.currentComposer ? 'composer' : 'guesser';
|
||||||
|
});
|
||||||
|
|
||||||
|
selectSongAndOptions(gameState);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const determineNextComposer = (roomId, gameState, users) => {
|
||||||
|
if (gameState.round === 1 || !gameState.currentComposer) {
|
||||||
|
return users[Math.floor(Math.random() * users.length)].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentIndex = users.findIndex(user => user.id === gameState.currentComposer);
|
||||||
|
|
||||||
|
if (currentIndex === -1) {
|
||||||
|
return users[Math.floor(Math.random() * users.length)].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return users[(currentIndex + 1) % users.length].id;
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectSongAndOptions = (gameState) => {
|
||||||
|
gameState.selectedSong = SONGS[Math.floor(Math.random() * SONGS.length)];
|
||||||
|
|
||||||
|
const songOptions = [...SONGS].sort(() => Math.random() - 0.5).slice(0, 5);
|
||||||
|
|
||||||
|
if (!songOptions.includes(gameState.selectedSong)) {
|
||||||
|
songOptions[Math.floor(Math.random() * songOptions.length)] = gameState.selectedSong;
|
||||||
|
}
|
||||||
|
|
||||||
|
gameState.songOptions = songOptions;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTimeRemaining = (roomId) => {
|
||||||
|
const gameState = gameStates[roomId];
|
||||||
|
if (!gameState || !gameState.roundStartTime) return 0;
|
||||||
|
|
||||||
|
const phaseDuration = gameState.phaseTimeLimit[gameState.phase] * 1000;
|
||||||
|
const timeElapsed = Date.now() - gameState.roundStartTime;
|
||||||
|
const timeRemaining = Math.max(0, phaseDuration - timeElapsed);
|
||||||
|
|
||||||
|
return Math.ceil(timeRemaining / 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const advancePhase = (roomId) => {
|
||||||
|
const gameState = gameStates[roomId];
|
||||||
|
if (!gameState) {
|
||||||
|
console.error(`Cannot advance phase: no game state for room ${roomId}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentPhase = gameState.phase;
|
||||||
|
console.log(`Advancing phase for room ${roomId} from ${currentPhase}`);
|
||||||
|
|
||||||
|
if (currentPhase === 'composing') {
|
||||||
|
gameState.phase = 'guessing';
|
||||||
|
gameState.roundStartTime = Date.now();
|
||||||
|
console.log(`Room ${roomId} advanced to guessing phase`);
|
||||||
|
return { phase: 'guessing' };
|
||||||
|
}
|
||||||
|
else if (currentPhase === 'guessing') {
|
||||||
|
gameState.phase = 'results';
|
||||||
|
console.log(`Room ${roomId} advanced to results phase`);
|
||||||
|
return { phase: 'results' };
|
||||||
|
}
|
||||||
|
else if (currentPhase === 'results') {
|
||||||
|
console.log(`Room ${roomId} starting new round from results phase`);
|
||||||
|
return startNewRound(roomId);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn(`Cannot advance from unknown phase "${currentPhase}" in room ${roomId}`);
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateFrequency = (roomId, frequency) => {
|
||||||
|
if (!gameStates[roomId]) return false;
|
||||||
|
gameStates[roomId].lastFrequency = frequency;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitGuess = (roomId, userId, songId) => {
|
||||||
|
const gameState = gameStates[roomId];
|
||||||
|
if (!gameState || gameState.phase !== 'guessing' || gameState.roles[userId] !== 'guesser') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isCorrect = gameState.selectedSong.id === parseInt(songId);
|
||||||
|
const points = isCorrect ? 10 : 0;
|
||||||
|
|
||||||
|
gameState.guessResults[userId] = {songId, isCorrect, points};
|
||||||
|
|
||||||
|
if (isCorrect) {
|
||||||
|
gameState.scores[userId] = (gameState.scores[userId] || 0) + points;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isCorrect,
|
||||||
|
correctSong: gameState.selectedSong,
|
||||||
|
points
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCurrentFrequency = (roomId) => gameStates[roomId]?.lastFrequency || 440;
|
||||||
|
const getRoles = (roomId) => gameStates[roomId]?.roles || {};
|
||||||
|
const getUserRole = (roomId, userId) => gameStates[roomId]?.roles[userId] || null;
|
||||||
|
const getSongOptions = (roomId) => gameStates[roomId]?.songOptions || [];
|
||||||
|
const getSelectedSong = (roomId) => gameStates[roomId]?.selectedSong || null;
|
||||||
|
const getRoundResults = (roomId) => {
|
||||||
|
const gameState = gameStates[roomId];
|
||||||
|
if (!gameState) return {round: 0, scores: {}, guessResults: {}, selectedSong: null};
|
||||||
|
|
||||||
|
return {
|
||||||
|
round: gameState.round,
|
||||||
|
selectedSong: gameState.selectedSong,
|
||||||
|
guessResults: gameState.guessResults,
|
||||||
|
scores: gameState.scores
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleanupGameState = (roomId) => {
|
||||||
|
delete gameStates[roomId];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCurrentComposer = (roomId) => gameStates[roomId]?.currentComposer || null;
|
||||||
|
|
||||||
|
const getGameState = (roomId) => {
|
||||||
|
return gameStates[roomId] || null;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
initializeGameState,
|
||||||
|
startNewRound,
|
||||||
|
getTimeRemaining,
|
||||||
|
advancePhase,
|
||||||
|
updateFrequency,
|
||||||
|
getCurrentFrequency,
|
||||||
|
submitGuess,
|
||||||
|
getRoundResults,
|
||||||
|
getRoles,
|
||||||
|
getUserRole,
|
||||||
|
getSongOptions,
|
||||||
|
getSelectedSong,
|
||||||
|
cleanupGameState,
|
||||||
|
getCurrentComposer,
|
||||||
|
getGameState
|
||||||
|
};
|
@ -1,3 +1,9 @@
|
|||||||
|
let cleanupGameState;
|
||||||
|
|
||||||
|
const setCleanupGameState = (cleanupFunction) => {
|
||||||
|
cleanupGameState = cleanupFunction;
|
||||||
|
};
|
||||||
|
|
||||||
let rooms = {};
|
let rooms = {};
|
||||||
|
|
||||||
module.exports.roomExists = (roomId) => rooms[roomId] !== undefined;
|
module.exports.roomExists = (roomId) => rooms[roomId] !== undefined;
|
||||||
@ -76,6 +82,7 @@ module.exports.disconnectUser = (userId) => {
|
|||||||
|
|
||||||
if (memberIndex !== -1) {
|
if (memberIndex !== -1) {
|
||||||
if (room.members[memberIndex].creator && room.members.length > 1) {
|
if (room.members[memberIndex].creator && room.members.length > 1) {
|
||||||
|
// Transfer host status to the next user
|
||||||
room.members[1].creator = true;
|
room.members[1].creator = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,6 +90,9 @@ module.exports.disconnectUser = (userId) => {
|
|||||||
console.log(`User ${userId} disconnected from room ${roomId}`);
|
console.log(`User ${userId} disconnected from room ${roomId}`);
|
||||||
|
|
||||||
if (room.members.length === 0) {
|
if (room.members.length === 0) {
|
||||||
|
if (cleanupGameState) {
|
||||||
|
cleanupGameState(roomId);
|
||||||
|
}
|
||||||
delete rooms[roomId];
|
delete rooms[roomId];
|
||||||
console.log(`Room ${roomId} deleted because it's empty`);
|
console.log(`Room ${roomId} deleted because it's empty`);
|
||||||
}
|
}
|
||||||
@ -90,4 +100,36 @@ module.exports.disconnectUser = (userId) => {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module.exports.validateRoomMembers = (io, roomId) => {
|
||||||
|
if (!rooms[roomId]) return [];
|
||||||
|
|
||||||
|
const validMembers = [];
|
||||||
|
const connectedSockets = io.sockets.adapter.rooms.get(roomId) || new Set();
|
||||||
|
|
||||||
|
rooms[roomId].members = rooms[roomId].members.filter(member => {
|
||||||
|
const stillConnected = connectedSockets.has(member.id);
|
||||||
|
if (stillConnected) {
|
||||||
|
validMembers.push(member);
|
||||||
|
} else {
|
||||||
|
console.log(`Removing disconnected user ${member.name} (${member.id}) from room ${roomId}`);
|
||||||
|
}
|
||||||
|
return stillConnected;
|
||||||
|
});
|
||||||
|
|
||||||
|
return validMembers;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.setCleanupGameState = setCleanupGameState;
|
||||||
|
|
||||||
|
module.exports.getUserName = (userId) => {
|
||||||
|
for (const roomId in rooms) {
|
||||||
|
const room = rooms[roomId];
|
||||||
|
const member = room.members.find(m => m.id === userId);
|
||||||
|
if (member) {
|
||||||
|
return member.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
@ -1,23 +1,114 @@
|
|||||||
const {
|
const roomController = require("../controller/room");
|
||||||
connectUserToRoom,
|
const gameController = require("../controller/game");
|
||||||
roomExists,
|
|
||||||
disconnectUser,
|
|
||||||
getUserRoom,
|
|
||||||
getRoomUsers,
|
|
||||||
isRoomOpen,
|
|
||||||
startGame,
|
|
||||||
isUserHost
|
|
||||||
} = require("../controller/room");
|
|
||||||
|
|
||||||
module.exports = (io) => (socket) => {
|
module.exports = (io) => (socket) => {
|
||||||
let currentRoomId = null;
|
let currentRoomId = null;
|
||||||
let currentUser = null;
|
let currentUser = null;
|
||||||
|
let phaseTimers = {};
|
||||||
|
|
||||||
|
const clearRoomTimers = (roomId) => {
|
||||||
|
if (phaseTimers[roomId]) {
|
||||||
|
console.log(`Clearing timer for room ${roomId}`);
|
||||||
|
clearTimeout(phaseTimers[roomId]);
|
||||||
|
delete phaseTimers[roomId];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const startPhaseTimer = (roomId) => {
|
||||||
|
clearRoomTimers(roomId);
|
||||||
|
|
||||||
|
const gameState = gameController.getGameState(roomId);
|
||||||
|
if (!gameState) {
|
||||||
|
console.error(`Cannot start timer: no game state for room ${roomId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeRemaining = gameController.getTimeRemaining(roomId) * 1000;
|
||||||
|
console.log(`Starting ${gameState.phase} phase timer for room ${roomId} with ${timeRemaining}ms`);
|
||||||
|
|
||||||
|
phaseTimers[roomId] = setTimeout(() => {
|
||||||
|
console.log(`Timer expired for room ${roomId}, advancing phase from ${gameState.phase}`);
|
||||||
|
const advanced = gameController.advancePhase(roomId);
|
||||||
|
|
||||||
|
if (!advanced) {
|
||||||
|
console.log(`Failed to advance phase for room ${roomId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof advanced === 'boolean') {
|
||||||
|
handleRoundStart(roomId);
|
||||||
|
} else {
|
||||||
|
const newPhase = gameController.getGameState(roomId).phase;
|
||||||
|
console.log(`Advanced to ${newPhase} phase in room ${roomId}`);
|
||||||
|
|
||||||
|
if (newPhase === 'guessing') {
|
||||||
|
handleGuessingPhaseStart(roomId);
|
||||||
|
} else if (newPhase === 'results') {
|
||||||
|
handleResultsPhaseStart(roomId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, timeRemaining);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRoundStart = (roomId) => {
|
||||||
|
const roles = gameController.getRoles(roomId);
|
||||||
|
const selectedSong = gameController.getSelectedSong(roomId);
|
||||||
|
const timeLeft = gameController.getTimeRemaining(roomId);
|
||||||
|
|
||||||
|
io.to(roomId).emit('roles-assigned', roles);
|
||||||
|
|
||||||
|
Object.entries(roles).forEach(([userId, role]) => {
|
||||||
|
if (role === 'composer') {
|
||||||
|
io.to(userId).emit('song-selected', selectedSong);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
io.to(roomId).emit('round-started', {
|
||||||
|
round: gameController.getRoundResults(roomId).round,
|
||||||
|
timeRemaining: timeLeft
|
||||||
|
});
|
||||||
|
|
||||||
|
startPhaseTimer(roomId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGuessingPhaseStart = (roomId) => {
|
||||||
|
const gameState = gameController.getGameState(roomId);
|
||||||
|
if (!gameState) return;
|
||||||
|
|
||||||
|
const roles = gameController.getRoles(roomId);
|
||||||
|
const songOptions = gameController.getSongOptions(roomId);
|
||||||
|
const timeLeft = gameController.getTimeRemaining(roomId);
|
||||||
|
|
||||||
|
console.log(`Starting guessing phase for room ${roomId} with ${Object.keys(roles).length} players`);
|
||||||
|
|
||||||
|
io.to(roomId).emit('phase-changed', {
|
||||||
|
phase: 'guessing',
|
||||||
|
timeRemaining: timeLeft
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.entries(roles).forEach(([userId, role]) => {
|
||||||
|
if (role === 'guesser') {
|
||||||
|
io.to(userId).emit('guessing-phase-started', {
|
||||||
|
timeRemaining: timeLeft,
|
||||||
|
songOptions
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
startPhaseTimer(roomId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResultsPhaseStart = (roomId) => {
|
||||||
|
const results = gameController.getRoundResults(roomId);
|
||||||
|
io.to(roomId).emit('round-results', results);
|
||||||
|
};
|
||||||
|
|
||||||
socket.on("disconnect", () => {
|
socket.on("disconnect", () => {
|
||||||
const roomId = getUserRoom(socket.id);
|
const roomId = roomController.getUserRoom(socket.id);
|
||||||
if (roomId) socket.to(roomId).emit("user-disconnected", socket.id);
|
if (roomId) {
|
||||||
|
socket.to(roomId).emit("user-disconnected", socket.id);
|
||||||
disconnectUser(socket.id);
|
roomController.disconnectUser(socket.id);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("join-room", ({roomId, name}) => {
|
socket.on("join-room", ({roomId, name}) => {
|
||||||
@ -25,18 +116,17 @@ module.exports = (io) => (socket) => {
|
|||||||
|
|
||||||
roomId = roomId.toString().toUpperCase();
|
roomId = roomId.toString().toUpperCase();
|
||||||
|
|
||||||
if (roomExists(roomId)) {
|
if (roomController.roomExists(roomId)) {
|
||||||
if (!isRoomOpen(roomId)) {
|
if (!roomController.isRoomOpen(roomId)) {
|
||||||
return socket.emit("room-closed", roomId);
|
return socket.emit("room-closed", roomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
currentUser = {id: socket.id, name: name.toString()};
|
currentUser = {id: socket.id, name: name.toString()};
|
||||||
connectUserToRoom(roomId, currentUser);
|
roomController.connectUserToRoom(roomId, currentUser);
|
||||||
socket.join(roomId);
|
socket.join(roomId);
|
||||||
|
|
||||||
const users = getRoomUsers(roomId);
|
const users = roomController.getRoomUsers(roomId);
|
||||||
io.to(roomId).emit("room-users-update", users);
|
io.to(roomId).emit("room-users-update", users);
|
||||||
|
|
||||||
socket.to(roomId).emit("user-connected", currentUser);
|
socket.to(roomId).emit("user-connected", currentUser);
|
||||||
|
|
||||||
socket.emit("room-joined", roomId);
|
socket.emit("room-joined", roomId);
|
||||||
@ -48,54 +138,123 @@ module.exports = (io) => (socket) => {
|
|||||||
|
|
||||||
socket.on("create-room", ({name}) => {
|
socket.on("create-room", ({name}) => {
|
||||||
if (!name) return socket.emit("room-name-required");
|
if (!name) return socket.emit("room-name-required");
|
||||||
|
|
||||||
const roomId = Math.random().toString(36).substring(7).toUpperCase();
|
const roomId = Math.random().toString(36).substring(7).toUpperCase();
|
||||||
currentUser = {id: socket.id, name: name?.toString()};
|
currentUser = {id: socket.id, name: name?.toString(), creator: true};
|
||||||
connectUserToRoom(roomId, currentUser);
|
roomController.connectUserToRoom(roomId, currentUser);
|
||||||
socket.join(roomId);
|
socket.join(roomId);
|
||||||
socket.emit("room-created", roomId);
|
socket.emit("room-created", roomId);
|
||||||
currentRoomId = roomId;
|
currentRoomId = roomId;
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("start-game", () => {
|
socket.on("start-game", () => {
|
||||||
const roomId = getUserRoom(socket.id);
|
const roomId = roomController.getUserRoom(socket.id);
|
||||||
if (roomId && isUserHost(socket.id)) {
|
if (!roomId || !roomController.isUserHost(socket.id)) {
|
||||||
if (startGame(roomId)) {
|
return socket.emit("not-authorized");
|
||||||
io.to(roomId).emit("game-started");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
socket.emit("not-authorized");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
roomController.validateRoomMembers(io, roomId);
|
||||||
|
const users = roomController.getRoomUsers(roomId);
|
||||||
|
|
||||||
|
if (users.length < 2) {
|
||||||
|
return socket.emit("error", { message: "At least 2 players are required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!roomController.startGame(roomId)) return;
|
||||||
|
gameController.initializeGameState(roomId);
|
||||||
|
if (!gameController.startNewRound(roomId)) return;
|
||||||
|
|
||||||
|
io.to(roomId).emit("game-started");
|
||||||
|
handleRoundStart(roomId);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("send-message", (messageData) => {
|
socket.on("send-message", (messageData) => {
|
||||||
const roomId = getUserRoom(socket.id);
|
const roomId = roomController.getUserRoom(socket.id);
|
||||||
if (roomId) {
|
if (roomId) socket.to(roomId).emit("chat-message", messageData);
|
||||||
socket.to(roomId).emit("chat-message", messageData);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("get-user-info", () => {
|
socket.on("get-user-info", () => {
|
||||||
if (currentUser) {
|
if (currentUser) socket.emit("user-info", currentUser);
|
||||||
socket.emit("user-info", currentUser);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("get-room-users", () => {
|
socket.on("get-room-users", () => {
|
||||||
const roomId = getUserRoom(socket.id);
|
const roomId = roomController.getUserRoom(socket.id);
|
||||||
if (roomId) {
|
if (roomId) {
|
||||||
const users = getRoomUsers(roomId);
|
const users = roomController.getRoomUsers(roomId);
|
||||||
socket.emit("room-users", users);
|
socket.emit("room-users", users);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("check-host-status", () => {
|
socket.on("check-host-status", () => {
|
||||||
const isHost = isUserHost(socket.id);
|
socket.emit("host-status", { isHost: roomController.isUserHost(socket.id) });
|
||||||
socket.emit("host-status", { isHost });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("get-room-code", () => {
|
socket.on("get-room-code", () => {
|
||||||
if (currentRoomId) {
|
if (currentRoomId) socket.emit("room-code", currentRoomId);
|
||||||
socket.emit("room-code", currentRoomId);
|
});
|
||||||
|
|
||||||
|
socket.on("submit-frequency", ({ frequency }) => {
|
||||||
|
const roomId = roomController.getUserRoom(socket.id);
|
||||||
|
if (!roomId) return;
|
||||||
|
|
||||||
|
const userRole = gameController.getUserRole(roomId, socket.id);
|
||||||
|
if (userRole !== 'composer') return;
|
||||||
|
|
||||||
|
if (gameController.updateFrequency(roomId, frequency)) {
|
||||||
|
socket.to(roomId).emit("frequency-update", { frequency });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
socket.on("submit-guess", ({ songId }) => {
|
||||||
|
const roomId = roomController.getUserRoom(socket.id);
|
||||||
|
if (!roomId) return;
|
||||||
|
|
||||||
|
console.log(`User ${socket.id} submitted guess: Song ID ${songId}`);
|
||||||
|
|
||||||
|
const gamePhase = gameController.getGameState(roomId)?.phase;
|
||||||
|
if (gamePhase !== 'guessing') {
|
||||||
|
console.log(`Ignoring guess: room ${roomId} is in ${gamePhase} phase, not guessing`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = gameController.submitGuess(roomId, socket.id, songId);
|
||||||
|
if (result) {
|
||||||
|
console.log(`Guess result for ${socket.id}:`, result);
|
||||||
|
socket.emit("guess-result", result);
|
||||||
|
|
||||||
|
const currentComposer = gameController.getCurrentComposer(roomId);
|
||||||
|
if (currentComposer) {
|
||||||
|
const guesserName = roomController.getUserName(socket.id) || "Someone";
|
||||||
|
io.to(currentComposer).emit("player-guessed", {
|
||||||
|
guesserName,
|
||||||
|
isCorrect: result.isCorrect
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("next-round", () => {
|
||||||
|
const roomId = roomController.getUserRoom(socket.id);
|
||||||
|
if (!roomId || !roomController.isUserHost(socket.id)) return;
|
||||||
|
|
||||||
|
roomController.validateRoomMembers(io, roomId);
|
||||||
|
const users = roomController.getRoomUsers(roomId);
|
||||||
|
|
||||||
|
if (users.length < 2) {
|
||||||
|
return socket.emit("error", { message: "At least 2 players are required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gameController.startNewRound(roomId)) {
|
||||||
|
handleRoundStart(roomId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("get-current-frequency", () => {
|
||||||
|
const roomId = roomController.getUserRoom(socket.id);
|
||||||
|
if (roomId) {
|
||||||
|
socket.emit("current-frequency", {
|
||||||
|
frequency: gameController.getCurrentFrequency(roomId)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@ -5,17 +5,38 @@ const app = express();
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
app.use(express.static(path.join(__dirname, './dist')));
|
app.use(express.static(path.join(__dirname, './dist')));
|
||||||
|
app.disable("x-powered-by");
|
||||||
|
|
||||||
app.get('*', (req, res) => res.sendFile(path.join(__dirname, './dist', 'index.html')));
|
app.get('*', (req, res) => res.sendFile(path.join(__dirname, './dist', 'index.html')));
|
||||||
|
|
||||||
app.disable("x-powered-by");
|
|
||||||
|
|
||||||
const server = http.createServer(app);
|
const server = http.createServer(app);
|
||||||
const io = new Server(server, {cors: {origin: "*"}});
|
|
||||||
|
|
||||||
// Pass io to the connection handler
|
const io = new Server(server, {
|
||||||
|
cors: {origin: "*"},
|
||||||
|
pingTimeout: 30000,
|
||||||
|
pingInterval: 10000
|
||||||
|
});
|
||||||
|
|
||||||
|
const roomController = require('./controller/room');
|
||||||
|
const gameController = require('./controller/game');
|
||||||
|
roomController.setCleanupGameState(gameController.cleanupGameState);
|
||||||
|
|
||||||
|
// Handle socket connections
|
||||||
io.on("connection", require("./handler/connection")(io));
|
io.on("connection", require("./handler/connection")(io));
|
||||||
|
|
||||||
server.listen(5287, () => {
|
server.on('error', (error) => {
|
||||||
console.log("Server running on port 5287");
|
console.error('Server error:', error);
|
||||||
|
});
|
||||||
|
|
||||||
|
const PORT = process.env.PORT || 5287;
|
||||||
|
server.listen(PORT, () => {
|
||||||
|
console.log(`Server running on port ${PORT}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('SIGINT', () => {
|
||||||
|
console.log('Server shutting down...');
|
||||||
|
server.close(() => {
|
||||||
|
console.log('Server closed');
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
});
|
});
|
Loading…
x
Reference in New Issue
Block a user