Create Ending
All checks were successful
Publish Docker image / Push Docker image to Docker Hub (push) Successful in 1m36s
All checks were successful
Publish Docker image / Push Docker image to Docker Hub (push) Successful in 1m36s
This commit is contained in:
parent
09fb1e3938
commit
032ebc2368
@ -5,6 +5,7 @@ import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
|||||||
import {faDrum, faGuitar, faHeadphones, faMusic} from "@fortawesome/free-solid-svg-icons";
|
import {faDrum, faGuitar, faHeadphones, faMusic} from "@fortawesome/free-solid-svg-icons";
|
||||||
import Home from "@/pages/Home";
|
import Home from "@/pages/Home";
|
||||||
import WaitingRoom from "@/pages/WaitingRoom";
|
import WaitingRoom from "@/pages/WaitingRoom";
|
||||||
|
import Ending from "@/pages/Ending";
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const {currentState} = useContext(StateContext);
|
const {currentState} = useContext(StateContext);
|
||||||
@ -63,6 +64,7 @@ const App = () => {
|
|||||||
{currentState === "WaitingRoom" && <WaitingRoom />}
|
{currentState === "WaitingRoom" && <WaitingRoom />}
|
||||||
{currentState === "Home" && <Home />}
|
{currentState === "Home" && <Home />}
|
||||||
{currentState === "Game" && <Game />}
|
{currentState === "Game" && <Game />}
|
||||||
|
{currentState === "Ending" && <Ending />}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
72
client/src/pages/Ending/Ending.jsx
Normal file
72
client/src/pages/Ending/Ending.jsx
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import { useEffect, useState, useContext } from "react";
|
||||||
|
import { StateContext } from "@/common/contexts/StateContext";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { faTrophy, faMedal, faAward, faHome, faCrown } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import "./styles.sass";
|
||||||
|
|
||||||
|
export const Ending = () => {
|
||||||
|
const { setCurrentState } = useContext(StateContext);
|
||||||
|
const [finalScores, setFinalScores] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const savedData = JSON.parse(localStorage.getItem('finalScores') || '{"scores":{}}');
|
||||||
|
const sortedScores = Object.entries(savedData.scores)
|
||||||
|
.map(([userId, data]) => ({
|
||||||
|
id: userId,
|
||||||
|
name: data.name,
|
||||||
|
score: data.score
|
||||||
|
}))
|
||||||
|
.sort((a, b) => b.score - a.score);
|
||||||
|
setFinalScores(sortedScores);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getPlayerIcon = (index) => {
|
||||||
|
switch(index) {
|
||||||
|
case 0: return <FontAwesomeIcon icon={faTrophy} className="gold" />;
|
||||||
|
case 1: return <FontAwesomeIcon icon={faMedal} className="silver" />;
|
||||||
|
case 2: return <FontAwesomeIcon icon={faAward} className="bronze" />;
|
||||||
|
default: return <FontAwesomeIcon icon={faCrown} className="normal" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReturnHome = () => {
|
||||||
|
localStorage.removeItem('finalScores');
|
||||||
|
setCurrentState("Home");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="ending-page">
|
||||||
|
<div className="background-overlay">
|
||||||
|
<div className="rotating-gradient"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="ending-content">
|
||||||
|
<h1>Spiel beendet!</h1>
|
||||||
|
<div className="final-scores">
|
||||||
|
<h2>Endstand</h2>
|
||||||
|
<div className="leaderboard">
|
||||||
|
{finalScores.map((player, index) => (
|
||||||
|
<div
|
||||||
|
key={player.id}
|
||||||
|
className={`leaderboard-entry ${index < 3 ? `top-${index + 1}` : ''}`}
|
||||||
|
>
|
||||||
|
<div className="rank-icon">
|
||||||
|
{getPlayerIcon(index)}
|
||||||
|
</div>
|
||||||
|
<div className="player-info">
|
||||||
|
<span className="player-name">{player.name}</span>
|
||||||
|
<span className="player-score">{player.score} Punkte</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button className="return-home" onClick={handleReturnHome}>
|
||||||
|
<FontAwesomeIcon icon={faHome} />
|
||||||
|
Zurück zum Hauptmenü
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
1
client/src/pages/Ending/index.js
Normal file
1
client/src/pages/Ending/index.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export {Ending as default} from "./Ending.jsx";
|
128
client/src/pages/Ending/styles.sass
Normal file
128
client/src/pages/Ending/styles.sass
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
@import "@/common/styles/colors"
|
||||||
|
|
||||||
|
.ending-page
|
||||||
|
height: 100vh
|
||||||
|
width: 100vw
|
||||||
|
display: flex
|
||||||
|
justify-content: center
|
||||||
|
align-items: center
|
||||||
|
position: relative
|
||||||
|
|
||||||
|
.ending-content
|
||||||
|
text-align: center
|
||||||
|
z-index: 2
|
||||||
|
animation: float-up 0.8s ease-out
|
||||||
|
|
||||||
|
h1
|
||||||
|
font-size: 48pt
|
||||||
|
color: $white
|
||||||
|
margin-bottom: 40px
|
||||||
|
background: linear-gradient(135deg, $yellow, $pink)
|
||||||
|
-webkit-background-clip: text
|
||||||
|
background-clip: text
|
||||||
|
-webkit-text-fill-color: transparent
|
||||||
|
animation: title-shimmer 3s infinite alternate ease-in-out
|
||||||
|
|
||||||
|
.final-scores
|
||||||
|
background: rgba(255, 255, 255, 0.1)
|
||||||
|
backdrop-filter: blur(10px)
|
||||||
|
border-radius: 20px
|
||||||
|
padding: 30px
|
||||||
|
margin-bottom: 30px
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2)
|
||||||
|
|
||||||
|
h2
|
||||||
|
color: $white
|
||||||
|
margin-bottom: 20px
|
||||||
|
font-size: 24pt
|
||||||
|
|
||||||
|
.leaderboard
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
gap: 15px
|
||||||
|
|
||||||
|
.leaderboard-entry
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
padding: 15px 20px
|
||||||
|
background: rgba(255, 255, 255, 0.05)
|
||||||
|
border-radius: 15px
|
||||||
|
transition: all 0.3s ease
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1)
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
transform: translateY(-2px)
|
||||||
|
background: rgba(255, 255, 255, 0.1)
|
||||||
|
|
||||||
|
&.top-1
|
||||||
|
background: linear-gradient(135deg, rgba(255, 215, 0, 0.2), rgba(255, 215, 0, 0.1))
|
||||||
|
border-color: rgba(255, 215, 0, 0.3)
|
||||||
|
transform: scale(1.05)
|
||||||
|
|
||||||
|
.rank-icon
|
||||||
|
color: #FFD700
|
||||||
|
filter: drop-shadow(0 0 10px rgba(255, 215, 0, 0.5))
|
||||||
|
|
||||||
|
&.top-2
|
||||||
|
background: linear-gradient(135deg, rgba(192, 192, 192, 0.2), rgba(192, 192, 192, 0.1))
|
||||||
|
border-color: rgba(192, 192, 192, 0.3)
|
||||||
|
|
||||||
|
.rank-icon
|
||||||
|
color: #C0C0C0
|
||||||
|
filter: drop-shadow(0 0 10px rgba(192, 192, 192, 0.5))
|
||||||
|
|
||||||
|
&.top-3
|
||||||
|
background: linear-gradient(135deg, rgba(205, 127, 50, 0.2), rgba(205, 127, 50, 0.1))
|
||||||
|
border-color: rgba(205, 127, 50, 0.3)
|
||||||
|
|
||||||
|
.rank-icon
|
||||||
|
color: #CD7F32
|
||||||
|
filter: drop-shadow(0 0 10px rgba(205, 127, 50, 0.5))
|
||||||
|
|
||||||
|
.rank-icon
|
||||||
|
font-size: 24pt
|
||||||
|
margin-right: 20px
|
||||||
|
|
||||||
|
.player-info
|
||||||
|
flex: 1
|
||||||
|
display: flex
|
||||||
|
justify-content: space-between
|
||||||
|
align-items: center
|
||||||
|
|
||||||
|
.player-name
|
||||||
|
color: $white
|
||||||
|
font-size: 18pt
|
||||||
|
|
||||||
|
.player-score
|
||||||
|
color: $yellow
|
||||||
|
font-size: 16pt
|
||||||
|
font-weight: bold
|
||||||
|
|
||||||
|
.return-home
|
||||||
|
padding: 15px 30px
|
||||||
|
background: linear-gradient(135deg, $purple, $blue)
|
||||||
|
border: none
|
||||||
|
border-radius: 12px
|
||||||
|
color: $white
|
||||||
|
font-size: 16pt
|
||||||
|
cursor: pointer
|
||||||
|
transition: all 0.3s ease
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
gap: 10px
|
||||||
|
margin: 0 auto
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
transform: translateY(-3px)
|
||||||
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.4)
|
||||||
|
|
||||||
|
svg
|
||||||
|
font-size: 18pt
|
||||||
|
|
||||||
|
@keyframes float-up
|
||||||
|
0%
|
||||||
|
opacity: 0
|
||||||
|
transform: translateY(30px)
|
||||||
|
100%
|
||||||
|
opacity: 1
|
||||||
|
transform: translateY(0)
|
@ -1,5 +1,6 @@
|
|||||||
import "./styles.sass";
|
import "./styles.sass";
|
||||||
import {SocketContext} from "@/common/contexts/SocketContext";
|
import {SocketContext} from "@/common/contexts/SocketContext";
|
||||||
|
import {StateContext} from "@/common/contexts/StateContext";
|
||||||
import {useContext, useState, useEffect, useRef, useCallback} 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";
|
||||||
@ -10,15 +11,14 @@ import {
|
|||||||
faClock,
|
faClock,
|
||||||
faCrown,
|
faCrown,
|
||||||
faPaperPlane,
|
faPaperPlane,
|
||||||
faCheckCircle,
|
faCheckCircle
|
||||||
faVolumeUp,
|
|
||||||
faStepForward
|
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { fetchPlaylistSongs } from "@/services/youtubeService.js";
|
import { fetchPlaylistSongs } from "@/services/youtubeService.js";
|
||||||
import YouTubePlayer from "../../components/YouTubePlayer/YouTubePlayer";
|
import YouTubePlayer from "../../components/YouTubePlayer/YouTubePlayer";
|
||||||
|
|
||||||
export const Game = () => {
|
export const Game = () => {
|
||||||
const {send, on, socket, connected, connect} = useContext(SocketContext);
|
const {send, on, socket, connected, connect} = useContext(SocketContext);
|
||||||
|
const {setCurrentState} = useContext(StateContext);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!connected) {
|
if (!connected) {
|
||||||
@ -69,12 +69,14 @@ export const Game = () => {
|
|||||||
},
|
},
|
||||||
"song-selected": setCurrentSong,
|
"song-selected": setCurrentSong,
|
||||||
"round-started": (data) => {
|
"round-started": (data) => {
|
||||||
setRound(data.round);
|
requestAnimationFrame(() => {
|
||||||
setPhase("composing");
|
setPhase("composing");
|
||||||
setTimeLeft(data.timeRemaining);
|
setRound(data.round);
|
||||||
setSelectedSong(null);
|
setTimeLeft(data.timeRemaining);
|
||||||
setHasGuessed(false);
|
setSelectedSong(null);
|
||||||
setGuessResult(null);
|
setHasGuessed(false);
|
||||||
|
setGuessResult(null);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
"guessing-phase-started": (data) => {
|
"guessing-phase-started": (data) => {
|
||||||
console.log("Guessing phase started:", data);
|
console.log("Guessing phase started:", data);
|
||||||
@ -133,7 +135,7 @@ export const Game = () => {
|
|||||||
if (phase === "composing") {
|
if (phase === "composing") {
|
||||||
console.log("Received frequency update:", data.frequency);
|
console.log("Received frequency update:", data.frequency);
|
||||||
setFrequency(data.frequency);
|
setFrequency(data.frequency);
|
||||||
setComposerIsPlaying(data.isPlaying); // Make sure isPlaying is handled
|
setComposerIsPlaying(data.isPlaying);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"phase-changed": (data) => {
|
"phase-changed": (data) => {
|
||||||
@ -156,6 +158,10 @@ export const Game = () => {
|
|||||||
console.log("Received song options early:", data);
|
console.log("Received song options early:", data);
|
||||||
setSongOptions(data.songOptions || []);
|
setSongOptions(data.songOptions || []);
|
||||||
},
|
},
|
||||||
|
"game-ended": (finalData) => {
|
||||||
|
setCurrentState("Ending");
|
||||||
|
localStorage.setItem('finalScores', JSON.stringify(finalData));
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const cleanupFunctions = Object.entries(eventHandlers).map(
|
const cleanupFunctions = Object.entries(eventHandlers).map(
|
||||||
@ -168,7 +174,7 @@ export const Game = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return () => cleanupFunctions.forEach(cleanup => cleanup());
|
return () => cleanupFunctions.forEach(cleanup => cleanup());
|
||||||
}, [socket, on, send, role, currentSong, phase, connected, connectedUsers]);
|
}, [socket, on, send, role, currentSong, phase, connected, connectedUsers, setCurrentState, round]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (timerIntervalRef.current) clearInterval(timerIntervalRef.current);
|
if (timerIntervalRef.current) clearInterval(timerIntervalRef.current);
|
||||||
@ -279,10 +285,8 @@ export const Game = () => {
|
|||||||
}, [selectedSong, send, phase]);
|
}, [selectedSong, send, phase]);
|
||||||
|
|
||||||
const handleNextRound = useCallback(() => {
|
const handleNextRound = useCallback(() => {
|
||||||
send("next-round");
|
|
||||||
setSelectedSong(null);
|
|
||||||
setGuessResult(null);
|
|
||||||
setTimeLeft(0);
|
setTimeLeft(0);
|
||||||
|
send("next-round");
|
||||||
}, [send]);
|
}, [send]);
|
||||||
|
|
||||||
const handlePlayerReady = useCallback(() => {
|
const handlePlayerReady = useCallback(() => {
|
||||||
|
@ -44,6 +44,8 @@ const initializeGameState = (roomId) => {
|
|||||||
return gameStates[roomId];
|
return gameStates[roomId];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MAX_ROUNDS = 5;
|
||||||
|
|
||||||
const startNewRound = async (roomId) => {
|
const startNewRound = async (roomId) => {
|
||||||
const gameState = gameStates[roomId];
|
const gameState = gameStates[roomId];
|
||||||
if (!gameState) return false;
|
if (!gameState) return false;
|
||||||
@ -52,6 +54,11 @@ const startNewRound = async (roomId) => {
|
|||||||
if (users.length < 2) return false;
|
if (users.length < 2) return false;
|
||||||
|
|
||||||
gameState.round += 1;
|
gameState.round += 1;
|
||||||
|
|
||||||
|
if (gameState.round > MAX_ROUNDS) {
|
||||||
|
return { gameEnd: true, finalScores: gameState.scores };
|
||||||
|
}
|
||||||
|
|
||||||
gameState.phase = 'composing';
|
gameState.phase = 'composing';
|
||||||
gameState.guessResults = {};
|
gameState.guessResults = {};
|
||||||
gameState.roundStartTime = Date.now();
|
gameState.roundStartTime = Date.now();
|
||||||
@ -83,7 +90,6 @@ const determineNextComposer = (roomId, gameState, users) => {
|
|||||||
return users[(currentIndex + 1) % users.length].id;
|
return users[(currentIndex + 1) % users.length].id;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Then modify the selectSongAndOptions function to use the shuffle:
|
|
||||||
const selectSongAndOptions = async (gameState) => {
|
const selectSongAndOptions = async (gameState) => {
|
||||||
try {
|
try {
|
||||||
console.log(`Fetching songs from playlist: ${gameState.selectedPlaylist}`);
|
console.log(`Fetching songs from playlist: ${gameState.selectedPlaylist}`);
|
||||||
@ -107,7 +113,6 @@ const selectSongAndOptions = async (gameState) => {
|
|||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shuffle the song options to randomize the position of the correct answer
|
|
||||||
gameState.songOptions = shuffleArray(Array.from(optionIds).map(id => ({ id })));
|
gameState.songOptions = shuffleArray(Array.from(optionIds).map(id => ({ id })));
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@ -219,6 +224,27 @@ const getGameState = (roomId) => {
|
|||||||
return gameStates[roomId] || null;
|
return gameStates[roomId] || null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getFinalScores = (roomId) => {
|
||||||
|
const gameState = gameStates[roomId];
|
||||||
|
if (!gameState) return null;
|
||||||
|
|
||||||
|
const users = roomController.getRoomUsers(roomId);
|
||||||
|
const finalScores = {};
|
||||||
|
|
||||||
|
Object.entries(gameState.scores).forEach(([userId, score]) => {
|
||||||
|
const user = users.find(u => u.id === userId);
|
||||||
|
finalScores[userId] = {
|
||||||
|
score: score,
|
||||||
|
name: user?.name || "Player"
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
scores: finalScores,
|
||||||
|
lastRound: gameState.round
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
initializeGameState,
|
initializeGameState,
|
||||||
startNewRound,
|
startNewRound,
|
||||||
@ -234,5 +260,7 @@ module.exports = {
|
|||||||
getSelectedSong,
|
getSelectedSong,
|
||||||
cleanupGameState,
|
cleanupGameState,
|
||||||
getCurrentComposer,
|
getCurrentComposer,
|
||||||
getGameState
|
getGameState,
|
||||||
|
getFinalScores,
|
||||||
|
MAX_ROUNDS
|
||||||
};
|
};
|
||||||
|
@ -109,6 +109,23 @@ module.exports = (io) => (socket) => {
|
|||||||
io.to(roomId).emit('round-results', results);
|
io.to(roomId).emit('round-results', results);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleNextRound = async (roomId) => {
|
||||||
|
try {
|
||||||
|
const result = await gameController.startNewRound(roomId);
|
||||||
|
|
||||||
|
if (result.gameEnd) {
|
||||||
|
const finalData = gameController.getFinalScores(roomId);
|
||||||
|
io.to(roomId).emit('game-ended', finalData);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRoundStart(roomId);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error starting next round:", error);
|
||||||
|
socket.emit("error", { message: "Failed to start next round due to an error" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
socket.on("disconnect", () => {
|
socket.on("disconnect", () => {
|
||||||
const roomId = roomController.getUserRoom(socket.id);
|
const roomId = roomController.getUserRoom(socket.id);
|
||||||
if (roomId) {
|
if (roomId) {
|
||||||
@ -298,17 +315,7 @@ module.exports = (io) => (socket) => {
|
|||||||
return socket.emit("error", { message: "At least 2 players are required" });
|
return socket.emit("error", { message: "At least 2 players are required" });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
await handleNextRound(roomId);
|
||||||
const success = await gameController.startNewRound(roomId);
|
|
||||||
if (success) {
|
|
||||||
handleRoundStart(roomId);
|
|
||||||
} else {
|
|
||||||
socket.emit("error", { message: "Failed to start next round" });
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error starting next round:", error);
|
|
||||||
socket.emit("error", { message: "Failed to start next round due to an error" });
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("get-current-frequency", () => {
|
socket.on("get-current-frequency", () => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user