Files
decoda/app/src/contexts/GameStateContext.jsx
2026-06-05 00:11:45 -03:00

428 lines
13 KiB
JavaScript

/**
* @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
* <GameStateProvider gameConfig={config}>
* <GameArea />
* </GameStateProvider>
*
* @context
* - {@link useGameState} - Para consumir este contexto
*/
export function GameStateProvider({ children, gameConfig }) {
return (
<GameProgressProvider gameConfig={gameConfig}>
<GameStateInnerProvider gameConfig={gameConfig}>
{children}
</GameStateInnerProvider>
</GameProgressProvider>
);
}
/**
* 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 (
<GameStateContext.Provider
value={{
// English API
executionState,
setExecutionState,
generatedCode,
setGeneratedCode,
currentBlockCount,
execute,
finalizeWithSuccess,
finalizeWithFailure,
restart,
stop,
currentPhase,
// public: change phase using `changePhase` behavior
setCurrentPhase: changePhase,
completedPhases,
setCompletedPhases,
resetProgress,
gameConfig,
registerExecutionFunction,
registerCodeEditorFunction,
onWorkspaceChange,
onCodeEditorChange,
// Wrapping fn in arrow prevents React from treating it as a state updater.
// useState(fn) calls fn(prevState) — storing a callback directly would cause
// handleWorkspaceChange(prevCallback) to fire during render (illegal side-effect).
setOnWorkspaceChange: (fn) => setOnWorkspaceChangeCallback(() => fn),
editorType,
setEditorType,
codeEditorContent,
failureMessage,
setFailureMessage,
isDebugMode,
setIsDebugMode,
}}
>
{children}
</GameStateContext.Provider>
);
}
/**
* 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 <button onClick={execute}>Executar</button>;
* }
*/
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,
};