149 lines
5.3 KiB
TypeScript
149 lines
5.3 KiB
TypeScript
"use client";
|
|
|
|
import { useMemo, useState } from "react";
|
|
import { EndScreen } from "./components/EndScreen";
|
|
import { GameScreen } from "./components/GameScreen";
|
|
import { StartScreen } from "./components/StartScreen";
|
|
import { usePkAudio } from "./hooks/usePkAudio";
|
|
import { usePkSetup } from "./hooks/usePkSetup";
|
|
import type { AnswerState, Team, WordItem } from "./types";
|
|
import { getFairRoundCount, normalizeOptionCount, shuffleArray } from "./utils";
|
|
|
|
export default function PkPage() {
|
|
const {
|
|
selectedGrade,
|
|
setSelectedGrade,
|
|
classTypes,
|
|
selectedClassTypeId,
|
|
setSelectedClassTypeId,
|
|
availableWords,
|
|
setAvailableWords,
|
|
loadingSetup,
|
|
settings,
|
|
} = usePkSetup();
|
|
const [started, setStarted] = useState(false);
|
|
const [ended, setEnded] = useState(false);
|
|
const [roundWords, setRoundWords] = useState<WordItem[]>([]);
|
|
const [questionIndex, setQuestionIndex] = useState(0);
|
|
const [currentTeam, setCurrentTeam] = useState<Team>("red");
|
|
const [scoreRed, setScoreRed] = useState(0);
|
|
const [scoreBlue, setScoreBlue] = useState(0);
|
|
const [answerState, setAnswerState] = useState<AnswerState>("idle");
|
|
const [selectedWord, setSelectedWord] = useState<number | null>(null);
|
|
|
|
const { speechTip, setSpeechTip, playTone, speakWord } = usePkAudio({
|
|
onGeneratedAudio: (word) => {
|
|
setRoundWords((items) => items.map((item) => (item.id === word.id ? word : item)));
|
|
setAvailableWords((items) => items.map((item) => (item.id === word.id ? word : item)));
|
|
},
|
|
});
|
|
|
|
const selectedClassType = classTypes.find((classType) => classType.id === selectedClassTypeId);
|
|
const currentWord = roundWords[questionIndex] ?? availableWords[0];
|
|
const optionCount = normalizeOptionCount(settings.pkOptionCount);
|
|
const options = useMemo(() => {
|
|
if (!currentWord) return [];
|
|
const wrongOptions = availableWords.filter((word) => word.id !== currentWord.id);
|
|
return shuffleArray([currentWord, ...shuffleArray(wrongOptions).slice(0, optionCount - 1)]);
|
|
}, [currentWord, availableWords, optionCount]);
|
|
const roundWordLimit = Math.max(0, Math.floor(settings.pkWordCount || 0));
|
|
const plannedRoundCount = getFairRoundCount(availableWords.length, roundWordLimit);
|
|
const winner = scoreRed > scoreBlue ? "red" : scoreBlue > scoreRed ? "blue" : "draw";
|
|
|
|
function startGame() {
|
|
const nextRound = shuffleArray(availableWords).slice(0, plannedRoundCount);
|
|
if (nextRound.length === 0) return;
|
|
|
|
setRoundWords(nextRound);
|
|
setQuestionIndex(0);
|
|
setCurrentTeam("red");
|
|
setScoreRed(0);
|
|
setScoreBlue(0);
|
|
setAnswerState("idle");
|
|
setSelectedWord(null);
|
|
setStarted(true);
|
|
setEnded(false);
|
|
setSpeechTip(settings.autoSpeak ? "准备朗读中..." : "自动朗读已关闭。");
|
|
if (settings.autoSpeak) window.setTimeout(() => void speakWord(nextRound[0]), 350);
|
|
}
|
|
|
|
function checkAnswer(option: WordItem) {
|
|
if (answerState !== "idle") return;
|
|
const correct = option.id === currentWord.id;
|
|
setSelectedWord(option.id);
|
|
setAnswerState(correct ? "correct" : "wrong");
|
|
void playTone(correct);
|
|
|
|
if (correct) {
|
|
if (currentTeam === "red") setScoreRed((score) => score + settings.scorePerCorrect);
|
|
else setScoreBlue((score) => score + settings.scorePerCorrect);
|
|
} else if (settings.scorePerWrong > 0) {
|
|
if (currentTeam === "red") setScoreRed((score) => Math.max(0, score - settings.scorePerWrong));
|
|
else setScoreBlue((score) => Math.max(0, score - settings.scorePerWrong));
|
|
}
|
|
}
|
|
|
|
function nextQuestion() {
|
|
window.speechSynthesis?.cancel();
|
|
const nextIndex = questionIndex + 1;
|
|
const nextTeam = currentTeam === "red" ? "blue" : "red";
|
|
setCurrentTeam(nextTeam);
|
|
setAnswerState("idle");
|
|
setSelectedWord(null);
|
|
|
|
if (nextIndex >= roundWords.length) {
|
|
setEnded(true);
|
|
return;
|
|
}
|
|
|
|
setQuestionIndex(nextIndex);
|
|
setSpeechTip(settings.autoSpeak ? "准备朗读中..." : "自动朗读已关闭。");
|
|
if (settings.autoSpeak) window.setTimeout(() => void speakWord(roundWords[nextIndex]), 350);
|
|
}
|
|
|
|
if (!started) {
|
|
return (
|
|
<StartScreen
|
|
selectedGrade={selectedGrade}
|
|
classTypes={classTypes}
|
|
selectedClassTypeId={selectedClassTypeId}
|
|
selectedClassTypeName={selectedClassType?.name}
|
|
availableWordCount={availableWords.length}
|
|
plannedRoundCount={plannedRoundCount}
|
|
loadingSetup={loadingSetup}
|
|
scorePerCorrect={settings.scorePerCorrect}
|
|
scorePerWrong={settings.scorePerWrong}
|
|
onSelectGrade={setSelectedGrade}
|
|
onSelectClassType={setSelectedClassTypeId}
|
|
onStartGame={startGame}
|
|
/>
|
|
);
|
|
}
|
|
|
|
if (ended) {
|
|
return <EndScreen winner={winner} scoreRed={scoreRed} scoreBlue={scoreBlue} onRestart={startGame} />;
|
|
}
|
|
|
|
return (
|
|
<GameScreen
|
|
selectedGrade={selectedGrade}
|
|
selectedClassTypeName={selectedClassType?.name}
|
|
roundWords={roundWords}
|
|
currentWord={currentWord}
|
|
options={options}
|
|
optionCount={optionCount}
|
|
questionIndex={questionIndex}
|
|
currentTeam={currentTeam}
|
|
scoreRed={scoreRed}
|
|
scoreBlue={scoreBlue}
|
|
answerState={answerState}
|
|
selectedWord={selectedWord}
|
|
speechTip={speechTip}
|
|
settings={settings}
|
|
onSpeakWord={(word) => void speakWord(word)}
|
|
onCheckAnswer={checkAnswer}
|
|
onNextQuestion={nextQuestion}
|
|
/>
|
|
);
|
|
}
|