| import { useEffect, useRef, useCallback, useState } from 'react';
|
| import { useSharedValue, withTiming, Easing, runOnJS } from 'react-native-reanimated';
|
| import { MathQuestion } from '../types';
|
| import { generateQuestions } from '../constants/gameData';
|
|
|
| interface GameEngineConfig {
|
| totalQuestions?: number;
|
| timePerQuestion?: number;
|
| basePoints?: number;
|
| comboMultiplier?: number;
|
| }
|
|
|
| interface GameEngineReturn {
|
| questions: MathQuestion[];
|
| currentQuestionIndex: number;
|
| score: number;
|
| combo: number;
|
| maxCombo: number;
|
| correctAnswers: number;
|
| isGameOver: boolean;
|
| timerProgress: ReturnType<typeof useSharedValue>;
|
| answerFeedback: (boolean | null)[];
|
| handleAnswer: (selectedAnswer: number) => void;
|
| totalQuestions: number;
|
| avgTime: number;
|
| }
|
|
|
| export const useGameEngine = (
|
| config: GameEngineConfig = {},
|
| onGameOver?: (results: {
|
| score: number;
|
| combo: number;
|
| correctAnswers: number;
|
| totalQuestions: number;
|
| accuracy: number;
|
| avgTime: number;
|
| }) => void
|
| ): GameEngineReturn => {
|
| const {
|
| totalQuestions = 10,
|
| timePerQuestion = 15,
|
| basePoints = 100,
|
| comboMultiplier = 0.5,
|
| } = config;
|
|
|
| const [questions] = useState(() => generateQuestions(totalQuestions));
|
| const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
|
| const [score, setScore] = useState(0);
|
| const [combo, setCombo] = useState(0);
|
| const [maxCombo, setMaxCombo] = useState(0);
|
| const [correctAnswers, setCorrectAnswers] = useState(0);
|
| const [isGameOver, setIsGameOver] = useState(false);
|
| const [answerFeedback, setAnswerFeedback] = useState<(boolean | null)[]>(
|
| Array(4).fill(null)
|
| );
|
| const [answerDisabled, setAnswerDisabled] = useState(false);
|
|
|
| const timerProgress = useSharedValue(1);
|
| const questionStartTime = useRef(Date.now());
|
| const totalTimeSpent = useRef(0);
|
| const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
|
|
|
| const startTimer = useCallback(() => {
|
| timerProgress.value = 1;
|
| timerProgress.value = withTiming(0, {
|
| duration: timePerQuestion * 1000,
|
| easing: Easing.linear,
|
| });
|
| questionStartTime.current = Date.now();
|
|
|
|
|
| if (timerRef.current) clearTimeout(timerRef.current);
|
| timerRef.current = setTimeout(() => {
|
| handleTimeout();
|
| }, timePerQuestion * 1000);
|
| }, [timePerQuestion]);
|
|
|
| useEffect(() => {
|
| if (!isGameOver) {
|
| startTimer();
|
| }
|
| return () => {
|
| if (timerRef.current) clearTimeout(timerRef.current);
|
| };
|
| }, [currentQuestionIndex, isGameOver]);
|
|
|
| const handleTimeout = () => {
|
| if (isGameOver) return;
|
| setCombo(0);
|
| moveToNextQuestion();
|
| };
|
|
|
| const moveToNextQuestion = () => {
|
| if (timerRef.current) clearTimeout(timerRef.current);
|
|
|
| const timeSpent = (Date.now() - questionStartTime.current) / 1000;
|
| totalTimeSpent.current += timeSpent;
|
|
|
| if (currentQuestionIndex >= totalQuestions - 1) {
|
|
|
| setIsGameOver(true);
|
| const accuracy = Math.round((correctAnswers / totalQuestions) * 100);
|
| const avgTime = Math.round((totalTimeSpent.current / totalQuestions) * 10) / 10;
|
| if (onGameOver) {
|
| setTimeout(() => {
|
| onGameOver({
|
| score,
|
| combo: maxCombo,
|
| correctAnswers,
|
| totalQuestions,
|
| accuracy,
|
| avgTime,
|
| });
|
| }, 800);
|
| }
|
| } else {
|
| setTimeout(() => {
|
| setCurrentQuestionIndex((prev) => prev + 1);
|
| setAnswerFeedback(Array(4).fill(null));
|
| setAnswerDisabled(false);
|
| }, 600);
|
| }
|
| };
|
|
|
| const handleAnswer = (selectedAnswer: number) => {
|
| if (answerDisabled || isGameOver) return;
|
| setAnswerDisabled(true);
|
|
|
| if (timerRef.current) clearTimeout(timerRef.current);
|
|
|
| const question = questions[currentQuestionIndex];
|
| const isCorrect = selectedAnswer === question.correctAnswer;
|
|
|
|
|
| const feedback = question.options.map((opt) => {
|
| if (opt === question.correctAnswer) return true;
|
| if (opt === selectedAnswer && !isCorrect) return false;
|
| return null;
|
| });
|
| setAnswerFeedback(feedback);
|
|
|
| const timeSpent = (Date.now() - questionStartTime.current) / 1000;
|
| totalTimeSpent.current += timeSpent;
|
|
|
| if (isCorrect) {
|
| const newCombo = combo + 1;
|
| const comboBonus = Math.floor(basePoints * comboMultiplier * (newCombo - 1));
|
| const timeBonus = Math.floor(Math.max(0, (timePerQuestion - timeSpent) * 10));
|
| const points = basePoints + comboBonus + timeBonus;
|
|
|
| setScore((prev) => prev + points);
|
| setCombo(newCombo);
|
| setMaxCombo((prev) => Math.max(prev, newCombo));
|
| setCorrectAnswers((prev) => prev + 1);
|
| } else {
|
| setCombo(0);
|
| }
|
|
|
|
|
| if (currentQuestionIndex >= totalQuestions - 1) {
|
|
|
| setTimeout(() => {
|
| setIsGameOver(true);
|
| const finalCorrect = isCorrect ? correctAnswers + 1 : correctAnswers;
|
| const accuracy = Math.round((finalCorrect / totalQuestions) * 100);
|
| const avgTime = Math.round((totalTimeSpent.current / totalQuestions) * 10) / 10;
|
| if (onGameOver) {
|
| onGameOver({
|
| score: isCorrect ? score + basePoints + Math.floor(basePoints * comboMultiplier * combo) : score,
|
| combo: Math.max(maxCombo, isCorrect ? combo + 1 : combo),
|
| correctAnswers: finalCorrect,
|
| totalQuestions,
|
| accuracy,
|
| avgTime,
|
| });
|
| }
|
| }, 800);
|
| } else {
|
| setTimeout(() => {
|
| setCurrentQuestionIndex((prev) => prev + 1);
|
| setAnswerFeedback(Array(4).fill(null));
|
| setAnswerDisabled(false);
|
| }, 600);
|
| }
|
| };
|
|
|
| return {
|
| questions,
|
| currentQuestionIndex,
|
| score,
|
| combo,
|
| maxCombo,
|
| correctAnswers,
|
| isGameOver,
|
| timerProgress,
|
| answerFeedback,
|
| handleAnswer,
|
| totalQuestions,
|
| avgTime: totalTimeSpent.current > 0
|
| ? Math.round((totalTimeSpent.current / Math.max(currentQuestionIndex, 1)) * 10) / 10
|
| : 0,
|
| };
|
| };
|
|
|