Spaces:
Runtime error
Runtime error
| import { create } from "zustand"; | |
| /* βββββββββββββββββββ Types βββββββββββββββββββ */ | |
| export interface ArenaState { | |
| /* ββ Session ββ */ | |
| nodeId: string | null; | |
| courseId: string | null; | |
| totalStages: number; | |
| currentStageIndex: number; | |
| /* ββ Stage state ββ */ | |
| isCorrect: boolean | null; | |
| showFeedback: boolean; | |
| /* ββ Score tracking ββ */ | |
| correctCount: number; | |
| incorrectCount: number; | |
| consecutiveErrors: number; | |
| remedyTriggered: boolean; | |
| hintsUsed: number; | |
| /* ββ Timing ββ */ | |
| startTime: number | null; | |
| endTime: number | null; | |
| /* ββ Animation triggers ββ */ | |
| showConfetti: boolean; | |
| shakeScreen: boolean; | |
| } | |
| export interface ArenaActions { | |
| /* ββ Session lifecycle ββ */ | |
| startSession: (opts: { | |
| nodeId: string; | |
| courseId: string; | |
| totalStages: number; | |
| }) => void; | |
| endSession: () => void; | |
| /* ββ Stage progression ββ */ | |
| nextStage: () => void; | |
| /* ββ Answer evaluation ββ */ | |
| markCorrect: () => void; | |
| markIncorrect: () => void; | |
| /* ββ Feedback ββ */ | |
| showFeedbackPanel: () => void; | |
| hideFeedbackPanel: () => void; | |
| /* ββ Hints ββ */ | |
| useHint: () => void; | |
| /* ββ Animation triggers ββ */ | |
| triggerConfetti: () => void; | |
| triggerShake: () => void; | |
| clearAnimations: () => void; | |
| /* ββ Reset ββ */ | |
| resetArena: () => void; | |
| } | |
| /* βββββββββββββββββββ Initial State βββββββββββββββββββ */ | |
| /** How many consecutive wrong answers before remedy is triggered */ | |
| export const REMEDY_THRESHOLD = 3; | |
| const INITIAL_STATE: ArenaState = { | |
| nodeId: null, | |
| courseId: null, | |
| totalStages: 0, | |
| currentStageIndex: 0, | |
| isCorrect: null, | |
| showFeedback: false, | |
| correctCount: 0, | |
| incorrectCount: 0, | |
| consecutiveErrors: 0, | |
| remedyTriggered: false, | |
| hintsUsed: 0, | |
| startTime: null, | |
| endTime: null, | |
| showConfetti: false, | |
| shakeScreen: false, | |
| }; | |
| /* βββββββββββββββββββ Store βββββββββββββββββββ */ | |
| const useArenaStore = create<ArenaState & ArenaActions>()((set, get) => ({ | |
| ...INITIAL_STATE, | |
| /* ββ Session lifecycle ββ */ | |
| startSession: ({ nodeId, courseId, totalStages }) => | |
| set({ | |
| ...INITIAL_STATE, | |
| nodeId, | |
| courseId, | |
| totalStages, | |
| startTime: Date.now(), | |
| }), | |
| endSession: () => | |
| set({ | |
| endTime: Date.now(), | |
| }), | |
| /* ββ Stage progression ββ */ | |
| nextStage: () => | |
| set((s) => ({ | |
| currentStageIndex: Math.min(s.currentStageIndex + 1, s.totalStages - 1), | |
| isCorrect: null, | |
| showFeedback: false, | |
| showConfetti: false, | |
| shakeScreen: false, | |
| })), | |
| /* ββ Answer evaluation ββ */ | |
| markCorrect: () => | |
| set((s) => ({ | |
| isCorrect: true, | |
| correctCount: s.correctCount + 1, | |
| consecutiveErrors: 0, | |
| })), | |
| markIncorrect: () => | |
| set((s) => { | |
| const next = s.consecutiveErrors + 1; | |
| return { | |
| isCorrect: false, | |
| incorrectCount: s.incorrectCount + 1, | |
| consecutiveErrors: next, | |
| remedyTriggered: next >= REMEDY_THRESHOLD ? true : s.remedyTriggered, | |
| }; | |
| }), | |
| /* ββ Feedback ββ */ | |
| showFeedbackPanel: () => set({ showFeedback: true }), | |
| hideFeedbackPanel: () => set({ showFeedback: false }), | |
| /* ββ Hints ββ */ | |
| useHint: () => set((s) => ({ hintsUsed: s.hintsUsed + 1 })), | |
| /* ββ Animation triggers ββ */ | |
| triggerConfetti: () => set({ showConfetti: true }), | |
| triggerShake: () => { | |
| set({ shakeScreen: true }); | |
| setTimeout(() => set({ shakeScreen: false }), 500); | |
| }, | |
| clearAnimations: () => set({ showConfetti: false, shakeScreen: false }), | |
| /* ββ Reset ββ */ | |
| resetArena: () => set(INITIAL_STATE), | |
| })); | |
| /** Derived: compute accuracy percentage */ | |
| export function getAccuracy(state: ArenaState): number { | |
| const total = state.correctCount + state.incorrectCount; | |
| if (total === 0) return 100; | |
| return Math.round((state.correctCount / total) * 100); | |
| } | |
| /** Derived: compute elapsed time string (e.g. "2m 14s") */ | |
| export function getElapsedTime(state: ArenaState): string { | |
| const start = state.startTime ?? Date.now(); | |
| const end = state.endTime ?? Date.now(); | |
| const secs = Math.floor((end - start) / 1000); | |
| const m = Math.floor(secs / 60); | |
| const s = secs % 60; | |
| return `${m}m ${s.toString().padStart(2, "0")}s`; | |
| } | |
| /** Derived: compute XP gained */ | |
| export function getXpGained(state: ArenaState): number { | |
| const accuracy = getAccuracy(state); | |
| const baseXp = 30; | |
| const bonus = Math.floor((accuracy / 100) * 20); | |
| const hintPenalty = state.hintsUsed * 5; | |
| return Math.max(10, baseXp + bonus - hintPenalty); | |
| } | |
| export default useArenaStore; | |