1- import { useState , useMemo , useEffect , useRef } from 'react' ;
1+ import { useState , useMemo , useEffect , useRef , useCallback } from 'react' ;
22import { useNavigate } from 'react-router-dom' ;
33import confetti from 'canvas-confetti' ;
44import type { World , QuestionDocument , UserProgress } from '../types/question' ;
@@ -15,6 +15,27 @@ import './GamePage.css';
1515
1616type GameView = 'world-map' | 'world-questions' | 'playing' | 'reviewing' ;
1717
18+ /**
19+ * Obtém o nome amigável do mundo
20+ */
21+ const getWorldName = ( world : World ) : string => {
22+ const names : Record < World , string > = {
23+ basic_commands : 'Primeiros Passos' ,
24+ numbers : 'Números Mágicos' ,
25+ variables : 'Mundo das Variáveis' ,
26+ conditions : 'Terra das Decisões' ,
27+ decisions : 'Terra das Decisões' ,
28+ loops : 'Ilha da Repetição' ,
29+ functions : 'Vale das Funções' ,
30+ lists : 'Floresta das Listas' ,
31+ strings : 'Reino das Palavras' ,
32+ user_input : 'Conversando com o Usuário' ,
33+ dictionaries : 'Agenda Mágica' ,
34+ error_handling : 'Caçando Bugs' ,
35+ } ;
36+ return names [ world ] || world ;
37+ } ;
38+
1839/**
1940 * Página principal do jogo
2041 */
@@ -76,24 +97,62 @@ export function GamePage() {
7697 return progress ;
7798 } , [ allProgress , allQuestions ] ) ;
7899
100+ /**
101+ * Toca um som de celebração usando AudioContext (sem arquivos externos)
102+ */
103+ const playSuccessSound = useCallback ( ( ) => {
104+ try {
105+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
106+ const AudioContext = window . AudioContext || ( window as any ) . webkitAudioContext ;
107+ if ( ! AudioContext ) return ;
108+
109+ const ctx = new AudioContext ( ) ;
110+ const oscillators = [
111+ { freq : 523.25 , type : 'sine' , start : 0 , dur : 0.2 } , // C5
112+ { freq : 659.25 , type : 'sine' , start : 0.1 , dur : 0.2 } , // E5
113+ { freq : 783.99 , type : 'sine' , start : 0.2 , dur : 0.4 } , // G5
114+ { freq : 1046.50 , type : 'sine' , start : 0.3 , dur : 0.6 } // C6
115+ ] ;
116+
117+ oscillators . forEach ( ( { freq, type, start, dur } ) => {
118+ const osc = ctx . createOscillator ( ) ;
119+ const gain = ctx . createGain ( ) ;
120+
121+ osc . type = type as OscillatorType ;
122+ osc . frequency . setValueAtTime ( freq , ctx . currentTime ) ;
123+
124+ gain . gain . setValueAtTime ( 0.3 , ctx . currentTime ) ;
125+ gain . gain . exponentialRampToValueAtTime ( 0.01 , ctx . currentTime + dur ) ;
126+
127+ osc . connect ( gain ) ;
128+ gain . connect ( ctx . destination ) ;
129+
130+ osc . start ( ctx . currentTime + start ) ;
131+ osc . stop ( ctx . currentTime + start + dur ) ;
132+ } ) ;
133+ } catch ( e ) {
134+ console . error ( 'Audio play failed' , e ) ;
135+ }
136+ } , [ ] ) ;
137+
79138 /**
80139 * Seleciona um mundo para ver suas questões
81140 */
82- const handleSelectWorld = ( world : World ) => {
141+ const handleSelectWorld = useCallback ( ( world : World ) => {
83142 const questions = getQuestionsByWorld ( world ) ;
84143 setSelectedWorld ( world ) ;
85144 setWorldQuestions ( questions ) ;
86145 setView ( 'world-questions' ) ;
87146 // Reset perfect run state when entering a world
88147 setIsPerfectRun ( true ) ;
89- } ;
148+ } , [ getQuestionsByWorld ] ) ;
90149
91150
92151 /**
93152 * Começa a jogar uma questão
94153 * Se a questão já foi completada, mostra modal com opções
95154 */
96- const handleStartQuestion = ( question : QuestionDocument ) => {
155+ const handleStartQuestion = useCallback ( ( question : QuestionDocument ) => {
97156 const progress = getQuestionProgress ( question . id ) ;
98157 const index = worldQuestions . findIndex ( q => q . id === question . id ) ;
99158
@@ -111,38 +170,38 @@ export function GamePage() {
111170 // eslint-disable-next-line react-hooks/purity
112171 questionStartTime . current = Date . now ( ) ;
113172 }
114- } ;
173+ } , [ getQuestionProgress , worldQuestions ] ) ;
115174
116175 /**
117176 * Usuário escolheu "Ver minha resposta" no modal
118177 */
119- const handleViewAnswer = ( ) => {
178+ const handleViewAnswer = useCallback ( ( ) => {
120179 setShowCompletedModal ( false ) ;
121180 setView ( 'reviewing' ) ;
122- } ;
181+ } , [ ] ) ;
123182
124183 /**
125184 * Usuário escolheu "Refazer" no modal (modo prática, sem pontos)
126185 */
127- const handleRedoQuestion = ( ) => {
186+ const handleRedoQuestion = useCallback ( ( ) => {
128187 setShowCompletedModal ( false ) ;
129188 setView ( 'playing' ) ;
130189 questionStartTime . current = Date . now ( ) ;
131- } ;
190+ } , [ ] ) ;
132191
133192 /**
134193 * Fecha o modal de questão completada
135194 */
136- const handleCloseCompletedModal = ( ) => {
195+ const handleCloseCompletedModal = useCallback ( ( ) => {
137196 setShowCompletedModal ( false ) ;
138197 setCurrentQuestion ( null ) ;
139198 setCompletedQuestionProgress ( null ) ;
140- } ;
199+ } , [ ] ) ;
141200
142201 /**
143202 * Callback quando uma questão é completada
144203 */
145- const handleQuestionComplete = async ( passed : boolean , score : number ) => {
204+ const handleQuestionComplete = useCallback ( async ( passed : boolean , score : number ) => {
146205 // Calcula tempo de resposta em segundos
147206 const responseTimeSeconds = ( Date . now ( ) - questionStartTime . current ) / 1000 ;
148207
@@ -166,50 +225,12 @@ export function GamePage() {
166225 starsEarned
167226 } ) ;
168227 }
169- } ;
170-
171- /**
172- * Toca um som de celebração usando AudioContext (sem arquivos externos)
173- */
174- const playSuccessSound = ( ) => {
175- try {
176- // eslint-disable-next-line @typescript-eslint/no-explicit-any
177- const AudioContext = window . AudioContext || ( window as any ) . webkitAudioContext ;
178- if ( ! AudioContext ) return ;
179-
180- const ctx = new AudioContext ( ) ;
181- const oscillators = [
182- { freq : 523.25 , type : 'sine' , start : 0 , dur : 0.2 } , // C5
183- { freq : 659.25 , type : 'sine' , start : 0.1 , dur : 0.2 } , // E5
184- { freq : 783.99 , type : 'sine' , start : 0.2 , dur : 0.4 } , // G5
185- { freq : 1046.50 , type : 'sine' , start : 0.3 , dur : 0.6 } // C6
186- ] ;
187-
188- oscillators . forEach ( ( { freq, type, start, dur } ) => {
189- const osc = ctx . createOscillator ( ) ;
190- const gain = ctx . createGain ( ) ;
191-
192- osc . type = type as OscillatorType ;
193- osc . frequency . setValueAtTime ( freq , ctx . currentTime ) ;
194-
195- gain . gain . setValueAtTime ( 0.3 , ctx . currentTime ) ;
196- gain . gain . exponentialRampToValueAtTime ( 0.01 , ctx . currentTime + dur ) ;
197-
198- osc . connect ( gain ) ;
199- gain . connect ( ctx . destination ) ;
200-
201- osc . start ( ctx . currentTime + start ) ;
202- osc . stop ( ctx . currentTime + start + dur ) ;
203- } ) ;
204- } catch ( e ) {
205- console . error ( 'Audio play failed' , e ) ;
206- }
207- } ;
228+ } , [ currentQuestion , getQuestionProgress , recordAttempt , recordQuestionCompleted ] ) ;
208229
209230 /**
210231 * Vai para a próxima questão não resolvida ou volta para a lista
211232 */
212- const handleNext = ( ) => {
233+ const handleNext = useCallback ( ( ) => {
213234 let nextIndex = currentQuestionIndex + 1 ;
214235 let foundUnresolved = false ;
215236
@@ -274,45 +295,34 @@ export function GamePage() {
274295 setView ( 'world-questions' ) ;
275296 setCurrentQuestion ( null ) ;
276297 }
277- } ;
298+ } , [
299+ currentQuestionIndex ,
300+ worldQuestions ,
301+ getQuestionProgress ,
302+ selectedWorld ,
303+ worldProgress ,
304+ playSuccessSound ,
305+ checkWorldAchievements ,
306+ isPerfectRun ,
307+ navigate
308+ ] ) ;
278309
279310 /**
280311 * Volta para a lista de questões do mundo
281312 */
282- const handleBackToQuestions = ( ) => {
313+ const handleBackToQuestions = useCallback ( ( ) => {
283314 setView ( 'world-questions' ) ;
284315 setCurrentQuestion ( null ) ;
285- } ;
316+ } , [ ] ) ;
286317
287318 /**
288319 * Volta para o mapa de mundos
289320 */
290- const handleBackToMap = ( ) => {
321+ const handleBackToMap = useCallback ( ( ) => {
291322 setView ( 'world-map' ) ;
292323 setSelectedWorld ( null ) ;
293324 setWorldQuestions ( [ ] ) ;
294- } ;
295-
296- /**
297- * Obtém o nome amigável do mundo
298- */
299- const getWorldName = ( world : World ) : string => {
300- const names : Record < World , string > = {
301- basic_commands : 'Primeiros Passos' ,
302- numbers : 'Números Mágicos' ,
303- variables : 'Mundo das Variáveis' ,
304- conditions : 'Terra das Decisões' ,
305- decisions : 'Terra das Decisões' ,
306- loops : 'Ilha da Repetição' ,
307- functions : 'Vale das Funções' ,
308- lists : 'Floresta das Listas' ,
309- strings : 'Reino das Palavras' ,
310- user_input : 'Conversando com o Usuário' ,
311- dictionaries : 'Agenda Mágica' ,
312- error_handling : 'Caçando Bugs' ,
313- } ;
314- return names [ world ] || world ;
315- } ;
325+ } , [ ] ) ;
316326
317327 // Mostra loading do Pyodide ou questões
318328 if ( ! ready || questionsLoading ) {
0 commit comments