/** * @fileoverview React component for GameStateContext.jsx * * @module contexts.GameStateContext */ import React, { createContext, useContext, useEffect, useState, useCallback, useRef, } from "react"; import PropTypes from "prop-types"; import { gameEventBus } from "../utils/gameEvents"; import { GameProgressProvider, useGameProgress } from "./GameProgressContext"; import { useGameActivityTracking } from "../services/analytics/useGameActivityTracking"; import { getAnalytics } from "../services/analytics/AnalyticsManager"; export const GAME_STATES = { PARADO: "parado", EXECUTANDO: "executando", SUCESSO: "sucesso", FALHA: "falha", }; const GameStateContext = createContext(); /** * Provedor de contexto global para o estado de execução do jogo. * Gerencia: código gerado, estado de execução, fases completadas, mensagens de erro. * * @component * @param {Object} props - Componente provider props * @param {React.ReactNode} props.children - Componentes filhos * @param {Object} props.gameConfig - Configuração do jogo (gameId, fases, etc) * @param {string} props.gameConfig.gameId - ID único do jogo * @param {Array} props.gameConfig.fases - Array de fases do jogo * * @returns {React.Context} BaseExecution context with executor methods and state * * @example * * * * * @context * - {@link useGameState} - Para consumir este contexto */ export function GameStateProvider({ children, gameConfig }) { return ( {children} ); } /** * Provider interno que consome GameProgressContext e gerencia estado de execução. * Expõe via GameStateContext todos os campos (progresso + execução) para * preservar compatibilidade com os consumidores existentes de useGameState(). * * @private */ function GameStateInnerProvider({ children, gameConfig }) { const { currentPhase, setCurrentPhase, completedPhases, setCompletedPhases, changePhase: progressChangePhase, resetProgress: progressResetProgress, } = useGameProgress(); const [executionState, setExecutionState] = useState(GAME_STATES.PARADO); const [generatedCode, setGeneratedCode] = useState(""); const [currentBlockCount, setCurrentBlockCount] = useState(0); const [onWorkspaceChangeCallback, setOnWorkspaceChangeCallback] = useState(null); const [editorType, setEditorType] = useState("blockly"); // 'blockly' ou 'code' const [codeEditorContent, setCodeEditorContent] = useState(""); const [failureMessage, setFailureMessage] = useState(""); const [isDebugMode, setIsDebugMode] = useState(() => { // Try both window.location.search and hash-based query params (for HashRouter) let urlParams = new URLSearchParams(window.location.search); let debugKey = Array.from(urlParams.keys()).find( (k) => k.toLowerCase() === "debug", ); // If not found in search, try hash (for HashRouter with ?debug=true after hash) if (!debugKey && window.location.hash.includes('?')) { const hashSearch = window.location.hash.split('?')[1]; urlParams = new URLSearchParams(hashSearch); debugKey = Array.from(urlParams.keys()).find( (k) => k.toLowerCase() === "debug", ); } const isDebug = urlParams.get(debugKey)?.toLowerCase() === "true"; return isDebug; }); const getCodeFromWorkspace = useRef(null); const getCodeFromEditor = useRef(null); // Rastrear atividade do jogo (analytics) useGameActivityTracking(gameConfig, { executionState, currentPhase, currentBlockCount, editorType, }); const trackGenericEvent = useCallback( (eventName, extraData = {}) => { if (!gameConfig?.gameId) return; const analytics = getAnalytics(); analytics.trackEvent(eventName, { atividade_id: gameConfig.gameId, atividade_nome: gameConfig.gameName || gameConfig.gameId, fase_numero: currentPhase, editor_tipo: editorType, blocos_atuais: currentBlockCount, ...extraData, }); }, [gameConfig?.gameId, gameConfig?.gameName, currentPhase, editorType, currentBlockCount], ); /** * Executa o código/workspace do editor de forma síncrona. * Registra o código gerado e muda estado para EXECUTANDO. * Valida se o editor registrou a função de execução antes de processar. * * @function execute * @returns {void} * @throws {console.error} Se editor não registrou a função de execução */ const execute = () => { trackGenericEvent("tentativa_execucao", { origem_acao: "botao_executar", }); trackGenericEvent("blocos_usados", { origem_acao: "execucao", }); if (editorType === "code") { if (getCodeFromEditor.current) { const codigo = getCodeFromEditor.current(); if (codigo && codigo.trim()) { setGeneratedCode(codigo); setExecutionState(GAME_STATES.EXECUTANDO); } else { console.error( "CodeEditor ainda não registrou sua função de execução.", ); } } } else { if (getCodeFromWorkspace.current) { const { codigo, workspace } = getCodeFromWorkspace.current(); if (codigo && workspace) { setGeneratedCode({ codigo, workspace }); setExecutionState(GAME_STATES.EXECUTANDO); } } else { console.error( "BlocklyEditor ainda não registrou sua função de execução.", ); } } }; /** * Marca a execução como bem-sucedida e registra a fase como completada. * Adiciona a fase ao array de fases concluídas se ainda não estiver incluída. * Persiste as mudanças no localStorage. * * @function finalizeWithSuccess * @returns {void} */ const finalizeWithSuccess = () => { setExecutionState(GAME_STATES.SUCESSO); if (!completedPhases.includes(currentPhase)) { setCompletedPhases([...completedPhases, currentPhase]); } }; /** * Marca a execução como falhada. * Atualiza o estado para FALHA sem limpar o código gerado. * Permite que o jogador veja o código e os erros para corrigir. * * @function finalizeWithFailure * @returns {void} */ const finalizeWithFailure = () => { setExecutionState(GAME_STATES.FALHA); }; /** * Reinicia o estado de execução e limpa o código gerado. * Usado para permitir nova execução após sucesso/falha sem mudar de fase. * Mantém a fase atual e fases completadas intactas. * * @function restart * @returns {void} */ const restart = () => { setExecutionState(GAME_STATES.PARADO); setGeneratedCode(""); }; /** * Remove todo o progresso salvo e reseta fases para a primeira. * Persiste a remoção no `localStorage` usando a chave do jogo. * * @function resetProgress * @returns {void} */ const resetProgress = () => { progressResetProgress(); }; /** * Navega para uma fase específica do jogo. * Reseta o estado de execução, limpa o código gerado e conta de blocos. * Usado quando o jogador seleciona uma nova fase no seletor. * * @function changePhase * @param {number} numeroFase - Número da fase para navegar (1-indexed) * @returns {void} */ const changePhase = (numeroFase, source = "unknown") => { if (source === "manual_selector") { trackGenericEvent("troca_fase_manual", { fase_origem: currentPhase, fase_destino: numeroFase, origem_acao: source, }); } progressChangePhase(numeroFase); setExecutionState(GAME_STATES.PARADO); setGeneratedCode(""); setCurrentBlockCount(0); setCodeEditorContent(""); }; /** * Para a execução atual e limpa o código gerado sem alterar a fase. * Usado para interromper execuções em andamento pelo usuário ou pela UI. * * @function stop * @returns {void} */ const stop = () => { // Avisar Phaser para parar antes de mudar estado React gameEventBus.stopExecution(); setExecutionState(GAME_STATES.PARADO); setGeneratedCode(""); }; /** * Registra a função que extrai código/workspace do editor Blockly. * A função registrada deve retornar um objeto `{ codigo, workspace }`. * * @function registerExecutionFunction * @param {Function} func - Função que retorna { codigo, workspace } * @returns {void} */ const registerExecutionFunction = useCallback((func) => { getCodeFromWorkspace.current = func; }, []); /** * Registra a função que retorna o código do editor de texto (code editor). * Usado quando `editorType` é `code` para obter o código atual. * * @function registerCodeEditorFunction * @param {Function} func - Função que retorna uma string com o código * @returns {void} */ const registerCodeEditorFunction = useCallback((func) => { getCodeFromEditor.current = func; }, []); /** * Callback disparado quando a workspace do Blockly sofre alterações. * Atualiza contador de blocos e repassa para callback externo se fornecido. * * @function onWorkspaceChange * @param {number} blockCount - Quantidade atual de blocos na workspace * @returns {void} */ const onWorkspaceChange = useCallback( (blockCount) => { setCurrentBlockCount(blockCount); if (onWorkspaceChangeCallback) { onWorkspaceChangeCallback(blockCount); } }, [onWorkspaceChangeCallback], ); /** * Callback para mudanças no editor de código (texto). * Atualiza o conteúdo e ajusta o contador de blocos (1 se houver código, 0 caso contrário). * * @function onCodeEditorChange * @param {string} content - Conteúdo atual do editor de código * @returns {void} */ const onCodeEditorChange = useCallback((content) => { setCodeEditorContent(content); setCurrentBlockCount(content.trim() ? 1 : 0); }, []); useEffect(() => { if (editorType === "code" && getCodeFromEditor.current) { setCurrentBlockCount( codeEditorContent && codeEditorContent.trim() ? 1 : 0, ); } else { setCodeEditorContent(0); } }, [editorType, codeEditorContent]); return ( setOnWorkspaceChangeCallback(() => fn), editorType, setEditorType, codeEditorContent, failureMessage, setFailureMessage, isDebugMode, setIsDebugMode, }} > {children} ); } /** * Hook customizado para consumir o contexto global de estado do jogo. * Fornece acesso aos estados de execução, progresso e métodos de controle do jogo. * * @hook * @returns {Object} Objeto com estados e métodos do jogo: * - Execução: executionState, execute(), finalizeWithSuccess(), finalizeWithFailure(), restart(), stop() * - Progresso: currentPhase, changePhase(fase), completedPhases, resetProgress() * - Código: generatedCode, codeEditorContent * - Registro: registerExecutionFunction(), registerCodeEditorFunction() * - Callbacks: onWorkspaceChange(), onCodeEditorChange() * - Mensagens: failureMessage, setFailureMessage() * - Debug: isDebugMode, setIsDebugMode() * - Compatibilidade: aliases em português (estadoExecucao, codigoGerado, etc) * * @throws {Error} Se usado fora de GameStateProvider * * @example * function MyComponent() { * const { executionState, execute, currentPhase } = useGameState(); * return ; * } */ export function useGameState() { const context = useContext(GameStateContext); if (!context) { throw new Error("useGameState deve ser usado dentro de GameStateProvider"); } return context; } GameStateProvider.propTypes = { children: PropTypes.node.isRequired, gameConfig: PropTypes.shape({ gameId: PropTypes.string.isRequired, fases: PropTypes.array.isRequired, }).isRequired, }; GameStateInnerProvider.propTypes = { children: PropTypes.node.isRequired, gameConfig: PropTypes.shape({ gameId: PropTypes.string.isRequired, fases: PropTypes.array.isRequired, }).isRequired, };