diff --git a/app/src/App.jsx b/app/src/App.jsx index 1675cac..c49e35f 100644 --- a/app/src/App.jsx +++ b/app/src/App.jsx @@ -31,6 +31,8 @@ const OrdenacaoGame = lazy(() => import("./atividades/programacao/ordenacao/Orde const PuzzleGame = lazy(() => import("./atividades/programacao/puzzle/PuzzleGame")); const TurtleGame = lazy(() => import("./atividades/programacao/turtle/TurtleGame")); const CriptoGame = lazy(() => import("./atividades/programacao/cripto/CriptoGame")); +const ExemploGame = lazy(() => import("./atividades/programacao/exemplo/ExemploGame")); +const ExemploGame2 = lazy(() => import("./atividades/programacao/exemplo2/ExemploGame2")); const LoadingFallback = () => (
} /> } /> } /> + } /> + } /> {/* Modal overlay routes — rendered on top of the background page */} diff --git a/app/src/atividades/programacao/exemplo/ExemploGame.jsx b/app/src/atividades/programacao/exemplo/ExemploGame.jsx new file mode 100644 index 0000000..4a45e0e --- /dev/null +++ b/app/src/atividades/programacao/exemplo/ExemploGame.jsx @@ -0,0 +1,47 @@ +import React, { useEffect, useMemo } from "react"; +import PropTypes from "prop-types"; +import GameBase from "../../../components/game/GameBase"; +import GameEditor from "../../../components/game/GameEditor"; +import BlocklyEditor from "../../../components/game/editors/BlocklyEditor"; +import { createGame } from "./game"; +import { gameConfig } from "./config/config"; +import { generateDynamicToolbox, registerBlocks } from "./blocks/blocks"; +import { GameStateProvider, useGameState } from "../../../contexts/GameStateContext"; + +function ExemploContent() { + const { setFailureMessage, isDebugMode } = useGameState(); + + // Registra os blocos customizados no Blockly ao montar o componente + useEffect(() => { + registerBlocks(); + }, []); + + // Memoriza o gerador para evitar recriações desnecessárias do toolbox + const toolboxGenerator = useMemo(() => { + return (allowedBlocks) => generateDynamicToolbox(allowedBlocks); + }, []); + + return ( + + + + + + ); +} + +export default function ExemploGame() { + return ( + + + + ); +} + +ExemploContent.propTypes = {}; +ExemploGame.propTypes = {}; diff --git a/app/src/atividades/programacao/exemplo/blocks/blocks.js b/app/src/atividades/programacao/exemplo/blocks/blocks.js new file mode 100644 index 0000000..10e6187 --- /dev/null +++ b/app/src/atividades/programacao/exemplo/blocks/blocks.js @@ -0,0 +1,58 @@ +"use strict"; + +import "blockly/blocks"; +import { CORES_CUSTOMIZADAS } from "@/blockly/blocklyColors"; +import { configurarGerador, gerarStatement } from "@/blockly/generator"; +import { gerarToolboxDeEstrutura } from "@/blockly/toolbox"; +import { criarBlocoStatementSimples } from "@/blockly/blockFactory"; + +// controls_repeat_ext e math_number são blocos nativos do Blockly (não precisam de defineBlocks) +const ESTRUTURA_TOOLBOX = [ + { + nome: "Repetição", + cssContainer: "cat_repeticao", + blocos: ["controls_repeat_ext"], + }, + { + nome: "Movimento", + cor: CORES_CUSTOMIZADAS.MOVIMENTO, + cssContainer: "cat_movimento", + blocos: ["exemplo_mover_direita", "exemplo_mover_baixo"], + }, + { + nome: "Matemática", + cssContainer: "cat_matematica", + blocos: ["math_number"], + }, +]; + +export const registerBlocks = () => { + defineBlocks(); + defineGenerators(); +}; + +export const generateDynamicToolbox = (allowedBlocks = []) => { + return gerarToolboxDeEstrutura(ESTRUTURA_TOOLBOX, allowedBlocks); +}; + +const defineBlocks = () => { + criarBlocoStatementSimples( + "exemplo_mover_direita", + "mover para DIREITA", + CORES_CUSTOMIZADAS.MOVIMENTO + ); + + criarBlocoStatementSimples( + "exemplo_mover_baixo", + "mover para BAIXO", + CORES_CUSTOMIZADAS.MOVIMENTO + ); +}; + +const defineGenerators = () => { + // Ativa o prefix de highlight no gerador (necessário para feedback visual dos blocos) + configurarGerador(); + + gerarStatement("exemplo_mover_direita", "moverDireita"); + gerarStatement("exemplo_mover_baixo", "moverBaixo"); +}; diff --git a/app/src/atividades/programacao/exemplo/config/config.js b/app/src/atividades/programacao/exemplo/config/config.js new file mode 100644 index 0000000..f913a9d --- /dev/null +++ b/app/src/atividades/programacao/exemplo/config/config.js @@ -0,0 +1,51 @@ +export const gameConfig = { + gameId: "exemplo", + gameName: "Exemplo", + type: "blocks", + icon: "🎯", + thumbnail: "/images/atividades/programacao/exemplo-thumbnail.png", + descricao: + "Atividade de demonstração arquitetural. Guie o personagem até o alvo usando blocos de movimento e repetição.", + dificuldade: "Iniciante", + categoria: "Lógica", + tempoEstimado: "5 min", + conceitos: ["Sequenciamento", "Repetição"], + route: "/atividades/programacao/exemplo", + component: "ExemploGame", + objectives: [ + "Demonstrar o fluxo completo de instanciação, execução e validação da plataforma Decoda", + ], + metadata: { + lastUpdated: "2026-06-24", + version: "1.1.0", + }, + + fases: [ + { + id: 1, + nome: "Fase 1: Chegue ao Alvo", + descricao: + "Use 'repetir N vezes' com 'mover para DIREITA' e 'mover para BAIXO' para guiar o personagem até o alvo verde. Dica: a solução ótima usa apenas 6 blocos!", + timeout: 15, + // Limite pedagógico: força o aluno a usar laços em vez de repetir blocos manualmente + maxBlocks: 6, + allowedBlocks: [ + "controls_repeat_ext", + "math_number", + "exemplo_mover_direita", + "exemplo_mover_baixo", + ], + // Grade 5×5; jogador em (0,0), alvo em (4,4) + cols: 5, + rows: 5, + jogadorInicio: { col: 0, row: 0 }, + alvo: { col: 4, row: 4 }, + }, + ], + + mensagens: { + semMovimento: "O personagem não se mexeu! Use os blocos de movimento.", + saiu: "O personagem saiu da tela! Cuidado com os limites da grade.", + naoAlcancou: "O personagem não chegou ao alvo. Tente de novo!", + }, +}; diff --git a/app/src/atividades/programacao/exemplo/game.js b/app/src/atividades/programacao/exemplo/game.js new file mode 100644 index 0000000..edcb1f5 --- /dev/null +++ b/app/src/atividades/programacao/exemplo/game.js @@ -0,0 +1,194 @@ +import Phaser from "phaser"; +import { BaseGameScene } from "../../../shared/BaseGameScene.js"; +import { setupExemploAPI } from "./hooks/setupExemploAPI.js"; +import { validationSolution } from "./validation/validators.js"; +import { gameConfig } from "./config/config.js"; +import { Assets, Constantes } from "./ui/constants.js"; +import { montarGrade, criarAlvo, criarJogador } from "./ui/layout.js"; + +export class ExemploScene extends BaseGameScene { + constructor() { + super("ExemploScene"); + this.jogadorLogico = { col: 0, row: 0 }; + this.jogadorSprite = null; + this.alvoSprite = null; + this._gridGraphics = null; + this.executionStopped = false; + } + + preload() { + this.preloadGlobalAssets(); + // Carrega o logo via URL pública — assets em /public não precisam de import de módulo + this.load.image(Assets.CHAVES.LOGO, Assets.PATHS.LOGO); + } + + create() { + this.validatorFunc = (historico) => + validationSolution(historico, this.configFase, gameConfig, this); + + // Registra a API do interpreter e os handlers de run/reset via BaseGameScene + this.setupStandardController( + () => setupExemploAPI(this, { animationSpeed: 200 }), + this.validatorFunc + ); + + this.montarFase(); + } + + onBeforeRun() { + this.isRunning = true; + this.historico = []; + this.executionStopped = false; + } + + onReset() { + this.isRunning = false; + this.executionStopped = true; + this.montarFase(); + } + + async onSuccess() { + this.isRunning = false; + return new Promise((resolve) => { + this.tweens.add({ + targets: this.jogadorSprite, + scaleX: 1.6, + scaleY: 1.6, + duration: 180, + yoyo: true, + repeat: 2, + ease: "Back.easeOut", + onComplete: resolve, + }); + }); + } + + async onFailure() { + this.isRunning = false; + return new Promise((resolve) => { + this.tweens.add({ + targets: this.jogadorSprite, + x: "+=6", + yoyo: true, + repeat: 8, + duration: 40, + onComplete: resolve, + }); + }); + } + + montarFase() { + if (this._gridGraphics) this._gridGraphics.destroy(); + if (this.alvoSprite) this.alvoSprite.destroy(); + if (this.jogadorSprite) this.jogadorSprite.destroy(); + + const cfg = this.configFase; + const cols = cfg?.cols || Constantes.COLS; + const rows = cfg?.rows || Constantes.ROWS; + const inicio = cfg?.jogadorInicio || { col: 0, row: 0 }; + const alvo = cfg?.alvo || { col: 4, row: 4 }; + + this._gridGraphics = montarGrade(this, cols, rows); + this.alvoSprite = criarAlvo(this, alvo.col, alvo.row); + this.jogadorLogico = { col: inicio.col, row: inicio.row }; + this.jogadorSprite = criarJogador(this, inicio.col, inicio.row); + this.executionStopped = false; + } + + // --- API exposta ao js-interpreter via setupExemploAPI --- + + moverDireita() { + if (this.executionStopped) return Promise.resolve(); + + const novaCol = this.jogadorLogico.col + 1; + if (novaCol >= (this.configFase?.cols || Constantes.COLS)) { + return this._falharSaida(); + } + + this.jogadorLogico.col = novaCol; + this.historico.push({ tipo: "mover", direcao: "DIREITA", col: novaCol, row: this.jogadorLogico.row }); + return this._animarJogador(() => this._checarAlvo()); + } + + moverBaixo() { + if (this.executionStopped) return Promise.resolve(); + + const novaRow = this.jogadorLogico.row + 1; + if (novaRow >= (this.configFase?.rows || Constantes.ROWS)) { + return this._falharSaida(); + } + + this.jogadorLogico.row = novaRow; + this.historico.push({ tipo: "mover", direcao: "BAIXO", col: this.jogadorLogico.col, row: novaRow }); + return this._animarJogador(() => this._checarAlvo()); + } + + // Para o interpreter e agenda handleFailure quando o jogador sai da grade + _falharSaida() { + this.executionStopped = true; + this.gameInterpreter.stopInternal(); + this.time.delayedCall(100, () => + this.handleFailure(this.gameConfig?.mensagens?.saiu || "Saiu da tela!") + ); + return Promise.resolve(); + } + + // Anima o sprite até a posição lógica atual; executa onComplete ao término do tween + _animarJogador(onComplete) { + const { CELL_SIZE } = Constantes; + return new Promise((resolve) => { + this.tweens.add({ + targets: this.jogadorSprite, + x: this.jogadorLogico.col * CELL_SIZE + CELL_SIZE / 2, + y: this.jogadorLogico.row * CELL_SIZE + CELL_SIZE / 2, + duration: 150, + ease: "Power1", + onComplete: () => { + if (onComplete) onComplete(); + resolve(); + }, + }); + }); + } + + // Verifica se o jogador atingiu o alvo após cada movimento bem-sucedido + _checarAlvo() { + if (this.executionStopped) return; + const alvo = this.configFase?.alvo || { col: 4, row: 4 }; + if (this.jogadorLogico.col !== alvo.col || this.jogadorLogico.row !== alvo.row) return; + + this.executionStopped = true; + // Para o interpreter sem marcar como parado pelo usuário — validação ainda ocorre + this.gameInterpreter.stopInternal(); + this.time.delayedCall(300, () => { + if (this.validatorFunc) this.handleValidation(this.validatorFunc); + }); + } +} + +/** + * Factory para criar a configuração Phaser do jogo Exemplo. + * Injeta configFase e gameConfig no registry antes do boot da cena. + */ +export const createGame = (elementoPai, configFaseAtual) => { + const scene = new ExemploScene(); + const { CELL_SIZE, COLS, ROWS } = Constantes; + const cols = configFaseAtual?.cols || COLS; + const rows = configFaseAtual?.rows || ROWS; + + return { + type: Phaser.AUTO, + width: cols * CELL_SIZE, + height: rows * CELL_SIZE, + backgroundColor: "#1a1a2e", + parent: elementoPai, + scale: { mode: Phaser.Scale.FIT, autoCenter: Phaser.Scale.CENTER_BOTH }, + scene, + callbacks: { + preBoot: (game) => { + game.registry.set("configFase", configFaseAtual); + game.registry.set("gameConfig", gameConfig); + }, + }, + }; +}; diff --git a/app/src/atividades/programacao/exemplo/hooks/setupExemploAPI.js b/app/src/atividades/programacao/exemplo/hooks/setupExemploAPI.js new file mode 100644 index 0000000..641854a --- /dev/null +++ b/app/src/atividades/programacao/exemplo/hooks/setupExemploAPI.js @@ -0,0 +1,42 @@ +import { ApiHelpers } from "../../../../interpreters/ApiHelpers.js"; + +/** + * Retorna a função de init do js-interpreter para o jogo Exemplo. + * É aqui que a ponte entre o interpretador sandboxed e a cena Phaser é construída: + * cada função registrada expõe um método da cena como uma chamada segura para o aluno. + * + * @param {ExemploScene} scene - Instância da cena Phaser ativa + * @param {Object} config - Configurações opcionais (ex: animationSpeed) + * @returns {Function} Função de init com assinatura (interpreter, globalScope) + */ +export const setupExemploAPI = (scene, config) => { + const delay = (config && config.animationSpeed) || 200; + + return function (interpreter, globalScope) { + // Ações assíncronas: createAsyncFunction aguarda o callback para avançar o interpreter + ApiHelpers.registerFunction( + interpreter, + globalScope, + "moverDireita", + ApiHelpers.createActionWrapper(scene, "moverDireita", delay), + true // isAsync + ); + + ApiHelpers.registerFunction( + interpreter, + globalScope, + "moverBaixo", + ApiHelpers.createActionWrapper(scene, "moverBaixo", delay), + true // isAsync + ); + + // Necessário para o highlight visual dos blocos durante a execução + ApiHelpers.registerFunction( + interpreter, + globalScope, + "highlightBlock", + ApiHelpers.createHighlightWrapper(scene), + false + ); + }; +}; diff --git a/app/src/atividades/programacao/exemplo/ui/constants.js b/app/src/atividades/programacao/exemplo/ui/constants.js new file mode 100644 index 0000000..32cee87 --- /dev/null +++ b/app/src/atividades/programacao/exemplo/ui/constants.js @@ -0,0 +1,19 @@ +// Assets da pasta public/ são referenciados por URL direta, sem import de módulo. +// Esta é a forma correta em Vite para arquivos em /public. +export const Assets = { + CHAVES: { LOGO: "exemplo_logo" }, + PATHS: { LOGO: "/img/logo.png" }, +}; + +export const Constantes = { + CELL_SIZE: 80, + COLS: 5, + ROWS: 5, +}; + +// Cores no formato Phaser (0xRRGGBB) +export const Cores = { + FUNDO: 0x1a1a2e, + GRADE: 0x2d2d4e, + ALVO: 0x69f0ae, +}; diff --git a/app/src/atividades/programacao/exemplo/ui/layout.js b/app/src/atividades/programacao/exemplo/ui/layout.js new file mode 100644 index 0000000..c9e4652 --- /dev/null +++ b/app/src/atividades/programacao/exemplo/ui/layout.js @@ -0,0 +1,66 @@ +import { Assets, Constantes, Cores } from "./constants.js"; + +/** + * Desenha a grade de fundo da cena. + * @param {Phaser.Scene} scene + * @param {number} cols + * @param {number} rows + * @returns {Phaser.GameObjects.Graphics} + */ +export function montarGrade(scene, cols, rows) { + const { CELL_SIZE } = Constantes; + const g = scene.add.graphics(); + + g.fillStyle(Cores.FUNDO); + g.fillRect(0, 0, cols * CELL_SIZE, rows * CELL_SIZE); + g.lineStyle(1, Cores.GRADE, 1); + + for (let c = 0; c <= cols; c++) { + g.lineBetween(c * CELL_SIZE, 0, c * CELL_SIZE, rows * CELL_SIZE); + } + for (let r = 0; r <= rows; r++) { + g.lineBetween(0, r * CELL_SIZE, cols * CELL_SIZE, r * CELL_SIZE); + } + + return g; +} + +/** + * Cria o retângulo visual do alvo. + * @param {Phaser.Scene} scene + * @param {number} col + * @param {number} row + * @returns {Phaser.GameObjects.Rectangle} + */ +export function criarAlvo(scene, col, row) { + const { CELL_SIZE } = Constantes; + return scene.add + .rectangle( + col * CELL_SIZE + CELL_SIZE / 2, + row * CELL_SIZE + CELL_SIZE / 2, + CELL_SIZE - 8, + CELL_SIZE - 8, + Cores.ALVO + ) + .setDepth(1); +} + +/** + * Cria o sprite do jogador usando o logo do projeto. + * O asset é carregado em ExemploScene.preload() via this.load.image(). + * @param {Phaser.Scene} scene + * @param {number} col + * @param {number} row + * @returns {Phaser.GameObjects.Image} + */ +export function criarJogador(scene, col, row) { + const { CELL_SIZE } = Constantes; + return scene.add + .image( + col * CELL_SIZE + CELL_SIZE / 2, + row * CELL_SIZE + CELL_SIZE / 2, + Assets.CHAVES.LOGO + ) + .setDisplaySize(CELL_SIZE - 10, CELL_SIZE - 10) + .setDepth(10); +} diff --git a/app/src/atividades/programacao/exemplo/validation/validators.js b/app/src/atividades/programacao/exemplo/validation/validators.js new file mode 100644 index 0000000..b219bab --- /dev/null +++ b/app/src/atividades/programacao/exemplo/validation/validators.js @@ -0,0 +1,22 @@ +import { BaseGameValidator } from "../../../../shared/BaseGameValidator"; + +export class ExemploValidator extends BaseGameValidator { + validatePhase(history, config, gameConfig, sceneRef) { + // Lê a posição lógica final do jogador diretamente da cena (fonte de verdade) + const { jogadorLogico } = sceneRef; + const alvo = config?.alvo; + + if (jogadorLogico.col === alvo.col && jogadorLogico.row === alvo.row) { + return this.success(); + } + + return this.failure( + gameConfig?.mensagens?.naoAlcancou || "O personagem não chegou ao alvo. Tente de novo!" + ); + } +} + +export function validationSolution(history, config, gameConfig, sceneRef) { + const validator = new ExemploValidator(); + return validator.validate(history, config, gameConfig, sceneRef); +} diff --git a/app/src/atividades/programacao/exemplo2/ExemploGame2.jsx b/app/src/atividades/programacao/exemplo2/ExemploGame2.jsx new file mode 100644 index 0000000..77fe0dc --- /dev/null +++ b/app/src/atividades/programacao/exemplo2/ExemploGame2.jsx @@ -0,0 +1,45 @@ +import React, { useEffect, useMemo } from "react"; +import PropTypes from "prop-types"; +import GameBase from "../../../components/game/GameBase"; +import GameEditor from "../../../components/game/GameEditor"; +import BlocklyEditor from "../../../components/game/editors/BlocklyEditor"; +import { createGame } from "./game"; +import { gameConfig } from "./config/config"; +import { generateDynamicToolbox, registerBlocks } from "./blocks/blocks"; +import { GameStateProvider, useGameState } from "../../../contexts/GameStateContext"; + +function ExemploContent2() { + const { setFailureMessage } = useGameState(); + + useEffect(() => { + registerBlocks(); + }, []); + + const toolboxGenerator = useMemo(() => { + return (allowedBlocks) => generateDynamicToolbox(allowedBlocks); + }, []); + + return ( + + + + + + ); +} + +export default function ExemploGame2() { + return ( + + + + ); +} + +ExemploContent2.propTypes = {}; +ExemploGame2.propTypes = {}; diff --git a/app/src/atividades/programacao/exemplo2/blocks/blocks.js b/app/src/atividades/programacao/exemplo2/blocks/blocks.js new file mode 100644 index 0000000..12a0214 --- /dev/null +++ b/app/src/atividades/programacao/exemplo2/blocks/blocks.js @@ -0,0 +1,54 @@ +"use strict"; + +import "blockly/blocks"; +import { CORES_BLOCKLY } from "@/blockly/blocklyColors"; +import { configurarGerador, gerarStatementComValor } from "@/blockly/generator"; +import { gerarToolboxDeEstrutura } from "@/blockly/toolbox"; +import { criarBlocoStatementComValor } from "@/blockly/blockFactory"; + +// text e text_join são blocos nativos do Blockly; não precisam de defineBlocks +const ESTRUTURA_TOOLBOX = [ + { + nome: "Funções", + cor: CORES_BLOCKLY.VARIAVEIS, + cssContainer: "cat_saida", + blocos: ["exemplo2_imprimir"], + }, + { + nome: "Texto", + cor: CORES_BLOCKLY.TEXTO, + cssContainer: "cat_texto", + blocos: ["text", "text_join"], + }, +]; + +export const registerBlocks = () => { + defineBlocks(); + defineGenerators(); +}; + +export const generateDynamicToolbox = (allowedBlocks = []) => { + return gerarToolboxDeEstrutura(ESTRUTURA_TOOLBOX, allowedBlocks); +}; + +const defineBlocks = () => { + // Bloco statement que aceita qualquer valor de texto como input + criarBlocoStatementComValor( + "exemplo2_imprimir", + "imprimir", + "TEXTO", + null, // aceita qualquer tipo (String ou texto concatenado) + CORES_BLOCKLY.VARIAVEIS + ); +}; + +const defineGenerators = () => { + configurarGerador(); + + // gerarStatementComValor lê o input "TEXTO" e usa o template para gerar o código + gerarStatementComValor( + "exemplo2_imprimir", + "TEXTO", + (valor) => `imprimir(${valor})` + ); +}; diff --git a/app/src/atividades/programacao/exemplo2/config/config.js b/app/src/atividades/programacao/exemplo2/config/config.js new file mode 100644 index 0000000..34f613f --- /dev/null +++ b/app/src/atividades/programacao/exemplo2/config/config.js @@ -0,0 +1,43 @@ +export const gameConfig = { + gameId: "exemplo2", + gameName: "Exemplo 2: Texto", + type: "blocks", + icon: "💬", + thumbnail: "/images/atividades/programacao/exemplo2-thumbnail.png", + descricao: + "Atividade de demonstração com texto. Use o bloco 'imprimir' para exibir a frase correta.", + dificuldade: "Iniciante", + categoria: "Lógica", + tempoEstimado: "5 min", + conceitos: ["Texto", "Concatenação"], + route: "/atividades/programacao/exemplo2", + component: "ExemploGame2", + objectives: [ + "Demonstrar como trabalhar com strings e concatenação de texto na plataforma Decoda", + ], + metadata: { + lastUpdated: "2026-06-24", + version: "1.0.0", + }, + + fases: [ + { + id: 1, + nome: "Fase 1: Escreva a Frase", + descricao: + 'Use o bloco "imprimir" com o texto correto. Dica: você pode escrever tudo em um bloco de texto ou juntar duas palavras!', + timeout: 10, + // 4 blocos permite: imprimir + juntar + "SOBERANIA" + " DIGITAL" + // 2 blocos resolve na forma simples: imprimir + "SOBERANIA DIGITAL" + maxBlocks: 4, + allowedBlocks: ["exemplo2_imprimir", "text", "text_join"], + textoEsperado: "SOBERANIA DIGITAL", + }, + ], + + mensagens: { + semMovimento: "Use o bloco 'imprimir' para mostrar um texto!", + textoErrado: (atual, esperado) => + `Texto incorreto: "${atual}". O esperado é "${esperado}".`, + }, +}; diff --git a/app/src/atividades/programacao/exemplo2/game.js b/app/src/atividades/programacao/exemplo2/game.js new file mode 100644 index 0000000..15fea84 --- /dev/null +++ b/app/src/atividades/programacao/exemplo2/game.js @@ -0,0 +1,117 @@ +import Phaser from "phaser"; +import { BaseGameScene } from "../../../shared/BaseGameScene.js"; +import { setupExemplo2API } from "./hooks/setupExemplo2API.js"; +import { validationSolution } from "./validation/validators.js"; +import { gameConfig } from "./config/config.js"; +import { Constantes } from "./ui/constants.js"; +import { montarDisplay } from "./ui/layout.js"; + +export class Exemplo2Scene extends BaseGameScene { + constructor() { + super("Exemplo2Scene"); + this.textoAtual = ""; + this.textoDisplay = null; + } + + preload() { + this.preloadGlobalAssets(); + } + + create() { + this.validatorFunc = (historico) => + validationSolution(historico, this.configFase, gameConfig, this); + + // Registra a API do interpreter e os handlers de run/reset via BaseGameScene + this.setupStandardController( + () => setupExemplo2API(this), + this.validatorFunc + ); + + this.montarFase(); + } + + onBeforeRun() { + this.isRunning = true; + this.historico = []; + this.textoAtual = ""; + this.textoDisplay.setText(""); + this.textoDisplay.setColor("#e0e0ff"); + } + + onReset() { + this.isRunning = false; + this.textoAtual = ""; + this.textoDisplay.setText(""); + this.textoDisplay.setColor("#e0e0ff"); + } + + async onSuccess() { + this.isRunning = false; + this.textoDisplay.setColor("#69f0ae"); + return new Promise((resolve) => { + this.tweens.add({ + targets: this.textoDisplay, + scaleX: 1.1, + scaleY: 1.1, + duration: 150, + yoyo: true, + repeat: 2, + onComplete: () => { + this.textoDisplay.setColor("#e0e0ff"); + resolve(); + }, + }); + }); + } + + async onFailure() { + this.isRunning = false; + this.textoDisplay.setColor("#ff4444"); + return new Promise((resolve) => { + this.time.delayedCall(500, () => { + this.textoDisplay.setColor("#e0e0ff"); + resolve(); + }); + }); + } + + montarFase() { + if (this.textoDisplay) this.textoDisplay.destroy(); + this.textoAtual = ""; + // montarDisplay cria os objetos visuais e retorna a referência ao texto de saída + this.textoDisplay = montarDisplay(this); + } + + // --- API exposta ao js-interpreter via setupExemplo2API --- + + imprimir(texto) { + this.textoAtual = texto; + this.textoDisplay.setText(texto); + this.historico.push({ tipo: "imprimir", texto }); + } +} + +/** + * Factory para criar a configuração Phaser do jogo Exemplo2. + * Injeta configFase e gameConfig no registry antes do boot da cena. + */ +export const createGame = (elementoPai, configFaseAtual) => { + const scene = new Exemplo2Scene(); + const { LARGURA, ALTURA } = Constantes; + + return { + type: Phaser.AUTO, + width: LARGURA, + height: ALTURA, + backgroundColor: "#0a0a1a", + parent: elementoPai, + scale: { mode: Phaser.Scale.FIT, autoCenter: Phaser.Scale.CENTER_BOTH }, + scene, + callbacks: { + preBoot: (game) => { + game.registry.set("configFase", configFaseAtual); + game.registry.set("gameConfig", gameConfig); + }, + }, + }; +}; diff --git a/app/src/atividades/programacao/exemplo2/hooks/setupExemplo2API.js b/app/src/atividades/programacao/exemplo2/hooks/setupExemplo2API.js new file mode 100644 index 0000000..794fc61 --- /dev/null +++ b/app/src/atividades/programacao/exemplo2/hooks/setupExemplo2API.js @@ -0,0 +1,31 @@ +import { ApiHelpers } from "../../../../interpreters/ApiHelpers.js"; + +/** + * Retorna a função de init do js-interpreter para o jogo Exemplo2. + * + * @param {Exemplo2Scene} scene + * @returns {Function} Função de init com assinatura (interpreter, globalScope) + */ +export const setupExemplo2API = (scene) => { + return function (interpreter, globalScope) { + // imprimir(texto) é síncrona: apenas atualiza o estado da cena, sem aguardar animação. + // Por isso usa createNativeFunction em vez de createAsyncFunction. + const imprimirWrapper = interpreter.createNativeFunction((textoRaw) => { + // js-interpreter pode passar primitivos diretamente ou encapsulados em {data: ...} + const texto = + textoRaw !== null && typeof textoRaw === "object" && textoRaw.data !== undefined + ? String(textoRaw.data) + : String(textoRaw ?? ""); + scene.imprimir(texto); + }); + interpreter.setProperty(globalScope, "imprimir", imprimirWrapper); + + ApiHelpers.registerFunction( + interpreter, + globalScope, + "highlightBlock", + ApiHelpers.createHighlightWrapper(scene), + false + ); + }; +}; diff --git a/app/src/atividades/programacao/exemplo2/ui/constants.js b/app/src/atividades/programacao/exemplo2/ui/constants.js new file mode 100644 index 0000000..1f5f968 --- /dev/null +++ b/app/src/atividades/programacao/exemplo2/ui/constants.js @@ -0,0 +1,17 @@ +export const Constantes = { + LARGURA: 480, + ALTURA: 220, +}; + +// Cores no formato Phaser (0xRRGGBB) +export const Cores = { + FUNDO: 0x0a0a1a, + PAINEL: 0x111128, + BORDA: 0x3a3a6e, +}; + +// Estilos de texto Phaser +export const Fontes = { + SAIDA: { fontSize: "22px", fontFamily: "monospace", color: "#e0e0ff" }, + LABEL: { fontSize: "13px", fontFamily: "monospace", color: "#555577" }, +}; diff --git a/app/src/atividades/programacao/exemplo2/ui/layout.js b/app/src/atividades/programacao/exemplo2/ui/layout.js new file mode 100644 index 0000000..46ae46b --- /dev/null +++ b/app/src/atividades/programacao/exemplo2/ui/layout.js @@ -0,0 +1,34 @@ +import { Constantes, Cores, Fontes } from "./constants.js"; + +/** + * Cria o display de texto da cena. + * Retorna a referência ao Phaser.Text de saída para ser atualizado pela cena. + * + * @param {Phaser.Scene} scene + * @returns {Phaser.GameObjects.Text} + */ +export function montarDisplay(scene) { + const { LARGURA, ALTURA } = Constantes; + const cx = LARGURA / 2; + const cy = ALTURA / 2; + + // Fundo escuro + scene.add.rectangle(cx, cy, LARGURA, ALTURA, Cores.FUNDO).setDepth(0); + + // Painel com borda sutil + scene.add + .rectangle(cx, cy, LARGURA - 40, ALTURA - 70, Cores.PAINEL) + .setStrokeStyle(1, Cores.BORDA) + .setDepth(1); + + // Label "Saída:" + scene.add.text(30, 18, "Saída:", Fontes.LABEL).setDepth(2); + + // Texto de saída — começa vazio, atualizado por imprimir() + const textoDisplay = scene.add + .text(cx, cy, "", { ...Fontes.SAIDA, align: "center" }) + .setOrigin(0.5, 0.5) + .setDepth(2); + + return textoDisplay; +} diff --git a/app/src/atividades/programacao/exemplo2/validation/validators.js b/app/src/atividades/programacao/exemplo2/validation/validators.js new file mode 100644 index 0000000..4cefbb7 --- /dev/null +++ b/app/src/atividades/programacao/exemplo2/validation/validators.js @@ -0,0 +1,22 @@ +import { BaseGameValidator } from "../../../../shared/BaseGameValidator"; + +export class Exemplo2Validator extends BaseGameValidator { + validatePhase(history, config, gameConfig, sceneRef) { + const esperado = config?.textoEsperado || "SOBERANIA DIGITAL"; + const atual = sceneRef?.textoAtual ?? ""; + + if (atual === esperado) return this.success(); + + const msgErro = + atual && typeof gameConfig?.mensagens?.textoErrado === "function" + ? gameConfig.mensagens.textoErrado(atual, esperado) + : `Texto incorreto: "${atual}". O esperado é "${esperado}".`; + + return this.failure(msgErro); + } +} + +export function validationSolution(history, config, gameConfig, sceneRef) { + const validator = new Exemplo2Validator(); + return validator.validate(history, config, gameConfig, sceneRef); +}