428 lines
13 KiB
JavaScript
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,
|
|
};
|