Add disable timer setting and fix per-question time limits
Add a 'Question Timer' toggle to game settings that lets the host disable the countdown timer. When disabled, questions show ∞ instead of a countdown, the host gets an 'End Question' button to manually advance, and all correct answers receive maximum points. Also fix a bug where per-question time limits were ignored — the timer and scoring always used the hardcoded 20-second default instead of each question's individual timeLimit.
This commit is contained in:
parent
bce534486c
commit
d1f82440a1
9 changed files with 94 additions and 31 deletions
5
App.tsx
5
App.tsx
|
|
@ -106,7 +106,8 @@ function App() {
|
||||||
kickPlayer,
|
kickPlayer,
|
||||||
leaveGame,
|
leaveGame,
|
||||||
connectedPlayerIds,
|
connectedPlayerIds,
|
||||||
setHostName
|
setHostName,
|
||||||
|
endQuestion,
|
||||||
} = useGame(defaultConfig);
|
} = useGame(defaultConfig);
|
||||||
|
|
||||||
const handleSaveQuiz = async () => {
|
const handleSaveQuiz = async () => {
|
||||||
|
|
@ -275,6 +276,8 @@ function App() {
|
||||||
hasAnswered={hasAnswered}
|
hasAnswered={hasAnswered}
|
||||||
lastPointsEarned={lastPointsEarned}
|
lastPointsEarned={lastPointsEarned}
|
||||||
hostPlays={gameConfig.hostParticipates}
|
hostPlays={gameConfig.hostParticipates}
|
||||||
|
timerEnabled={gameConfig.timerEnabled}
|
||||||
|
onEndQuestion={role === 'HOST' ? endQuestion : undefined}
|
||||||
/>
|
/>
|
||||||
) : role === 'CLIENT' && hasAnswered ? (
|
) : role === 'CLIENT' && hasAnswered ? (
|
||||||
<div className="flex flex-col items-center justify-center h-screen">
|
<div className="flex flex-col items-center justify-center h-screen">
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Shuffle, Eye, Flame, TrendingUp, MinusCircle, Award, Info, ChevronDown, ChevronUp, Dices, Users } from 'lucide-react';
|
import { Shuffle, Eye, Flame, TrendingUp, MinusCircle, Award, Info, ChevronDown, ChevronUp, Dices, Users, Timer } from 'lucide-react';
|
||||||
import type { GameConfig } from '../types';
|
import type { GameConfig } from '../types';
|
||||||
|
|
||||||
interface GameConfigPanelProps {
|
interface GameConfigPanelProps {
|
||||||
|
|
@ -263,6 +263,17 @@ export const GameConfigPanel: React.FC<GameConfigPanelProps> = ({
|
||||||
</div>
|
</div>
|
||||||
</ValueRow>
|
</ValueRow>
|
||||||
|
|
||||||
|
|
||||||
|
<ToggleRow
|
||||||
|
icon={<Timer size={20} />}
|
||||||
|
iconActive={config.timerEnabled}
|
||||||
|
label="Question Timer"
|
||||||
|
description="Countdown timer for each question"
|
||||||
|
checked={config.timerEnabled}
|
||||||
|
onChange={(v) => update({ timerEnabled: v })}
|
||||||
|
tooltip="When disabled, questions have no time limit. The host must manually end each question. All correct answers receive maximum points."
|
||||||
|
/>
|
||||||
|
|
||||||
<ToggleRow
|
<ToggleRow
|
||||||
icon={<Shuffle size={20} />}
|
icon={<Shuffle size={20} />}
|
||||||
iconActive={config.shuffleQuestions}
|
iconActive={config.shuffleQuestions}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
||||||
import { Question, AnswerOption, GameState, GameRole } from '../types';
|
import { Question, AnswerOption, GameState, GameRole } from '../types';
|
||||||
import { COLORS, SHAPES } from '../constants';
|
import { COLORS, SHAPES } from '../constants';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
import { StopCircle } from 'lucide-react';
|
||||||
|
|
||||||
interface GameScreenProps {
|
interface GameScreenProps {
|
||||||
question?: Question;
|
question?: Question;
|
||||||
|
|
@ -14,6 +15,8 @@ interface GameScreenProps {
|
||||||
hasAnswered: boolean;
|
hasAnswered: boolean;
|
||||||
lastPointsEarned: number | null;
|
lastPointsEarned: number | null;
|
||||||
hostPlays?: boolean;
|
hostPlays?: boolean;
|
||||||
|
timerEnabled?: boolean;
|
||||||
|
onEndQuestion?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GameScreen: React.FC<GameScreenProps> = ({
|
export const GameScreen: React.FC<GameScreenProps> = ({
|
||||||
|
|
@ -26,13 +29,15 @@ export const GameScreen: React.FC<GameScreenProps> = ({
|
||||||
onAnswer,
|
onAnswer,
|
||||||
hasAnswered,
|
hasAnswered,
|
||||||
hostPlays = true,
|
hostPlays = true,
|
||||||
|
timerEnabled = true,
|
||||||
|
onEndQuestion,
|
||||||
}) => {
|
}) => {
|
||||||
const isClient = role === 'CLIENT';
|
const isClient = role === 'CLIENT';
|
||||||
const isSpectator = role === 'HOST' && !hostPlays;
|
const isSpectator = role === 'HOST' && !hostPlays;
|
||||||
const displayOptions = question?.options || [];
|
const displayOptions = question?.options || [];
|
||||||
const timeLeftSeconds = Math.ceil(timeLeft / 1000);
|
const timeLeftSeconds = Math.ceil(timeLeft / 1000);
|
||||||
|
|
||||||
const isUrgent = timeLeftSeconds <= 5 && timeLeftSeconds > 0;
|
const isUrgent = timerEnabled && timeLeftSeconds <= 5 && timeLeftSeconds > 0;
|
||||||
const timerBorderColor = isUrgent ? 'border-red-500' : 'border-white';
|
const timerBorderColor = isUrgent ? 'border-red-500' : 'border-white';
|
||||||
const timerTextColor = isUrgent ? 'text-red-500' : 'text-theme-primary';
|
const timerTextColor = isUrgent ? 'text-red-500' : 'text-theme-primary';
|
||||||
const timerAnimation = isUrgent ? 'animate-ping' : '';
|
const timerAnimation = isUrgent ? 'animate-ping' : '';
|
||||||
|
|
@ -45,11 +50,26 @@ export const GameScreen: React.FC<GameScreenProps> = ({
|
||||||
{currentQuestionIndex + 1} / {totalQuestions}
|
{currentQuestionIndex + 1} / {totalQuestions}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative">
|
<div className="relative flex flex-col items-center gap-2">
|
||||||
<div className="absolute inset-0 bg-white/20 rounded-full blur-xl animate-pulse"></div>
|
<div className="absolute inset-0 bg-white/20 rounded-full blur-xl animate-pulse"></div>
|
||||||
<div className={`bg-white ${timerTextColor} rounded-full w-14 h-14 md:w-20 md:h-20 flex items-center justify-center text-2xl md:text-4xl font-black shadow-[0_6px_0_rgba(0,0,0,0.2)] border-4 ${timerBorderColor} ${timerAnimation} relative z-10 transition-colors duration-300`}>
|
{timerEnabled ? (
|
||||||
{timeLeftSeconds}
|
<div className={`bg-white ${timerTextColor} rounded-full w-14 h-14 md:w-20 md:h-20 flex items-center justify-center text-2xl md:text-4xl font-black shadow-[0_6px_0_rgba(0,0,0,0.2)] border-4 ${timerBorderColor} ${timerAnimation} relative z-10 transition-colors duration-300`}>
|
||||||
</div>
|
{timeLeftSeconds}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="bg-white text-theme-primary rounded-full w-14 h-14 md:w-20 md:h-20 flex items-center justify-center text-3xl md:text-5xl font-black shadow-[0_6px_0_rgba(0,0,0,0.2)] border-4 border-white relative z-10">
|
||||||
|
∞
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!timerEnabled && role === 'HOST' && onEndQuestion && (
|
||||||
|
<button
|
||||||
|
onClick={onEndQuestion}
|
||||||
|
className="relative z-10 flex items-center gap-1.5 bg-white/90 hover:bg-white text-red-600 px-3 py-1.5 rounded-full text-xs md:text-sm font-bold shadow-lg hover:shadow-xl transition-all active:scale-95"
|
||||||
|
>
|
||||||
|
<StopCircle size={16} />
|
||||||
|
End Question
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white/20 backdrop-blur-md px-3 md:px-6 py-1 md:py-2 rounded-xl md:rounded-2xl font-black text-sm md:text-xl shadow-sm border-2 border-white/10">
|
<div className="bg-white/20 backdrop-blur-md px-3 md:px-6 py-1 md:py-2 rounded-xl md:rounded-2xl font-black text-sm md:text-xl shadow-sm border-2 border-white/10">
|
||||||
|
|
@ -163,4 +183,4 @@ export const GameScreen: React.FC<GameScreenProps> = ({
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { useAuth } from 'react-oidc-context';
|
||||||
import { useAuthenticatedFetch } from './useAuthenticatedFetch';
|
import { useAuthenticatedFetch } from './useAuthenticatedFetch';
|
||||||
import { Quiz, Player, GameState, GameRole, NetworkMessage, AnswerOption, Question, GenerateQuizOptions, ProcessedDocument, GameConfig, DEFAULT_GAME_CONFIG, PointsBreakdown } from '../types';
|
import { Quiz, Player, GameState, GameRole, NetworkMessage, AnswerOption, Question, GenerateQuizOptions, ProcessedDocument, GameConfig, DEFAULT_GAME_CONFIG, PointsBreakdown } from '../types';
|
||||||
import { generateQuiz } from '../services/geminiService';
|
import { generateQuiz } from '../services/geminiService';
|
||||||
import { QUESTION_TIME, QUESTION_TIME_MS, PLAYER_COLORS, calculatePointsWithBreakdown, getPlayerRank } from '../constants';
|
import { PLAYER_COLORS, calculatePointsWithBreakdown, getPlayerRank } from '../constants';
|
||||||
import { Peer, DataConnection, PeerOptions } from 'peerjs';
|
import { Peer, DataConnection, PeerOptions } from 'peerjs';
|
||||||
import { uniqueNamesGenerator, adjectives, animals } from 'unique-names-generator';
|
import { uniqueNamesGenerator, adjectives, animals } from 'unique-names-generator';
|
||||||
|
|
||||||
|
|
@ -975,6 +975,7 @@ export const useGame = (defaultGameConfig?: GameConfig) => {
|
||||||
selectedShape: null,
|
selectedShape: null,
|
||||||
assignedName,
|
assignedName,
|
||||||
presenterId: presenterIdRef.current,
|
presenterId: presenterIdRef.current,
|
||||||
|
timerEnabled: gameConfigRef.current.timerEnabled,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (currentQuestion) {
|
if (currentQuestion) {
|
||||||
|
|
@ -985,6 +986,7 @@ export const useGame = (defaultGameConfig?: GameConfig) => {
|
||||||
...o,
|
...o,
|
||||||
isCorrect: false
|
isCorrect: false
|
||||||
}));
|
}));
|
||||||
|
welcomePayload.questionTimeLimit = currentQuestion.timeLimit;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reconnectedPlayer) {
|
if (reconnectedPlayer) {
|
||||||
|
|
@ -1030,7 +1032,7 @@ export const useGame = (defaultGameConfig?: GameConfig) => {
|
||||||
const breakdown = calculatePointsWithBreakdown({
|
const breakdown = calculatePointsWithBreakdown({
|
||||||
isCorrect,
|
isCorrect,
|
||||||
timeLeftMs: timeLeftRef.current,
|
timeLeftMs: timeLeftRef.current,
|
||||||
questionTimeMs: QUESTION_TIME_MS,
|
questionTimeMs: (quizRef.current?.questions[currentQuestionIndexRef.current]?.timeLimit || 20) * 1000,
|
||||||
streak: newStreak,
|
streak: newStreak,
|
||||||
playerRank,
|
playerRank,
|
||||||
isFirstCorrect,
|
isFirstCorrect,
|
||||||
|
|
@ -1133,7 +1135,6 @@ export const useGame = (defaultGameConfig?: GameConfig) => {
|
||||||
|
|
||||||
const startQuestion = (isResume: boolean = false) => {
|
const startQuestion = (isResume: boolean = false) => {
|
||||||
setGameState('QUESTION');
|
setGameState('QUESTION');
|
||||||
setTimeLeft(QUESTION_TIME_MS);
|
|
||||||
setFirstCorrectPlayerId(null);
|
setFirstCorrectPlayerId(null);
|
||||||
|
|
||||||
if (isResume) {
|
if (isResume) {
|
||||||
|
|
@ -1162,6 +1163,8 @@ export const useGame = (defaultGameConfig?: GameConfig) => {
|
||||||
const options = currentQ.options || [];
|
const options = currentQ.options || [];
|
||||||
const correctOpt = options.find(o => o.isCorrect);
|
const correctOpt = options.find(o => o.isCorrect);
|
||||||
const correctShape = correctOpt?.shape || 'triangle';
|
const correctShape = correctOpt?.shape || 'triangle';
|
||||||
|
const questionTimeMs = currentQ.timeLimit * 1000;
|
||||||
|
setTimeLeft(questionTimeMs);
|
||||||
setCurrentCorrectShape(correctShape);
|
setCurrentCorrectShape(correctShape);
|
||||||
|
|
||||||
const optionsForClient = options.map(o => ({
|
const optionsForClient = options.map(o => ({
|
||||||
|
|
@ -1169,32 +1172,37 @@ export const useGame = (defaultGameConfig?: GameConfig) => {
|
||||||
isCorrect: false
|
isCorrect: false
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const timerOn = gameConfigRef.current.timerEnabled;
|
||||||
broadcast({
|
broadcast({
|
||||||
type: 'QUESTION_START',
|
type: 'QUESTION_START',
|
||||||
payload: {
|
payload: {
|
||||||
totalQuestions: currentQuiz.questions.length,
|
totalQuestions: currentQuiz.questions.length,
|
||||||
currentQuestionIndex: currentIndex,
|
currentQuestionIndex: currentIndex,
|
||||||
timeLimit: QUESTION_TIME,
|
timeLimit: currentQ.timeLimit,
|
||||||
correctShape,
|
correctShape,
|
||||||
questionText: currentQ.text,
|
questionText: currentQ.text,
|
||||||
options: optionsForClient
|
options: optionsForClient,
|
||||||
|
timerEnabled: timerOn,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (timerRef.current) clearInterval(timerRef.current);
|
if (timerRef.current) clearInterval(timerRef.current);
|
||||||
let tickCount = 0;
|
|
||||||
timerRef.current = setInterval(() => {
|
if (gameConfigRef.current.timerEnabled) {
|
||||||
setTimeLeft(prev => {
|
let tickCount = 0;
|
||||||
if (prev <= 100) { endQuestion(); return 0; }
|
timerRef.current = setInterval(() => {
|
||||||
const newTime = prev - 100;
|
setTimeLeft(prev => {
|
||||||
tickCount++;
|
if (prev <= 100) { endQuestion(); return 0; }
|
||||||
if (tickCount % 10 === 0) {
|
const newTime = prev - 100;
|
||||||
broadcast({ type: 'TIME_SYNC', payload: { timeLeft: newTime } });
|
tickCount++;
|
||||||
}
|
if (tickCount % 10 === 0) {
|
||||||
return newTime;
|
broadcast({ type: 'TIME_SYNC', payload: { timeLeft: newTime } });
|
||||||
});
|
}
|
||||||
}, 100);
|
return newTime;
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const endQuestion = () => {
|
const endQuestion = () => {
|
||||||
|
|
@ -1209,7 +1217,7 @@ export const useGame = (defaultGameConfig?: GameConfig) => {
|
||||||
const breakdown = calculatePointsWithBreakdown({
|
const breakdown = calculatePointsWithBreakdown({
|
||||||
isCorrect: false,
|
isCorrect: false,
|
||||||
timeLeftMs: 0,
|
timeLeftMs: 0,
|
||||||
questionTimeMs: QUESTION_TIME_MS,
|
questionTimeMs: (quizRef.current?.questions[currentQuestionIndexRef.current]?.timeLimit || 20) * 1000,
|
||||||
streak: 0,
|
streak: 0,
|
||||||
playerRank,
|
playerRank,
|
||||||
isFirstCorrect: false,
|
isFirstCorrect: false,
|
||||||
|
|
@ -1463,7 +1471,7 @@ export const useGame = (defaultGameConfig?: GameConfig) => {
|
||||||
id: `q-${i}`,
|
id: `q-${i}`,
|
||||||
text: payload.questionText,
|
text: payload.questionText,
|
||||||
options: payload.options,
|
options: payload.options,
|
||||||
timeLimit: QUESTION_TIME
|
timeLimit: payload.questionTimeLimit || 20
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
questions.push({ id: `loading-${i}`, text: '', options: [], timeLimit: 0 });
|
questions.push({ id: `loading-${i}`, text: '', options: [], timeLimit: 0 });
|
||||||
|
|
@ -1482,7 +1490,9 @@ export const useGame = (defaultGameConfig?: GameConfig) => {
|
||||||
setGameState(serverGameState);
|
setGameState(serverGameState);
|
||||||
if (!playerHasAnswered && serverGameState === 'QUESTION') {
|
if (!playerHasAnswered && serverGameState === 'QUESTION') {
|
||||||
if (timerRef.current) clearInterval(timerRef.current);
|
if (timerRef.current) clearInterval(timerRef.current);
|
||||||
timerRef.current = setInterval(() => setTimeLeft(prev => Math.max(0, prev - 100)), 100);
|
if (payload.timerEnabled !== false) {
|
||||||
|
timerRef.current = setInterval(() => setTimeLeft(prev => Math.max(0, prev - 100)), 100);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (serverGameState === 'REVEAL') {
|
} else if (serverGameState === 'REVEAL') {
|
||||||
setGameState('REVEAL');
|
setGameState('REVEAL');
|
||||||
|
|
@ -1533,7 +1543,9 @@ export const useGame = (defaultGameConfig?: GameConfig) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (timerRef.current) clearInterval(timerRef.current);
|
if (timerRef.current) clearInterval(timerRef.current);
|
||||||
timerRef.current = setInterval(() => setTimeLeft(prev => Math.max(0, prev - 100)), 100);
|
if (data.payload.timerEnabled !== false) {
|
||||||
|
timerRef.current = setInterval(() => setTimeLeft(prev => Math.max(0, prev - 100)), 100);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.type === 'RESULT') {
|
if (data.type === 'RESULT') {
|
||||||
|
|
@ -1629,7 +1641,7 @@ export const useGame = (defaultGameConfig?: GameConfig) => {
|
||||||
const breakdown = calculatePointsWithBreakdown({
|
const breakdown = calculatePointsWithBreakdown({
|
||||||
isCorrect,
|
isCorrect,
|
||||||
timeLeftMs: timeLeftRef.current,
|
timeLeftMs: timeLeftRef.current,
|
||||||
questionTimeMs: QUESTION_TIME_MS,
|
questionTimeMs: (quizRef.current?.questions[currentQuestionIndexRef.current]?.timeLimit || 20) * 1000,
|
||||||
streak: newStreak,
|
streak: newStreak,
|
||||||
playerRank,
|
playerRank,
|
||||||
isFirstCorrect,
|
isFirstCorrect,
|
||||||
|
|
@ -1759,7 +1771,7 @@ export const useGame = (defaultGameConfig?: GameConfig) => {
|
||||||
return {
|
return {
|
||||||
role, gameState, quiz, players, currentQuestionIndex, timeLeft, error, gamePin, hasAnswered, lastPointsEarned, lastAnswerCorrect, currentCorrectShape, selectedOption, currentPlayerScore, currentStreak, currentPlayerId, gameConfig,
|
role, gameState, quiz, players, currentQuestionIndex, timeLeft, error, gamePin, hasAnswered, lastPointsEarned, lastAnswerCorrect, currentCorrectShape, selectedOption, currentPlayerScore, currentStreak, currentPlayerId, gameConfig,
|
||||||
pendingQuizToSave, dismissSavePrompt, sourceQuizId, isReconnecting, currentPlayerName, presenterId, connectedPlayerIds,
|
pendingQuizToSave, dismissSavePrompt, sourceQuizId, isReconnecting, currentPlayerName, presenterId, connectedPlayerIds,
|
||||||
startQuizGen, startManualCreation, cancelCreation, finalizeManualQuiz, loadSavedQuiz, joinGame, startGame: startHostGame, handleAnswer, nextQuestion, showScoreboard,
|
startQuizGen, startManualCreation, cancelCreation, finalizeManualQuiz, loadSavedQuiz, joinGame, startGame: startHostGame, handleAnswer, nextQuestion, showScoreboard, endQuestion,
|
||||||
updateQuizFromEditor, startGameFromEditor, backFromEditor, endGame, attemptReconnect, goHomeFromDisconnected, resumeGame, setPresenterPlayer, sendAdvance, kickPlayer, leaveGame, setHostName
|
updateQuizFromEditor, startGameFromEditor, backFromEditor, endGame, attemptReconnect, goHomeFromDisconnected, resumeGame, setPresenterPlayer, sendAdvance, kickPlayer, leaveGame, setHostName
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -294,6 +294,8 @@ describe('DefaultConfigModal', () => {
|
||||||
penaltyPercent: 30,
|
penaltyPercent: 30,
|
||||||
firstCorrectBonusEnabled: true,
|
firstCorrectBonusEnabled: true,
|
||||||
firstCorrectBonusPoints: 75,
|
firstCorrectBonusPoints: 75,
|
||||||
|
timerEnabled: true,
|
||||||
|
maxPlayers: 10,
|
||||||
};
|
};
|
||||||
|
|
||||||
render(<DefaultConfigModal {...defaultProps} config={allEnabledConfig} />);
|
render(<DefaultConfigModal {...defaultProps} config={allEnabledConfig} />);
|
||||||
|
|
|
||||||
|
|
@ -415,6 +415,8 @@ describe('GameConfigPanel', () => {
|
||||||
penaltyPercent: 30,
|
penaltyPercent: 30,
|
||||||
firstCorrectBonusEnabled: true,
|
firstCorrectBonusEnabled: true,
|
||||||
firstCorrectBonusPoints: 75,
|
firstCorrectBonusPoints: 75,
|
||||||
|
timerEnabled: true,
|
||||||
|
maxPlayers: 10,
|
||||||
};
|
};
|
||||||
|
|
||||||
render(<GameConfigPanel {...defaultProps} config={allEnabledConfig} />);
|
render(<GameConfigPanel {...defaultProps} config={allEnabledConfig} />);
|
||||||
|
|
@ -439,6 +441,8 @@ describe('GameConfigPanel', () => {
|
||||||
penaltyPercent: 25,
|
penaltyPercent: 25,
|
||||||
firstCorrectBonusEnabled: false,
|
firstCorrectBonusEnabled: false,
|
||||||
firstCorrectBonusPoints: 50,
|
firstCorrectBonusPoints: 50,
|
||||||
|
timerEnabled: true,
|
||||||
|
maxPlayers: 10,
|
||||||
};
|
};
|
||||||
|
|
||||||
render(<GameConfigPanel {...defaultProps} config={allDisabledConfig} />);
|
render(<GameConfigPanel {...defaultProps} config={allDisabledConfig} />);
|
||||||
|
|
|
||||||
|
|
@ -347,6 +347,8 @@ describe('QuizEditor - Game Config Integration', () => {
|
||||||
penaltyPercent: 30,
|
penaltyPercent: 30,
|
||||||
firstCorrectBonusEnabled: true,
|
firstCorrectBonusEnabled: true,
|
||||||
firstCorrectBonusPoints: 75,
|
firstCorrectBonusPoints: 75,
|
||||||
|
timerEnabled: true,
|
||||||
|
maxPlayers: 10,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -294,6 +294,8 @@ describe('useQuizLibrary', () => {
|
||||||
penaltyPercent: 25,
|
penaltyPercent: 25,
|
||||||
firstCorrectBonusEnabled: false,
|
firstCorrectBonusEnabled: false,
|
||||||
firstCorrectBonusPoints: 50,
|
firstCorrectBonusPoints: 50,
|
||||||
|
timerEnabled: true,
|
||||||
|
maxPlayers: 10,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1135,6 +1137,8 @@ it('creates blob with correct mime type', async () => {
|
||||||
penaltyPercent: 10,
|
penaltyPercent: 10,
|
||||||
firstCorrectBonusEnabled: true,
|
firstCorrectBonusEnabled: true,
|
||||||
firstCorrectBonusPoints: 25,
|
firstCorrectBonusPoints: 25,
|
||||||
|
timerEnabled: true,
|
||||||
|
maxPlayers: 10,
|
||||||
};
|
};
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
|
|
|
||||||
5
types.ts
5
types.ts
|
|
@ -67,6 +67,7 @@ export interface Question {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GameConfig {
|
export interface GameConfig {
|
||||||
|
timerEnabled: boolean;
|
||||||
shuffleQuestions: boolean;
|
shuffleQuestions: boolean;
|
||||||
shuffleAnswers: boolean;
|
shuffleAnswers: boolean;
|
||||||
hostParticipates: boolean;
|
hostParticipates: boolean;
|
||||||
|
|
@ -84,6 +85,7 @@ export interface GameConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_GAME_CONFIG: GameConfig = {
|
export const DEFAULT_GAME_CONFIG: GameConfig = {
|
||||||
|
timerEnabled: true,
|
||||||
shuffleQuestions: false,
|
shuffleQuestions: false,
|
||||||
shuffleAnswers: false,
|
shuffleAnswers: false,
|
||||||
hostParticipates: true,
|
hostParticipates: true,
|
||||||
|
|
@ -205,8 +207,10 @@ export type NetworkMessage =
|
||||||
options?: AnswerOption[];
|
options?: AnswerOption[];
|
||||||
correctShape?: string;
|
correctShape?: string;
|
||||||
timeLeft?: number;
|
timeLeft?: number;
|
||||||
|
questionTimeLimit?: number;
|
||||||
assignedName?: string;
|
assignedName?: string;
|
||||||
presenterId?: string | null;
|
presenterId?: string | null;
|
||||||
|
timerEnabled?: boolean;
|
||||||
} }
|
} }
|
||||||
| { type: 'PLAYER_JOINED'; payload: { player: Player } }
|
| { type: 'PLAYER_JOINED'; payload: { player: Player } }
|
||||||
| { type: 'GAME_START'; payload: {} }
|
| { type: 'GAME_START'; payload: {} }
|
||||||
|
|
@ -220,6 +224,7 @@ export type NetworkMessage =
|
||||||
correctShape: string;
|
correctShape: string;
|
||||||
questionText: string;
|
questionText: string;
|
||||||
options: AnswerOption[];
|
options: AnswerOption[];
|
||||||
|
timerEnabled?: boolean;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
| { type: 'ANSWER'; payload: { playerId: string; isCorrect: boolean; selectedShape: 'triangle' | 'diamond' | 'circle' | 'square' } }
|
| { type: 'ANSWER'; payload: { playerId: string; isCorrect: boolean; selectedShape: 'triangle' | 'diamond' | 'circle' | 'square' } }
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue