Files
hanhan 17fc24edc7
Build Docker Package / docker-package (push) Successful in 6m34s
feat: add word import and class type display controls
2026-07-02 14:34:00 +08:00

150 lines
5.4 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}
showCurrentWord={selectedClassType?.showCurrentWord ?? true}
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}
/>
);
}