Create Ending
All checks were successful
Publish Docker image / Push Docker image to Docker Hub (push) Successful in 1m36s

This commit is contained in:
Mathias Wagner 2025-03-01 18:07:38 +01:00
parent 09fb1e3938
commit 032ebc2368
7 changed files with 270 additions and 28 deletions

View File

@ -5,6 +5,7 @@ import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faDrum, faGuitar, faHeadphones, faMusic} from "@fortawesome/free-solid-svg-icons";
import Home from "@/pages/Home";
import WaitingRoom from "@/pages/WaitingRoom";
import Ending from "@/pages/Ending";
const App = () => {
const {currentState} = useContext(StateContext);
@ -63,6 +64,7 @@ const App = () => {
{currentState === "WaitingRoom" && <WaitingRoom />}
{currentState === "Home" && <Home />}
{currentState === "Game" && <Game />}
{currentState === "Ending" && <Ending />}
</>
)
}

View 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>
);
};

View File

@ -0,0 +1 @@
export {Ending as default} from "./Ending.jsx";

View 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)

View File

@ -1,5 +1,6 @@
import "./styles.sass";
import {SocketContext} from "@/common/contexts/SocketContext";
import {StateContext} from "@/common/contexts/StateContext";
import {useContext, useState, useEffect, useRef, useCallback} from "react";
import MusicSlider from "@/pages/Game/components/MusicSlider";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
@ -10,15 +11,14 @@ import {
faClock,
faCrown,
faPaperPlane,
faCheckCircle,
faVolumeUp,
faStepForward
faCheckCircle
} from "@fortawesome/free-solid-svg-icons";
import { fetchPlaylistSongs } from "@/services/youtubeService.js";
import YouTubePlayer from "../../components/YouTubePlayer/YouTubePlayer";
export const Game = () => {
const {send, on, socket, connected, connect} = useContext(SocketContext);
const {setCurrentState} = useContext(StateContext);
useEffect(() => {
if (!connected) {
@ -69,12 +69,14 @@ export const Game = () => {
},
"song-selected": setCurrentSong,
"round-started": (data) => {
setRound(data.round);
requestAnimationFrame(() => {
setPhase("composing");
setRound(data.round);
setTimeLeft(data.timeRemaining);
setSelectedSong(null);
setHasGuessed(false);
setGuessResult(null);
});
},
"guessing-phase-started": (data) => {
console.log("Guessing phase started:", data);
@ -133,7 +135,7 @@ export const Game = () => {
if (phase === "composing") {
console.log("Received frequency update:", data.frequency);
setFrequency(data.frequency);
setComposerIsPlaying(data.isPlaying); // Make sure isPlaying is handled
setComposerIsPlaying(data.isPlaying);
}
},
"phase-changed": (data) => {
@ -156,6 +158,10 @@ export const Game = () => {
console.log("Received song options early:", data);
setSongOptions(data.songOptions || []);
},
"game-ended": (finalData) => {
setCurrentState("Ending");
localStorage.setItem('finalScores', JSON.stringify(finalData));
},
};
const cleanupFunctions = Object.entries(eventHandlers).map(
@ -168,7 +174,7 @@ export const Game = () => {
}
return () => cleanupFunctions.forEach(cleanup => cleanup());
}, [socket, on, send, role, currentSong, phase, connected, connectedUsers]);
}, [socket, on, send, role, currentSong, phase, connected, connectedUsers, setCurrentState, round]);
useEffect(() => {
if (timerIntervalRef.current) clearInterval(timerIntervalRef.current);
@ -279,10 +285,8 @@ export const Game = () => {
}, [selectedSong, send, phase]);
const handleNextRound = useCallback(() => {
send("next-round");
setSelectedSong(null);
setGuessResult(null);
setTimeLeft(0);
send("next-round");
}, [send]);
const handlePlayerReady = useCallback(() => {

View File

@ -44,6 +44,8 @@ const initializeGameState = (roomId) => {
return gameStates[roomId];
};
const MAX_ROUNDS = 5;
const startNewRound = async (roomId) => {
const gameState = gameStates[roomId];
if (!gameState) return false;
@ -52,6 +54,11 @@ const startNewRound = async (roomId) => {
if (users.length < 2) return false;
gameState.round += 1;
if (gameState.round > MAX_ROUNDS) {
return { gameEnd: true, finalScores: gameState.scores };
}
gameState.phase = 'composing';
gameState.guessResults = {};
gameState.roundStartTime = Date.now();
@ -83,7 +90,6 @@ const determineNextComposer = (roomId, gameState, users) => {
return users[(currentIndex + 1) % users.length].id;
};
// Then modify the selectSongAndOptions function to use the shuffle:
const selectSongAndOptions = async (gameState) => {
try {
console.log(`Fetching songs from playlist: ${gameState.selectedPlaylist}`);
@ -107,7 +113,6 @@ const selectSongAndOptions = async (gameState) => {
count++;
}
// Shuffle the song options to randomize the position of the correct answer
gameState.songOptions = shuffleArray(Array.from(optionIds).map(id => ({ id })));
return true;
@ -219,6 +224,27 @@ const getGameState = (roomId) => {
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 = {
initializeGameState,
startNewRound,
@ -234,5 +260,7 @@ module.exports = {
getSelectedSong,
cleanupGameState,
getCurrentComposer,
getGameState
getGameState,
getFinalScores,
MAX_ROUNDS
};

View File

@ -109,6 +109,23 @@ module.exports = (io) => (socket) => {
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", () => {
const roomId = roomController.getUserRoom(socket.id);
if (roomId) {
@ -298,17 +315,7 @@ module.exports = (io) => (socket) => {
return socket.emit("error", { message: "At least 2 players are required" });
}
try {
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" });
}
await handleNextRound(roomId);
});
socket.on("get-current-frequency", () => {