diff --git a/client/src/components/GameBoard.jsx b/client/src/components/GameBoard.jsx index d46a9a0..e9f37bc 100644 --- a/client/src/components/GameBoard.jsx +++ b/client/src/components/GameBoard.jsx @@ -1,21 +1,11 @@ function GameBoard({ questions, usedQuestions, onSelectQuestion, currentRound, isHost }) { const isQuestionUsed = (category, points, questionIndex) => { - const foundByIndex = usedQuestions.find( - q => q.category === category && q.points === points && q.questionIndex === questionIndex + return usedQuestions.some( + q => q.category === category && ( + q.questionIndex === questionIndex || + (q.questionIndex === undefined && q.points === points) + ) ); - if (foundByIndex) return true; - - const usedCount = usedQuestions.filter( - q => q.category === category && q.points === points - ).length; - if (usedCount === 0) return false; - - const categoryData = questions.find(cat => cat.name === category); - const questionsWithSamePoints = categoryData?.questions - .map((q, idx) => ({ ...q, originalIndex: idx })) - .filter(q => q.points === points) || []; - const positionAmongSamePoints = questionsWithSamePoints.findIndex(q => q.originalIndex === questionIndex); - return positionAmongSamePoints >= 0 && positionAmongSamePoints < usedCount; } const getPointsForRound = (basePoints, round) => { @@ -71,7 +61,7 @@ function GameBoard({ questions, usedQuestions, onSelectQuestion, currentRound, i return ( + )} + + ) : ( +

+ 🔊 Ведущий проигрывает аудио... +

+ )} {question.questionText && (

{question.questionText}

)} - {!mediaEnded && ( + {isHost && !mediaEnded && (

⏸️ Таймер начнется после прослушивания

@@ -69,14 +83,28 @@ function QuestionModal({ question, timer, onSubmitAnswer, answeringTeamName, isH case 'video': return (
- + {isHost ? ( + <> + + {mediaEnded && ( + + )} + + ) : ( +

+ 🔊 Ведущий проигрывает видео... +

+ )} {question.questionText && (

{question.questionText}

)} - {!mediaEnded && ( + {isHost && !mediaEnded && (

⏸️ Таймер начнется после просмотра

diff --git a/client/src/components/TeamScores.jsx b/client/src/components/TeamScores.jsx index d6afba8..28d68a2 100644 --- a/client/src/components/TeamScores.jsx +++ b/client/src/components/TeamScores.jsx @@ -1,4 +1,22 @@ -function TeamScores({ teams, myTeamId }) { +import { useState } from 'react'; + +function TeamScores({ teams, myTeamId, isHost, onAdjustScore }) { + const [editingTeam, setEditingTeam] = useState(null); + const [adjustValue, setAdjustValue] = useState(''); + + const handleAdjust = (teamId, amount) => { + if (onAdjustScore) onAdjustScore(teamId, amount); + }; + + const handleCustomAdjust = (teamId) => { + const val = parseInt(adjustValue); + if (!isNaN(val) && val !== 0) { + handleAdjust(teamId, val); + setAdjustValue(''); + setEditingTeam(null); + } + }; + return (
{teams.map((team, index) => { @@ -27,6 +45,46 @@ function TeamScores({ teams, myTeamId }) {

{team.score}

баллов

+ + {isHost && ( +
+ {editingTeam === team.id ? ( +
+
+ {[-100, -50, 50, 100].map(val => ( + + ))} +
+
+ setAdjustValue(e.target.value)} + onKeyDown={e => e.key === 'Enter' && handleCustomAdjust(team.id)} + placeholder="±" + className="flex-1 bg-slate-800/80 border border-slate-600/50 rounded-lg px-2 py-1 text-xs text-white text-center focus:outline-none focus:border-blue-400 w-0" /> + + +
+
+ ) : ( + + )} +
+ )}
) })} @@ -34,4 +92,4 @@ function TeamScores({ teams, myTeamId }) { ) } -export default TeamScores +export default TeamScores \ No newline at end of file diff --git a/client/src/pages/Game.jsx b/client/src/pages/Game.jsx index e64981d..ad10ee8 100644 --- a/client/src/pages/Game.jsx +++ b/client/src/pages/Game.jsx @@ -79,6 +79,10 @@ function Game() { setTimer(timer) }) + socket.on('score-updated', ({ teams }) => { + setGame(prev => prev ? { ...prev, teams } : prev); + }) + socket.on('timer-update', ({ timer }) => { setTimer(timer) }) socket.on('time-up', ({ answerMedia, shouldFinish }) => { @@ -94,8 +98,16 @@ function Game() { console.log(`${teamName} buzzing!`) }) - socket.on('answer-result', ({ teamId, isCorrect, questionClosed, answerMedia, shouldFinish }) => { - console.log('Answer result:', { teamId, isCorrect, questionClosed, answerMedia, shouldFinish }) + socket.on('answer-result', ({ teamId, isCorrect, newScore, questionClosed, answerMedia, shouldFinish }) => { + console.log('Answer result:', { teamId, isCorrect, newScore, questionClosed, answerMedia, shouldFinish }) + // Update team score in local state + if (newScore !== undefined) { + setGame(prev => { + if (!prev) return prev; + const updatedTeams = prev.teams.map(t => t.id === teamId ? { ...t, score: newScore } : t); + return { ...prev, teams: updatedTeams }; + }); + } if (questionClosed) { if (answerMedia) { setAnswerMedia(answerMedia) @@ -124,6 +136,7 @@ function Game() { socket.off('game-started') socket.off('host-game-loaded') socket.off('question-selected') + socket.off('score-updated') socket.off('timer-update') socket.off('time-up') socket.off('team-buzzing') @@ -134,8 +147,11 @@ function Game() { }, [teamName, questions, isHost]) const handleStartGame = () => { socket.emit('start-game', { gameCode }) } - const handleSelectQuestion = (category, points) => { - if (isHost) socket.emit('select-question', { gameCode, category, points }) + const handleSelectQuestion = (category, points, questionIndex) => { + if (isHost) socket.emit('select-question', { gameCode, category, points, questionIndex }) + } + const handleAdjustScore = (teamId, amount) => { + socket.emit('adjust-score', { gameCode, teamId, amount }); } const handleBuzzIn = () => { if (myTeam) socket.emit('buzz-in', { gameCode, teamId: myTeam.id }) @@ -246,7 +262,7 @@ function Game() {
{/* Team scores */} - + {/* Waiting state */} {game.status === 'waiting' && isHost && ( @@ -304,7 +320,7 @@ function Game() { )} {/* Answer media modal */} - {answerMedia && ( + {answerMedia && isHost && (

diff --git a/server/public/audio/mosina_playlist.mp3 b/server/public/audio/mosina_playlist.mp3 new file mode 100644 index 0000000..c5e7bcf Binary files /dev/null and b/server/public/audio/mosina_playlist.mp3 differ diff --git a/server/public/images/professii_1.jpg b/server/public/images/professii_1.jpg new file mode 100644 index 0000000..b1fa602 Binary files /dev/null and b/server/public/images/professii_1.jpg differ diff --git a/server/src/data/questions.json b/server/src/data/questions.json index 978679e..4f311dc 100644 --- a/server/src/data/questions.json +++ b/server/src/data/questions.json @@ -332,9 +332,10 @@ "questions": [ { "points": 100, - "type": "text", - "question": "", - "answer": "" + "type": "image", + "question": "/images/professii_1.jpg", + "questionText": "Что зашифровано в этой картинке?", + "answer": "Третий закон Ньютона" }, { "points": 200, diff --git a/server/src/models/Game.js b/server/src/models/Game.js index 349e8da..ddd2a36 100644 --- a/server/src/models/Game.js +++ b/server/src/models/Game.js @@ -30,7 +30,8 @@ const gameSchema = new mongoose.Schema({ }, usedQuestions: [{ category: String, - points: Number + points: Number, + questionIndex: Number }], status: { type: String, diff --git a/server/src/socket/handlers.js b/server/src/socket/handlers.js index 4087c3b..6f65f19 100644 --- a/server/src/socket/handlers.js +++ b/server/src/socket/handlers.js @@ -202,7 +202,7 @@ export const setupSocketHandlers = (io, activeGames) => { }); // Выбор вопроса - socket.on('select-question', ({ gameCode, category, points }) => { + socket.on('select-question', ({ gameCode, category, points, questionIndex: requestedIndex }) => { const game = activeGames.get(gameCode); if (!game) return; @@ -210,20 +210,21 @@ export const setupSocketHandlers = (io, activeGames) => { // Находим категорию const categoryData = game.selectedCategories.find(cat => cat.name === category); - // Подсчитываем, сколько вопросов с такими баллами уже использовано в этой категории - const usedCount = game.usedQuestions.filter( - q => q.category === category && q.points === points - ).length; + // Используем переданный индекс вопроса напрямую + const questionIndex = requestedIndex; - // Находим следующий неиспользованный вопрос с такими баллами (по порядку) - const questionsWithPoints = categoryData?.questions - .map((q, idx) => ({ ...q, originalIndex: idx })) - .filter(q => q.points === points); + // Проверяем, не использован ли уже этот вопрос + const alreadyUsed = game.usedQuestions.some( + q => q.category === category && q.questionIndex === questionIndex + ); - const questionIndex = questionsWithPoints[usedCount]?.originalIndex; + if (alreadyUsed) { + console.log('❌ Question already used:', { category, questionIndex }); + socket.emit('error', { message: 'Этот вопрос уже был использован' }); + return; + } - console.log(`📊 Select question: category="${category}", points=${points}, usedCount=${usedCount}, questionIndex=${questionIndex}`); - console.log(`📊 Questions with these points:`, questionsWithPoints.map(q => q.originalIndex)); + console.log(`📊 Select question: category="${category}", points=${points}, questionIndex=${questionIndex}`); if (questionIndex === undefined) { console.log('❌ No available question found'); @@ -730,6 +731,27 @@ export const setupSocketHandlers = (io, activeGames) => { }); // Отключение + + // Manual score adjustment by host + socket.on('adjust-score', async ({ gameCode, teamId, amount }) => { + const game = activeGames.get(gameCode); + if (!game || socket.id !== game.hostId) return; + + const team = game.teams.find(t => t.id === teamId); + if (!team) return; + + team.score += amount; + console.log(`Score adjusted: ${team.name} ${amount > 0 ? '+' : ''}${amount} = ${team.score}`); + + try { + await Game.findOneAndUpdate({ gameCode }, { teams: game.teams }); + } catch (error) { + console.log('Error updating DB:', error.message); + } + + io.to(gameCode).emit('score-updated', { teams: game.teams }); + }); + socket.on('disconnect', () => { console.log('👤 Client disconnected:', socket.id);