File size: 4,854 Bytes
5c920e9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
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;