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
|
|
@ -4,7 +4,7 @@ import { useAuth } from 'react-oidc-context';
|
|||
import { useAuthenticatedFetch } from './useAuthenticatedFetch';
|
||||
import { Quiz, Player, GameState, GameRole, NetworkMessage, AnswerOption, Question, GenerateQuizOptions, ProcessedDocument, GameConfig, DEFAULT_GAME_CONFIG, PointsBreakdown } from '../types';
|
||||
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 { uniqueNamesGenerator, adjectives, animals } from 'unique-names-generator';
|
||||
|
||||
|
|
@ -975,6 +975,7 @@ export const useGame = (defaultGameConfig?: GameConfig) => {
|
|||
selectedShape: null,
|
||||
assignedName,
|
||||
presenterId: presenterIdRef.current,
|
||||
timerEnabled: gameConfigRef.current.timerEnabled,
|
||||
};
|
||||
|
||||
if (currentQuestion) {
|
||||
|
|
@ -985,6 +986,7 @@ export const useGame = (defaultGameConfig?: GameConfig) => {
|
|||
...o,
|
||||
isCorrect: false
|
||||
}));
|
||||
welcomePayload.questionTimeLimit = currentQuestion.timeLimit;
|
||||
}
|
||||
|
||||
if (reconnectedPlayer) {
|
||||
|
|
@ -1030,7 +1032,7 @@ export const useGame = (defaultGameConfig?: GameConfig) => {
|
|||
const breakdown = calculatePointsWithBreakdown({
|
||||
isCorrect,
|
||||
timeLeftMs: timeLeftRef.current,
|
||||
questionTimeMs: QUESTION_TIME_MS,
|
||||
questionTimeMs: (quizRef.current?.questions[currentQuestionIndexRef.current]?.timeLimit || 20) * 1000,
|
||||
streak: newStreak,
|
||||
playerRank,
|
||||
isFirstCorrect,
|
||||
|
|
@ -1133,7 +1135,6 @@ export const useGame = (defaultGameConfig?: GameConfig) => {
|
|||
|
||||
const startQuestion = (isResume: boolean = false) => {
|
||||
setGameState('QUESTION');
|
||||
setTimeLeft(QUESTION_TIME_MS);
|
||||
setFirstCorrectPlayerId(null);
|
||||
|
||||
if (isResume) {
|
||||
|
|
@ -1162,6 +1163,8 @@ export const useGame = (defaultGameConfig?: GameConfig) => {
|
|||
const options = currentQ.options || [];
|
||||
const correctOpt = options.find(o => o.isCorrect);
|
||||
const correctShape = correctOpt?.shape || 'triangle';
|
||||
const questionTimeMs = currentQ.timeLimit * 1000;
|
||||
setTimeLeft(questionTimeMs);
|
||||
setCurrentCorrectShape(correctShape);
|
||||
|
||||
const optionsForClient = options.map(o => ({
|
||||
|
|
@ -1169,32 +1172,37 @@ export const useGame = (defaultGameConfig?: GameConfig) => {
|
|||
isCorrect: false
|
||||
}));
|
||||
|
||||
const timerOn = gameConfigRef.current.timerEnabled;
|
||||
broadcast({
|
||||
type: 'QUESTION_START',
|
||||
payload: {
|
||||
totalQuestions: currentQuiz.questions.length,
|
||||
currentQuestionIndex: currentIndex,
|
||||
timeLimit: QUESTION_TIME,
|
||||
timeLimit: currentQ.timeLimit,
|
||||
correctShape,
|
||||
questionText: currentQ.text,
|
||||
options: optionsForClient
|
||||
options: optionsForClient,
|
||||
timerEnabled: timerOn,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (timerRef.current) clearInterval(timerRef.current);
|
||||
let tickCount = 0;
|
||||
timerRef.current = setInterval(() => {
|
||||
setTimeLeft(prev => {
|
||||
if (prev <= 100) { endQuestion(); return 0; }
|
||||
const newTime = prev - 100;
|
||||
tickCount++;
|
||||
if (tickCount % 10 === 0) {
|
||||
broadcast({ type: 'TIME_SYNC', payload: { timeLeft: newTime } });
|
||||
}
|
||||
return newTime;
|
||||
});
|
||||
}, 100);
|
||||
|
||||
if (gameConfigRef.current.timerEnabled) {
|
||||
let tickCount = 0;
|
||||
timerRef.current = setInterval(() => {
|
||||
setTimeLeft(prev => {
|
||||
if (prev <= 100) { endQuestion(); return 0; }
|
||||
const newTime = prev - 100;
|
||||
tickCount++;
|
||||
if (tickCount % 10 === 0) {
|
||||
broadcast({ type: 'TIME_SYNC', payload: { timeLeft: newTime } });
|
||||
}
|
||||
return newTime;
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
const endQuestion = () => {
|
||||
|
|
@ -1209,7 +1217,7 @@ export const useGame = (defaultGameConfig?: GameConfig) => {
|
|||
const breakdown = calculatePointsWithBreakdown({
|
||||
isCorrect: false,
|
||||
timeLeftMs: 0,
|
||||
questionTimeMs: QUESTION_TIME_MS,
|
||||
questionTimeMs: (quizRef.current?.questions[currentQuestionIndexRef.current]?.timeLimit || 20) * 1000,
|
||||
streak: 0,
|
||||
playerRank,
|
||||
isFirstCorrect: false,
|
||||
|
|
@ -1463,7 +1471,7 @@ export const useGame = (defaultGameConfig?: GameConfig) => {
|
|||
id: `q-${i}`,
|
||||
text: payload.questionText,
|
||||
options: payload.options,
|
||||
timeLimit: QUESTION_TIME
|
||||
timeLimit: payload.questionTimeLimit || 20
|
||||
});
|
||||
} else {
|
||||
questions.push({ id: `loading-${i}`, text: '', options: [], timeLimit: 0 });
|
||||
|
|
@ -1482,7 +1490,9 @@ export const useGame = (defaultGameConfig?: GameConfig) => {
|
|||
setGameState(serverGameState);
|
||||
if (!playerHasAnswered && serverGameState === 'QUESTION') {
|
||||
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') {
|
||||
setGameState('REVEAL');
|
||||
|
|
@ -1533,7 +1543,9 @@ export const useGame = (defaultGameConfig?: GameConfig) => {
|
|||
});
|
||||
|
||||
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') {
|
||||
|
|
@ -1629,7 +1641,7 @@ export const useGame = (defaultGameConfig?: GameConfig) => {
|
|||
const breakdown = calculatePointsWithBreakdown({
|
||||
isCorrect,
|
||||
timeLeftMs: timeLeftRef.current,
|
||||
questionTimeMs: QUESTION_TIME_MS,
|
||||
questionTimeMs: (quizRef.current?.questions[currentQuestionIndexRef.current]?.timeLimit || 20) * 1000,
|
||||
streak: newStreak,
|
||||
playerRank,
|
||||
isFirstCorrect,
|
||||
|
|
@ -1759,7 +1771,7 @@ export const useGame = (defaultGameConfig?: GameConfig) => {
|
|||
return {
|
||||
role, gameState, quiz, players, currentQuestionIndex, timeLeft, error, gamePin, hasAnswered, lastPointsEarned, lastAnswerCorrect, currentCorrectShape, selectedOption, currentPlayerScore, currentStreak, currentPlayerId, gameConfig,
|
||||
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
|
||||
};
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue