add templates #5
@@ -31,6 +31,8 @@ const OrdenacaoGame = lazy(() => import("./atividades/programacao/ordenacao/Orde
|
|||||||
const PuzzleGame = lazy(() => import("./atividades/programacao/puzzle/PuzzleGame"));
|
const PuzzleGame = lazy(() => import("./atividades/programacao/puzzle/PuzzleGame"));
|
||||||
const TurtleGame = lazy(() => import("./atividades/programacao/turtle/TurtleGame"));
|
const TurtleGame = lazy(() => import("./atividades/programacao/turtle/TurtleGame"));
|
||||||
const CriptoGame = lazy(() => import("./atividades/programacao/cripto/CriptoGame"));
|
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 = () => (
|
const LoadingFallback = () => (
|
||||||
<div
|
<div
|
||||||
@@ -99,6 +101,8 @@ function AppRoutes() {
|
|||||||
<Route path="/atividades/programacao/semaforo" element={<SemaforoGame />} />
|
<Route path="/atividades/programacao/semaforo" element={<SemaforoGame />} />
|
||||||
<Route path="/atividades/programacao/molemash" element={<MoleMashGame />} />
|
<Route path="/atividades/programacao/molemash" element={<MoleMashGame />} />
|
||||||
<Route path="/atividades/programacao/turtle" element={<TurtleGame />} />
|
<Route path="/atividades/programacao/turtle" element={<TurtleGame />} />
|
||||||
|
<Route path="/atividades/programacao/exemplo" element={<ExemploGame />} />
|
||||||
|
<Route path="/atividades/programacao/exemplo2" element={<ExemploGame2 />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|
||||||
{/* Modal overlay routes — rendered on top of the background page */}
|
{/* Modal overlay routes — rendered on top of the background page */}
|
||||||
|
|||||||
47
app/src/atividades/programacao/exemplo/ExemploGame.jsx
Normal file
47
app/src/atividades/programacao/exemplo/ExemploGame.jsx
Normal file
@@ -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 (
|
||||||
|
<GameBase
|
||||||
|
gameFactory={createGame}
|
||||||
|
gameConfig={gameConfig}
|
||||||
|
customFailureHandler={setFailureMessage}
|
||||||
|
failureHandler={setFailureMessage}
|
||||||
|
>
|
||||||
|
<GameEditor>
|
||||||
|
<BlocklyEditor toolboxGenerator={toolboxGenerator} />
|
||||||
|
</GameEditor>
|
||||||
|
</GameBase>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ExemploGame() {
|
||||||
|
return (
|
||||||
|
<GameStateProvider gameConfig={gameConfig}>
|
||||||
|
<ExemploContent />
|
||||||
|
</GameStateProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ExemploContent.propTypes = {};
|
||||||
|
ExemploGame.propTypes = {};
|
||||||
58
app/src/atividades/programacao/exemplo/blocks/blocks.js
Normal file
58
app/src/atividades/programacao/exemplo/blocks/blocks.js
Normal file
@@ -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");
|
||||||
|
};
|
||||||
51
app/src/atividades/programacao/exemplo/config/config.js
Normal file
51
app/src/atividades/programacao/exemplo/config/config.js
Normal file
@@ -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!",
|
||||||
|
},
|
||||||
|
};
|
||||||
194
app/src/atividades/programacao/exemplo/game.js
Normal file
194
app/src/atividades/programacao/exemplo/game.js
Normal file
@@ -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);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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
|
||||||
|
);
|
||||||
|
};
|
||||||
|
};
|
||||||
19
app/src/atividades/programacao/exemplo/ui/constants.js
Normal file
19
app/src/atividades/programacao/exemplo/ui/constants.js
Normal file
@@ -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,
|
||||||
|
};
|
||||||
66
app/src/atividades/programacao/exemplo/ui/layout.js
Normal file
66
app/src/atividades/programacao/exemplo/ui/layout.js
Normal file
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
45
app/src/atividades/programacao/exemplo2/ExemploGame2.jsx
Normal file
45
app/src/atividades/programacao/exemplo2/ExemploGame2.jsx
Normal file
@@ -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 (
|
||||||
|
<GameBase
|
||||||
|
gameFactory={createGame}
|
||||||
|
gameConfig={gameConfig}
|
||||||
|
customFailureHandler={setFailureMessage}
|
||||||
|
failureHandler={setFailureMessage}
|
||||||
|
>
|
||||||
|
<GameEditor>
|
||||||
|
<BlocklyEditor toolboxGenerator={toolboxGenerator} />
|
||||||
|
</GameEditor>
|
||||||
|
</GameBase>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ExemploGame2() {
|
||||||
|
return (
|
||||||
|
<GameStateProvider gameConfig={gameConfig}>
|
||||||
|
<ExemploContent2 />
|
||||||
|
</GameStateProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ExemploContent2.propTypes = {};
|
||||||
|
ExemploGame2.propTypes = {};
|
||||||
54
app/src/atividades/programacao/exemplo2/blocks/blocks.js
Normal file
54
app/src/atividades/programacao/exemplo2/blocks/blocks.js
Normal file
@@ -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})`
|
||||||
|
);
|
||||||
|
};
|
||||||
43
app/src/atividades/programacao/exemplo2/config/config.js
Normal file
43
app/src/atividades/programacao/exemplo2/config/config.js
Normal file
@@ -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}".`,
|
||||||
|
},
|
||||||
|
};
|
||||||
117
app/src/atividades/programacao/exemplo2/game.js
Normal file
117
app/src/atividades/programacao/exemplo2/game.js
Normal file
@@ -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);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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
|
||||||
|
);
|
||||||
|
};
|
||||||
|
};
|
||||||
17
app/src/atividades/programacao/exemplo2/ui/constants.js
Normal file
17
app/src/atividades/programacao/exemplo2/ui/constants.js
Normal file
@@ -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" },
|
||||||
|
};
|
||||||
34
app/src/atividades/programacao/exemplo2/ui/layout.js
Normal file
34
app/src/atividades/programacao/exemplo2/ui/layout.js
Normal file
@@ -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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user