desenvolvimento pre-lancamento
Commit inicial - add do repo privado para o repo NT style: changes header's logo and colors style: changes home page first session layout feat: creates about us home page section chore: creates home page section for whom chore: creates student materails home page section chore: creates teachers materials home page section chore: creates teacher materials home page section style: changes primary color style: changes color at activities page style: changes about page color style: changes name to Decoda fix: changes route to about page at footer fix: changes background color style: changes game page header colors style: changes footer colors chore: adds home page sections title style: changes main font family to Lato style: adds title font fix: changes sizes to be more responsive for mobile ajuste no build vercel atualiza regras envio homol Adiciona instrucoes de uso add JupyterLite fix solucao turtle Add Mole Mash e Modal de Falhas Add Progress Bar na pagina de Atividades fix game name chore: atualiza lockfile removendo vercel analytics inclusão de efeito ao mudar de fase add mecanismo de solução de fases em debug vite config test add BaseGame e refator do MoleMash refatoração turtle refatoração automato refatoração automato add tag bug 1 e 2 automato mostrar apenas games em homologação na pagina de atividades aumentar timeout das fases finais do Turtle fix bug scroll add video refactor semaforo arrumar ordem das cores add build docs update vercel update vercel update vercel update vercel update vercel add vercel jupyter add vercel jupyter fix deploy Vercel fix deploy Vercel fix deploy Vercel add cripto add cripto refatoração fix tour Mole Mash . remover arquivos de controle chore: adds development tag for activity card remover arquivos de status indevidamente versionados atualizar cores nas atividades add Quebra Cabeças add Quebra Cabeças add iniciativas add Iniciativas alteração de fotos pesadas fix menu mobile fix menu mobile fix menu mobile add Aspirador update icons update identidade visual documentação update jupyter add kernel python local add kernel python local add kernel python local feat: add health check feat: add primeiros passos add letramento mover letramento de lugar update path games update path games fix: ajuste clique rapido no botão executar remover dead code fix: refactor: extract shared utilities for storage, phase unlock and mobile detection stabilize context references and fix stale closure extrair GameProgressContext do GameStateContext (SRP) refactor(game): extrair usePhaser e useGameModals de GameBase + corrigir bugs descobertos refactor(game): remove todos os aliases PT/EN duplicados Remover aliases PT/EN da camada de modais refactor + tests security: add CodeSanitizer and integrate into GameInterpreter - CodeSanitizer.js: 4 built-in rules (max_length, infinite_while, infinite_for, excessive_nesting) with pluggable extra rules - GameInterpreter.executeCode: calls sanitizeCode() before js-interpreter, differentiates CodeSanitizationError (warn) from other errors (error) - 21 unit tests for CodeSanitizer (100% coverage) - 4 integration tests in GameInterpreter for sanitization paths add CodeSanitizer fix: fase 10 aspirador fix: bug semaforo teste feat: add version Ajusta a landing page para ficar mais próxima ao protótipo ajusta raio da borda do botão de Acesse nosso Laboratório pequenos ajustes de layout na página de iniciativas atualiza tabela de jogos educativos com os jogos disponíveis atualmente ajustados pequenos detalhes e informações do jogos na seção de guias pedagógicos troca nome playground para laboratório e adiciona imagens do lab adiciona documentação de conceitos básicos de programação ajustado pequenos erros de digitação adiciona tooltip com conceitos escondidos em hover na tag +N de conceitos update docs dev desativar tour setup matriz MoleMash setup matriz MoleMash fix: link update version update docs update docs mudou o layout de quem somos mudei as imgs dos icons e baixei o botao centraliza titulo com imagem e ajusta sessão com gradiente vermelho-rosa adiciona responsividade para a pagina quem somos ajusta botão de conheça nossa história ajustes ajustes na home + add. teclado update version security security feat: add tapume para telas pequenas v1.1.0 feat: decoda offline feat: doc offline offline fix: ajustes para release fix: navbar; config ordenação; versão fix: rotas docs e jupyter para pwa delete private files Co-authored-by: Indra Araujo <indra.araujo.santos@gmail.com> Co-authored-by: solange dos santos <sollangelive71@gmail.com>
53
app/src/atividades/letramento/letramentoRegistry.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import { MOUSE_ATIVIDADES_REGISTRY } from './mouse/mouseRegistry';
|
||||
import { TECLADO_ATIVIDADES_REGISTRY } from './teclado/tecladoRegistry';
|
||||
|
||||
// Registry of available letramento (digital literacy) activities.
|
||||
// Each category owns its own configuration file and this module only composes them.
|
||||
export const ATIVIDADES_REGISTRY = {
|
||||
...MOUSE_ATIVIDADES_REGISTRY,
|
||||
...TECLADO_ATIVIDADES_REGISTRY,
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} id
|
||||
* @returns {Object|null}
|
||||
*/
|
||||
export function getAtividade(id) {
|
||||
return ATIVIDADES_REGISTRY[id] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
export function listarAtividades() {
|
||||
return Object.values(ATIVIDADES_REGISTRY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lista atividades de uma categoria específica, ordenadas pela sequência da trilha
|
||||
* @param {string} categoria
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
export function listarAtividadesPorCategoria(categoria) {
|
||||
const atividades = Object.values(ATIVIDADES_REGISTRY).filter(
|
||||
(atividade) => atividade.categoria === categoria
|
||||
);
|
||||
|
||||
// Ordena pela sequência da trilha (começando pela que não aparece em 'proxima')
|
||||
const atividadesOrdenadas = [];
|
||||
const atividadesMap = new Map(atividades.map((a) => [a.id, a]));
|
||||
|
||||
// Encontra a primeira atividade (que não é 'proxima' de nenhuma outra)
|
||||
const proximasIds = new Set(atividades.map((a) => a.proxima).filter(Boolean));
|
||||
const primeira = atividades.find((a) => !proximasIds.has(a.id));
|
||||
|
||||
if (!primeira) return atividades; // Fallback caso haja ciclo
|
||||
|
||||
let atual = primeira;
|
||||
while (atual && atividadesOrdenadas.length < atividades.length) {
|
||||
atividadesOrdenadas.push(atual);
|
||||
atual = atual.proxima ? atividadesMap.get(atual.proxima) : null;
|
||||
}
|
||||
|
||||
return atividadesOrdenadas;
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
const channelToken = new URLSearchParams(window.location.hash.slice(1)).get('channelToken');
|
||||
|
||||
function notify(type, payload = {}) {
|
||||
window.parent.postMessage({ type, token: channelToken, ...payload }, '*');
|
||||
}
|
||||
|
||||
const arena = document.getElementById('arena');
|
||||
const banner = document.getElementById('successBanner');
|
||||
|
||||
let filesPlaced = 0;
|
||||
const TOTAL_FILES = 6;
|
||||
|
||||
notify('started');
|
||||
|
||||
// Create drop zone (folder)
|
||||
const dropZone = document.createElement('div');
|
||||
dropZone.className = 'absolute bottom-10 right-10 w-48 h-48 bg-yellow-100 border-4 border-yellow-300 rounded-xl flex flex-col items-center justify-center transition-all';
|
||||
dropZone.innerHTML = `
|
||||
<i data-lucide="folder" class="w-24 h-24 text-yellow-600 mb-2"></i>
|
||||
<p class="text-lg font-bold text-gray-800">Meus Arquivos</p>
|
||||
`;
|
||||
arena.appendChild(dropZone);
|
||||
lucide.createIcons();
|
||||
|
||||
// Create draggable files
|
||||
const fileTypes = [
|
||||
{ icon: 'file-text', color: 'red', name: 'Texto.txt' },
|
||||
{ icon: 'image', color: 'purple', name: 'Foto.jpg' },
|
||||
{ icon: 'music', color: 'green', name: 'Musica.mp3' },
|
||||
{ icon: 'video', color: 'blue', name: 'Video.mp4' },
|
||||
{ icon: 'file-code', color: 'orange', name: 'Codigo.js' },
|
||||
{ icon: 'file-archive', color: 'gray', name: 'Arquivo.zip' },
|
||||
];
|
||||
|
||||
fileTypes.forEach((file, i) => {
|
||||
createDraggableFile(file, i);
|
||||
});
|
||||
|
||||
function createDraggableFile(file, index) {
|
||||
const fileEl = document.createElement('div');
|
||||
fileEl.className = `absolute w-24 h-24 bg-white border-2 border-gray-300 rounded-lg flex flex-col items-center justify-center cursor-grab shadow-lg transition-all hover:shadow-xl`;
|
||||
const row = Math.floor(index / 3);
|
||||
const col = index % 3;
|
||||
fileEl.style.left = `${50 + col * 110}px`;
|
||||
fileEl.style.top = `${50 + row * 110}px`;
|
||||
fileEl.innerHTML = `
|
||||
<i data-lucide="${file.icon}" class="w-8 h-8 text-${file.color}-500 mb-1"></i>
|
||||
<p class="text-xs font-semibold text-gray-700">${file.name}</p>
|
||||
`;
|
||||
fileEl.draggable = true;
|
||||
|
||||
arena.appendChild(fileEl);
|
||||
lucide.createIcons();
|
||||
|
||||
fileEl.addEventListener('dragstart', (e) => {
|
||||
fileEl.classList.add('dragging');
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
});
|
||||
|
||||
fileEl.addEventListener('dragend', () => {
|
||||
fileEl.classList.remove('dragging');
|
||||
});
|
||||
}
|
||||
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('ring-4', 'ring-green-400', 'scale-105');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', () => {
|
||||
dropZone.classList.remove('ring-4', 'ring-green-400', 'scale-105');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('ring-4', 'ring-green-400', 'scale-105');
|
||||
|
||||
const dragging = document.querySelector('.dragging');
|
||||
if (dragging) {
|
||||
dragging.remove();
|
||||
filesPlaced++;
|
||||
notify('running', { step: filesPlaced });
|
||||
|
||||
if (filesPlaced >= TOTAL_FILES) {
|
||||
arena.classList.add('hidden');
|
||||
banner.classList.remove('hidden');
|
||||
lucide.createIcons();
|
||||
notify('success', { score: 100 });
|
||||
notify('completed', { score: 100 });
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Mouse - Arrastar</title>
|
||||
<link rel="stylesheet" href="../../shared/letramento.css">
|
||||
<script src="../../shared/lucide.js"></script>
|
||||
<style>
|
||||
body { overflow: hidden; height: 100vh; width: 100vw; }
|
||||
.dragging { opacity: 0.5; cursor: grabbing !important; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gradient-to-br from-brand-50 to-gray-100 flex items-center justify-center h-screen w-screen overflow-hidden p-4">
|
||||
<div class="w-full h-full flex flex-col">
|
||||
<div class="bg-white/95 backdrop-blur-sm border border-white/30 rounded-xl shadow-xl flex-1 flex flex-col overflow-hidden">
|
||||
<div class="flex items-center justify-center gap-4 p-4 border-b border-white/20">
|
||||
<i data-lucide="move" class="w-12 h-12 text-red-600"></i>
|
||||
<h2 class="text-2xl font-bold text-gray-800">Arraste os arquivos para a pasta</h2>
|
||||
</div>
|
||||
<div class="flex-1 p-6 overflow-hidden">
|
||||
<div id="arena" class="relative h-full bg-white/50 rounded-xl border-2 border-gray-200 shadow-inner overflow-hidden"></div>
|
||||
<div id="successBanner" class="hidden mt-6 bg-gradient-to-r from-green-50 to-emerald-50 border-2 border-green-300 rounded-xl p-8 flex items-center justify-center gap-4">
|
||||
<i data-lucide="check-circle" class="text-green-600 w-16 h-16"></i>
|
||||
<div>
|
||||
<h2 class="text-4xl font-bold text-green-700 mb-2">Muito bem!</h2>
|
||||
<p class="text-xl text-gray-700">Você aprendeu a arrastar!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="./activity.js"></script>
|
||||
<script>lucide.createIcons();</script>
|
||||
</body>
|
||||
</html>
|
||||
212
app/src/atividades/letramento/mouse/mouse-basico/activity.js
Normal file
@@ -0,0 +1,212 @@
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// Mouse Basic Activity - Logic
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
// ── postMessage helpers ──────────────────────────────────────────────
|
||||
const channelToken = new URLSearchParams(window.location.hash.slice(1)).get('channelToken');
|
||||
|
||||
function notify(type, payload = {}) {
|
||||
window.parent.postMessage({ type, token: channelToken, ...payload }, '*');
|
||||
}
|
||||
|
||||
// ── DOM Elements ─────────────────────────────────────────────────────
|
||||
const arena = document.getElementById('arena');
|
||||
const instrEl = document.getElementById('instruction');
|
||||
const hintEl = document.getElementById('hint');
|
||||
const coverWrap = document.getElementById('coverageWrap');
|
||||
const coverBar = document.getElementById('coverageBar');
|
||||
const progLabel = document.getElementById('progressLabel');
|
||||
const banner = document.getElementById('successBanner');
|
||||
const successMsg = document.getElementById('successMsg');
|
||||
|
||||
// ── State ────────────────────────────────────────────────────────────
|
||||
const TOTAL_STEPS = 3;
|
||||
let currentStep = 0;
|
||||
let started = false;
|
||||
|
||||
const steps = [
|
||||
{
|
||||
instruction: 'Passo 1 de 3: Mova o mouse pela área abaixo',
|
||||
hint: 'Explore toda a área movendo o mouse. Preencha pelo menos 60% dela.',
|
||||
setup: setupMoveStep,
|
||||
},
|
||||
{
|
||||
instruction: 'Passo 2 de 3: Clique no botão',
|
||||
hint: 'Mova o cursor até o botão e clique uma vez.',
|
||||
setup: setupClickStep,
|
||||
},
|
||||
{
|
||||
instruction: 'Passo 3 de 3: Dê um duplo clique no botão',
|
||||
hint: 'Dois cliques rápidos sobre o botão!',
|
||||
setup: setupDblClickStep,
|
||||
},
|
||||
];
|
||||
|
||||
// ── Rendering ────────────────────────────────────────────────────────
|
||||
function renderStep(step) {
|
||||
const s = steps[step];
|
||||
instrEl.textContent = s.instruction;
|
||||
hintEl.textContent = s.hint;
|
||||
clearArena();
|
||||
s.setup();
|
||||
}
|
||||
|
||||
function clearArena() {
|
||||
arena.innerHTML = '';
|
||||
arena.style.cursor = 'default';
|
||||
}
|
||||
|
||||
function nextStep(feedbackMsg) {
|
||||
if (!started) {
|
||||
notify('started', { step: currentStep + 1 });
|
||||
started = true;
|
||||
}
|
||||
|
||||
notify('running', { step: currentStep + 1, message: feedbackMsg });
|
||||
|
||||
currentStep++;
|
||||
|
||||
setTimeout(() => {
|
||||
if (currentStep >= TOTAL_STEPS) {
|
||||
finishActivity();
|
||||
} else {
|
||||
renderStep(currentStep);
|
||||
}
|
||||
}, 1200);
|
||||
}
|
||||
|
||||
function finishActivity() {
|
||||
instrEl.textContent = 'Você completou todos os passos!';
|
||||
hintEl.textContent = '';
|
||||
clearArena();
|
||||
arena.classList.add('hidden');
|
||||
coverWrap.classList.add('hidden');
|
||||
successMsg.textContent = 'Agora você sabe como mover e clicar com o mouse. Excelente trabalho!';
|
||||
banner.classList.remove('hidden');
|
||||
|
||||
// Reinitialize icons in success banner
|
||||
lucide.createIcons();
|
||||
|
||||
notify('success', { score: 100 });
|
||||
notify('completed', { score: 100 });
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// STEP 1: Move Mouse & Track Coverage
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
function setupMoveStep() {
|
||||
coverWrap.classList.remove('hidden');
|
||||
arena.style.cursor = 'crosshair';
|
||||
|
||||
const GRID_SIZE = 32;
|
||||
const cols = Math.ceil(arena.clientWidth / GRID_SIZE);
|
||||
const rows = Math.ceil(arena.clientHeight / GRID_SIZE);
|
||||
const totalCells = cols * rows;
|
||||
const visitedCells = new Set();
|
||||
|
||||
arena.addEventListener('mousemove', handleMouseMove);
|
||||
|
||||
function handleMouseMove(e) {
|
||||
const rect = arena.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
|
||||
// Track visited cells
|
||||
const col = Math.floor(x / GRID_SIZE);
|
||||
const row = Math.floor(y / GRID_SIZE);
|
||||
const cellId = `${col},${row}`;
|
||||
visitedCells.add(cellId);
|
||||
|
||||
const coverage = (visitedCells.size / totalCells) * 100;
|
||||
coverBar.style.width = `${coverage}%`;
|
||||
progLabel.textContent = `${Math.floor(coverage)}%`;
|
||||
|
||||
// Create cursor trail
|
||||
const trail = document.createElement('div');
|
||||
trail.className = 'absolute w-8 h-8 rounded-full bg-red-400/30 pointer-events-none';
|
||||
trail.style.left = `${x}px`;
|
||||
trail.style.top = `${y}px`;
|
||||
trail.style.transform = 'translate(-50%, -50%)';
|
||||
arena.appendChild(trail);
|
||||
|
||||
setTimeout(() => trail.remove(), 800);
|
||||
|
||||
// Success condition
|
||||
if (coverage >= 60) {
|
||||
arena.removeEventListener('mousemove', handleMouseMove);
|
||||
nextStep('Ótimo! Você explorou a área com sucesso!');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// STEP 2: Single Click
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
function setupClickStep() {
|
||||
arena.style.cursor = 'default';
|
||||
placeTarget(false);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// STEP 3: Double Click
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
function setupDblClickStep() {
|
||||
arena.style.cursor = 'default';
|
||||
placeTarget(true);
|
||||
}
|
||||
|
||||
function placeTarget(isDblClick) {
|
||||
const SIZE = 140;
|
||||
const maxX = arena.clientWidth - SIZE - 20;
|
||||
const maxY = arena.clientHeight - SIZE - 20;
|
||||
const x = Math.floor(Math.random() * maxX) + 10;
|
||||
const y = Math.floor(Math.random() * maxY) + 10;
|
||||
|
||||
const button = document.createElement('button');
|
||||
button.className = 'absolute bg-green-500 hover:bg-green-600 active:scale-95 text-white rounded-xl shadow-lg hover:shadow-xl transition-all duration-200 flex items-center justify-center gap-3 text-2xl font-bold';
|
||||
button.style.width = `${SIZE}px`;
|
||||
button.style.height = `${SIZE}px`;
|
||||
button.style.left = `${x}px`;
|
||||
button.style.top = `${y}px`;
|
||||
|
||||
// Add Lucide icon
|
||||
const icon = document.createElement('i');
|
||||
icon.setAttribute('data-lucide', isDblClick ? 'pointer' : 'hand');
|
||||
icon.className = 'w-16 h-16';
|
||||
button.appendChild(icon);
|
||||
|
||||
arena.appendChild(button);
|
||||
|
||||
// Initialize the icon
|
||||
lucide.createIcons();
|
||||
|
||||
if (isDblClick) {
|
||||
button.addEventListener('dblclick', () => {
|
||||
button.remove();
|
||||
nextStep('Incrível! Você aprendeu o duplo clique!');
|
||||
});
|
||||
|
||||
// Mobile fallback: two taps
|
||||
let tapCount = 0;
|
||||
let tapTimer;
|
||||
button.addEventListener('click', (e) => {
|
||||
tapCount++;
|
||||
if (tapCount === 1) {
|
||||
tapTimer = setTimeout(() => { tapCount = 0; }, 400);
|
||||
} else if (tapCount === 2) {
|
||||
clearTimeout(tapTimer);
|
||||
button.remove();
|
||||
nextStep('Incrível! Você aprendeu o duplo clique!');
|
||||
}
|
||||
e.stopPropagation();
|
||||
});
|
||||
} else {
|
||||
button.addEventListener('click', () => {
|
||||
button.remove();
|
||||
nextStep('Perfeito! Você clicou no alvo!');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── Initialize ───────────────────────────────────────────────────────
|
||||
renderStep(0);
|
||||
68
app/src/atividades/letramento/mouse/mouse-basico/index.html
Normal file
@@ -0,0 +1,68 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Mouse - Básico</title>
|
||||
|
||||
<link rel="stylesheet" href="../../shared/letramento.css">
|
||||
<script src="../../shared/lucide.js"></script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
overflow: hidden;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gradient-to-br from-brand-50 to-gray-100 flex items-center justify-center h-screen w-screen overflow-hidden p-4">
|
||||
<div class="w-full h-full flex flex-col">
|
||||
<!-- Activity Card -->
|
||||
<div class="bg-white/95 backdrop-blur-sm border border-white/30 rounded-xl shadow-xl flex-1 flex flex-col overflow-hidden">
|
||||
|
||||
<!-- Instructions Header -->
|
||||
<div class="flex items-center justify-center gap-4 p-4 border-b border-white/20">
|
||||
<i data-lucide="mouse-pointer" class="w-12 h-12 text-red-500"></i>
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-gray-800" id="instruction">Carregando...</h2>
|
||||
<p class="text-lg text-gray-600" id="hint"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content Container -->
|
||||
<div class="flex-1 p-6 overflow-hidden flex flex-col">
|
||||
|
||||
<!-- Progress Bar (Step 1 only) -->
|
||||
<div id="coverageWrap" class="hidden mb-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<p class="text-lg font-medium text-gray-700">Cobertura da área:</p>
|
||||
<p class="text-lg font-bold text-red-600" id="progressLabel">0%</p>
|
||||
</div>
|
||||
<div class="w-full h-6 bg-gray-200 rounded-full overflow-hidden shadow-inner">
|
||||
<div id="coverageBar" class="h-full bg-red-500 transition-all duration-300 rounded-full" style="width: 0%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Arena -->
|
||||
<div id="arena" class="relative flex-1 bg-white/50 rounded-xl border-2 border-gray-200 shadow-inner overflow-hidden"></div>
|
||||
|
||||
<!-- Success Banner -->
|
||||
<div id="successBanner" class="hidden mt-6 bg-gradient-to-r from-green-50 to-emerald-50 border-2 border-green-300 rounded-xl p-8 flex items-center justify-center gap-4">
|
||||
<i data-lucide="check-circle" class="text-green-600 w-16 h-16"></i>
|
||||
<div>
|
||||
<h2 class="text-4xl font-bold text-green-700 mb-2">Muito bem!</h2>
|
||||
<p class="text-xl text-gray-700" id="successMsg"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="./activity.js"></script>
|
||||
<script>
|
||||
// Initialize Lucide icons
|
||||
lucide.createIcons();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,73 @@
|
||||
const channelToken = new URLSearchParams(window.location.hash.slice(1)).get('channelToken');
|
||||
|
||||
function notify(type, payload = {}) {
|
||||
window.parent.postMessage({ type, token: channelToken, ...payload }, '*');
|
||||
}
|
||||
|
||||
const arena = document.getElementById('arena');
|
||||
const contextMenu = document.getElementById('contextMenu');
|
||||
const banner = document.getElementById('successBanner');
|
||||
|
||||
let currentStep = 0;
|
||||
|
||||
notify('started');
|
||||
|
||||
// Create target icon
|
||||
const target = document.createElement('div');
|
||||
target.className = 'flex items-center gap-3 bg-white border-2 border-gray-300 rounded-lg p-6 shadow-lg';
|
||||
target.innerHTML = `
|
||||
<i data-lucide="file" class="w-16 h-16 text-red-500"></i>
|
||||
<div>
|
||||
<p class="text-2xl font-bold text-gray-800">Documento.txt</p>
|
||||
<p class="text-sm text-gray-600">Clique com botão direito aqui</p>
|
||||
</div>
|
||||
`;
|
||||
arena.appendChild(target);
|
||||
lucide.createIcons();
|
||||
|
||||
target.addEventListener('contextmenu', (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (currentStep === 0) {
|
||||
notify('running', { step: 1 });
|
||||
currentStep = 1;
|
||||
}
|
||||
showContextMenu(e.pageX, e.pageY);
|
||||
});
|
||||
|
||||
function showContextMenu(x, y) {
|
||||
contextMenu.style.left = `${x - arena.getBoundingClientRect().left}px`;
|
||||
contextMenu.style.top = `${y - arena.getBoundingClientRect().top}px`;
|
||||
contextMenu.innerHTML = `
|
||||
<button class="w-full text-left px-4 py-2 hover:bg-gray-100 rounded flex items-center gap-2" data-action="open">
|
||||
<i data-lucide="folder-open" class="w-4 h-4"></i>
|
||||
Abrir
|
||||
</button>
|
||||
<button class="w-full text-left px-4 py-2 hover:bg-gray-100 rounded flex items-center gap-2" data-action="copy">
|
||||
<i data-lucide="copy" class="w-4 h-4"></i>
|
||||
Copiar
|
||||
</button>
|
||||
<button class="w-full text-left px-4 py-2 hover:bg-gray-100 rounded flex items-center gap-2" data-action="delete">
|
||||
<i data-lucide="trash-2" class="w-4 h-4"></i>
|
||||
Excluir
|
||||
</button>
|
||||
`;
|
||||
contextMenu.classList.remove('hidden');
|
||||
lucide.createIcons();
|
||||
|
||||
contextMenu.querySelectorAll('button').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
contextMenu.classList.add('hidden');
|
||||
target.remove();
|
||||
arena.classList.add('hidden');
|
||||
banner.classList.remove('hidden');
|
||||
lucide.createIcons();
|
||||
notify('success', { score: 100 });
|
||||
notify('completed', { score: 100 });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('click', () => {
|
||||
contextMenu.classList.add('hidden');
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Mouse - Botão Direito</title>
|
||||
<link rel="stylesheet" href="../../shared/letramento.css">
|
||||
<script src="../../shared/lucide.js"></script>
|
||||
<style>
|
||||
body { overflow: hidden; height: 100vh; width: 100vw; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gradient-to-br from-brand-50 to-gray-100 flex items-center justify-center h-screen w-screen overflow-hidden p-4">
|
||||
<div class="w-full h-full flex flex-col">
|
||||
<div class="bg-white/95 backdrop-blur-sm border border-white/30 rounded-xl shadow-xl flex-1 flex flex-col overflow-hidden">
|
||||
<div class="flex items-center justify-center gap-4 p-4 border-b border-white/20">
|
||||
<i data-lucide="mouse-pointer-click" class="w-12 h-12 text-red-600"></i>
|
||||
<h2 class="text-2xl font-bold text-gray-800">Clique com botão direito</h2>
|
||||
</div>
|
||||
<div class="flex-1 p-6 overflow-hidden">
|
||||
<div id="arena" class="relative h-full bg-white/50 rounded-xl border-2 border-gray-200 shadow-inner overflow-hidden flex items-center justify-center">
|
||||
<div id="contextMenu" class="hidden absolute bg-white border-2 border-gray-300 rounded-lg shadow-2xl p-2 min-w-[200px]"></div>
|
||||
</div>
|
||||
<div id="successBanner" class="hidden mt-6 bg-gradient-to-r from-green-50 to-emerald-50 border-2 border-green-300 rounded-xl p-8 flex items-center justify-center gap-4">
|
||||
<i data-lucide="check-circle" class="text-green-600 w-16 h-16"></i>
|
||||
<div>
|
||||
<h2 class="text-4xl font-bold text-green-700 mb-2">Muito bem!</h2>
|
||||
<p class="text-xl text-gray-700">Você aprendeu o botão direito!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="./activity.js"></script>
|
||||
<script>lucide.createIcons();</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,64 @@
|
||||
const channelToken = new URLSearchParams(window.location.hash.slice(1)).get('channelToken');
|
||||
|
||||
function notify(type, payload = {}) {
|
||||
window.parent.postMessage({ type, token: channelToken, ...payload }, '*');
|
||||
}
|
||||
|
||||
const arena = document.getElementById('arena');
|
||||
const banner = document.getElementById('successBanner');
|
||||
|
||||
let remaining = 10;
|
||||
|
||||
notify('started');
|
||||
|
||||
function createTargets() {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const SIZE = 80;
|
||||
const maxX = arena.clientWidth - SIZE - 20;
|
||||
const maxY = arena.clientHeight - SIZE - 20;
|
||||
|
||||
let x, y, overlapping;
|
||||
do {
|
||||
overlapping = false;
|
||||
x = Math.floor(Math.random() * maxX) + 10;
|
||||
y = Math.floor(Math.random() * maxY) + 10;
|
||||
|
||||
document.querySelectorAll('.target').forEach(existing => {
|
||||
const ex = parseInt(existing.style.left);
|
||||
const ey = parseInt(existing.style.top);
|
||||
const dist = Math.sqrt((x - ex) ** 2 + (y - ey) ** 2);
|
||||
if (dist < SIZE + 20) overlapping = true;
|
||||
});
|
||||
} while (overlapping);
|
||||
|
||||
const target = document.createElement('button');
|
||||
target.className = 'target absolute bg-gradient-to-br from-red-500 to-red-600 rounded-full shadow-lg hover:scale-110 transition-transform';
|
||||
target.style.width = `${SIZE}px`;
|
||||
target.style.height = `${SIZE}px`;
|
||||
target.style.left = `${x}px`;
|
||||
target.style.top = `${y}px`;
|
||||
|
||||
arena.appendChild(target);
|
||||
|
||||
target.addEventListener('click', () => {
|
||||
target.classList.remove('from-red-500', 'to-red-600');
|
||||
target.classList.add('from-green-400', 'to-green-500', 'opacity-50');
|
||||
target.disabled = true;
|
||||
|
||||
remaining--;
|
||||
notify('running', { remaining });
|
||||
|
||||
if (remaining === 0) {
|
||||
setTimeout(() => {
|
||||
arena.classList.add('hidden');
|
||||
banner.classList.remove('hidden');
|
||||
lucide.createIcons();
|
||||
notify('success', { score: 100 });
|
||||
notify('completed', { score: 100 });
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
createTargets();
|
||||
@@ -0,0 +1,35 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Mouse - Múltiplos Cliques</title>
|
||||
<link rel="stylesheet" href="../../shared/letramento.css">
|
||||
<script src="../../shared/lucide.js"></script>
|
||||
<style>
|
||||
body { overflow: hidden; height: 100vh; width: 100vw; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gradient-to-br from-brand-50 to-gray-100 flex items-center justify-center h-screen w-screen overflow-hidden p-4">
|
||||
<div class="w-full h-full flex flex-col">
|
||||
<div class="bg-white/95 backdrop-blur-sm border border-white/30 rounded-xl shadow-xl flex-1 flex flex-col overflow-hidden">
|
||||
<div class="flex items-center justify-center gap-4 p-4 border-b border-white/20">
|
||||
<i data-lucide="grid-3x3" class="w-12 h-12 text-red-600"></i>
|
||||
<h2 class="text-2xl font-bold text-gray-800">Clique em todos os círculos vermelhos</h2>
|
||||
</div>
|
||||
<div class="flex-1 p-6 overflow-hidden">
|
||||
<div id="arena" class="relative h-full bg-white/50 rounded-xl border-2 border-gray-200 shadow-inner overflow-hidden"></div>
|
||||
<div id="successBanner" class="hidden mt-6 bg-gradient-to-r from-green-50 to-emerald-50 border-2 border-green-300 rounded-xl p-8 flex items-center justify-center gap-4">
|
||||
<i data-lucide="check-circle" class="text-green-600 w-16 h-16"></i>
|
||||
<div>
|
||||
<h2 class="text-4xl font-bold text-green-700 mb-2">Muito bem!</h2>
|
||||
<p class="text-xl text-gray-700">Você clicou em todos!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="./activity.js"></script>
|
||||
<script>lucide.createIcons();</script>
|
||||
</body>
|
||||
</html>
|
||||
592
app/src/atividades/letramento/mouse/mouse-completo/activity.js
Normal file
@@ -0,0 +1,592 @@
|
||||
const channelToken = new URLSearchParams(window.location.hash.slice(1)).get('channelToken');
|
||||
|
||||
function notify(type, payload = {}) {
|
||||
window.parent.postMessage({ type, token: channelToken, ...payload }, '*');
|
||||
}
|
||||
|
||||
const arena = document.getElementById('arena');
|
||||
const instrEl = document.getElementById('instruction');
|
||||
const banner = document.getElementById('successBanner');
|
||||
|
||||
let currentChallenge = 0;
|
||||
const challenges = [
|
||||
{ name: 'Fase 1: Cliques Precisos', task: precisionChallenge },
|
||||
{ name: 'Fase 2: Sequência Rápida', task: sequenceSpeedChallenge },
|
||||
{ name: 'Fase 3: Seguir Trilha', task: pathFollowChallenge },
|
||||
{ name: 'Fase 4: Múltiplos Alvos', task: multiTargetChallenge },
|
||||
{ name: 'Fase 5: Menu de Contexto', task: contextMenuChallenge },
|
||||
{ name: 'Fase 6: Organizar Arquivos', task: fileOrganizerChallenge },
|
||||
{ name: 'Fase 7: Desenhar Círculo', task: drawCircleChallenge },
|
||||
{ name: 'Fase 8: Desafio Final', task: finalBossChallenge },
|
||||
];
|
||||
|
||||
notify('started');
|
||||
|
||||
function nextChallenge() {
|
||||
if (currentChallenge >= challenges.length) {
|
||||
finishActivity();
|
||||
return;
|
||||
}
|
||||
|
||||
arena.innerHTML = '';
|
||||
const challenge = challenges[currentChallenge];
|
||||
instrEl.textContent = challenge.name;
|
||||
notify('running', { step: currentChallenge + 1 });
|
||||
challenge.task();
|
||||
}
|
||||
|
||||
// Fase 1: Cliques em alvos progressivamente menores (precisão)
|
||||
function precisionChallenge() {
|
||||
let targets = 0;
|
||||
const sizes = [80, 60, 45, 35];
|
||||
|
||||
function createTarget() {
|
||||
if (targets >= sizes.length) {
|
||||
currentChallenge++;
|
||||
setTimeout(nextChallenge, 800);
|
||||
return;
|
||||
}
|
||||
|
||||
const target = document.createElement('button');
|
||||
const size = sizes[targets];
|
||||
const maxX = arena.clientWidth - size - 20;
|
||||
const maxY = arena.clientHeight - size - 20;
|
||||
|
||||
target.className = 'absolute bg-gradient-to-br from-red-500 to-red-600 rounded-full shadow-xl hover:scale-110 transition-transform';
|
||||
target.style.width = `${size}px`;
|
||||
target.style.height = `${size}px`;
|
||||
target.style.left = `${20 + Math.random() * maxX}px`;
|
||||
target.style.top = `${20 + Math.random() * maxY}px`;
|
||||
target.innerHTML = `<span class="text-white text-xl font-bold">${targets + 1}</span>`;
|
||||
|
||||
target.addEventListener('click', () => {
|
||||
target.remove();
|
||||
targets++;
|
||||
createTarget();
|
||||
});
|
||||
|
||||
arena.appendChild(target);
|
||||
}
|
||||
|
||||
createTarget();
|
||||
}
|
||||
|
||||
// Fase 2: Clicar números 1-5 o mais rápido possível
|
||||
function sequenceSpeedChallenge() {
|
||||
const startTime = Date.now();
|
||||
let current = 1;
|
||||
const total = 5;
|
||||
|
||||
for (let i = 1; i <= total; i++) {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'absolute w-20 h-20 bg-red-500 text-white text-2xl font-bold rounded-full shadow-lg hover:scale-110 transition-transform';
|
||||
btn.textContent = i;
|
||||
btn.style.left = `${Math.random() * (arena.clientWidth - 100)}px`;
|
||||
btn.style.top = `${Math.random() * (arena.clientHeight - 100)}px`;
|
||||
|
||||
btn.addEventListener('click', () => {
|
||||
if (i === current) {
|
||||
btn.classList.remove('bg-red-500', 'to-red-600');
|
||||
btn.classList.add('bg-green-400', 'to-green-500', 'opacity-50');
|
||||
// btn.style.background = '#10b981';
|
||||
btn.disabled = true;
|
||||
current++;
|
||||
|
||||
if (current > total) {
|
||||
const time = ((Date.now() - startTime) / 1000).toFixed(1);
|
||||
setTimeout(() => {
|
||||
instrEl.textContent = `Fase 2: Concluída em ${time}s!`;
|
||||
currentChallenge++;
|
||||
setTimeout(nextChallenge, 1500);
|
||||
}, 300);
|
||||
}
|
||||
} else {
|
||||
btn.classList.add('animate-bounce');
|
||||
setTimeout(() => btn.classList.remove('animate-bounce'), 500);
|
||||
}
|
||||
});
|
||||
|
||||
arena.appendChild(btn);
|
||||
}
|
||||
}
|
||||
|
||||
// Fase 3: Seguir caminho sem sair (CÓPIA EXATA da atividade mouse-controle)
|
||||
function pathFollowChallenge() {
|
||||
const width = arena.clientWidth;
|
||||
const height = arena.clientHeight;
|
||||
|
||||
// Criar SVG
|
||||
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||
svg.setAttribute('width', width);
|
||||
svg.setAttribute('height', height);
|
||||
svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
|
||||
svg.className = 'absolute inset-0';
|
||||
arena.appendChild(svg);
|
||||
|
||||
let started = false;
|
||||
let progress = 0;
|
||||
let completed = false; // Flag para evitar múltiplas conclusões
|
||||
const TARGET_PROGRESS = 90;
|
||||
|
||||
// Create curved path
|
||||
const pathData = `M 50,${height/2}
|
||||
Q ${width/4},${height/4} ${width/2},${height/2}
|
||||
T ${width-50},${height/2}`;
|
||||
|
||||
// Background path (wider, gray)
|
||||
const bgPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
||||
bgPath.setAttribute('d', pathData);
|
||||
bgPath.setAttribute('stroke', '#e5e7eb');
|
||||
bgPath.setAttribute('stroke-width', '120');
|
||||
bgPath.setAttribute('fill', 'none');
|
||||
bgPath.setAttribute('stroke-linecap', 'round');
|
||||
svg.appendChild(bgPath);
|
||||
|
||||
// Progress path (green)
|
||||
const progressPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
||||
progressPath.setAttribute('d', pathData);
|
||||
progressPath.setAttribute('stroke', '#22c55e');
|
||||
progressPath.setAttribute('stroke-width', '100');
|
||||
progressPath.setAttribute('fill', 'none');
|
||||
progressPath.setAttribute('stroke-linecap', 'round');
|
||||
progressPath.setAttribute('stroke-dasharray', '1000');
|
||||
progressPath.setAttribute('stroke-dashoffset', '1000');
|
||||
svg.appendChild(progressPath);
|
||||
|
||||
// Start point with pulse animation
|
||||
const startCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
||||
startCircle.setAttribute('cx', '50');
|
||||
startCircle.setAttribute('cy', height/2);
|
||||
startCircle.setAttribute('r', '30');
|
||||
startCircle.setAttribute('fill', '#ef4444');
|
||||
startCircle.setAttribute('cursor', 'pointer');
|
||||
startCircle.style.animation = 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite';
|
||||
svg.appendChild(startCircle);
|
||||
|
||||
// Pulse ring for extra visibility
|
||||
const pulseRing = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
||||
pulseRing.setAttribute('cx', '50');
|
||||
pulseRing.setAttribute('cy', height/2);
|
||||
pulseRing.setAttribute('r', '30');
|
||||
pulseRing.setAttribute('fill', 'none');
|
||||
pulseRing.setAttribute('stroke', '#ef4444');
|
||||
pulseRing.setAttribute('stroke-width', '3');
|
||||
svg.appendChild(pulseRing);
|
||||
|
||||
// Animate pulse ring
|
||||
pulseRing.innerHTML = '<animate attributeName="r" from="30" to="50" dur="1.5s" repeatCount="indefinite" /><animate attributeName="opacity" from="0.8" to="0" dur="1.5s" repeatCount="indefinite" />';
|
||||
|
||||
// Add "Click here" text
|
||||
const startText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||||
startText.setAttribute('x', '50');
|
||||
startText.setAttribute('y', height/2 + 60);
|
||||
startText.setAttribute('text-anchor', 'middle');
|
||||
startText.setAttribute('fill', '#ef4444');
|
||||
startText.setAttribute('font-size', '16');
|
||||
startText.setAttribute('font-weight', 'bold');
|
||||
startText.textContent = 'Clique aqui';
|
||||
svg.appendChild(startText);
|
||||
|
||||
// Add click to start
|
||||
startCircle.addEventListener('click', () => {
|
||||
if (!started) {
|
||||
started = true;
|
||||
startCircle.setAttribute('fill', '#22c55e');
|
||||
startCircle.style.animation = '';
|
||||
pulseRing.remove();
|
||||
startText.remove();
|
||||
}
|
||||
});
|
||||
|
||||
// End point
|
||||
const endCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
||||
endCircle.setAttribute('cx', width - 50);
|
||||
endCircle.setAttribute('cy', height/2);
|
||||
endCircle.setAttribute('r', '30');
|
||||
endCircle.setAttribute('fill', '#22c55e');
|
||||
svg.appendChild(endCircle);
|
||||
|
||||
// Track mouse movement
|
||||
svg.addEventListener('mousemove', (e) => {
|
||||
if (!started) return;
|
||||
|
||||
const rect = svg.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
|
||||
// Check if on path
|
||||
const pathLength = bgPath.getTotalLength();
|
||||
let minDist = Infinity;
|
||||
|
||||
for (let i = 0; i < pathLength; i += 10) {
|
||||
const point = bgPath.getPointAtLength(i);
|
||||
const dist = Math.sqrt((x - point.x) ** 2 + (y - point.y) ** 2);
|
||||
if (dist < minDist) minDist = dist;
|
||||
}
|
||||
|
||||
if (minDist < 60) {
|
||||
// On path
|
||||
const progressPercent = (x / width) * 100;
|
||||
if (progressPercent > progress) {
|
||||
progress = progressPercent;
|
||||
const offset = 1000 - (progress / 100) * 1000;
|
||||
progressPath.setAttribute('stroke-dashoffset', offset);
|
||||
|
||||
if (progress >= TARGET_PROGRESS && !completed) {
|
||||
// Avança para a próxima fase do desafio completo
|
||||
completed = true;
|
||||
currentChallenge++;
|
||||
setTimeout(nextChallenge, 800);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Fase 4: Clicar 8 alvos simultâneos
|
||||
function multiTargetChallenge() {
|
||||
let remaining = 8;
|
||||
|
||||
for (let i = 0; i < remaining; i++) {
|
||||
const target = document.createElement('button');
|
||||
target.className = 'absolute w-16 h-16 bg-gradient-to-br from-red-500 to-red-600 rounded-full shadow-lg hover:scale-110 transition-transform';
|
||||
target.style.left = `${50 + Math.random() * (arena.clientWidth - 150)}px`;
|
||||
target.style.top = `${50 + Math.random() * (arena.clientHeight - 150)}px`;
|
||||
|
||||
const bullseye = document.createElement('div');
|
||||
bullseye.className = 'absolute inset-0 flex items-center justify-center';
|
||||
bullseye.innerHTML = '<div class="w-6 h-6 bg-white rounded-full"></div>';
|
||||
target.appendChild(bullseye);
|
||||
|
||||
|
||||
target.addEventListener('click', () => {
|
||||
target.remove();
|
||||
remaining--;
|
||||
|
||||
if (remaining === 0) {
|
||||
currentChallenge++;
|
||||
setTimeout(nextChallenge, 800);
|
||||
}
|
||||
});
|
||||
|
||||
arena.appendChild(target);
|
||||
}
|
||||
|
||||
lucide.createIcons();
|
||||
}
|
||||
|
||||
// Fase 5: Menu de contexto e seleção correta
|
||||
function contextMenuChallenge() {
|
||||
const file = document.createElement('div');
|
||||
file.className = 'absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-32 h-32 bg-white border-2 border-gray-300 rounded-lg flex flex-col items-center justify-center gap-2 cursor-pointer';
|
||||
file.innerHTML = '<i data-lucide="file-text" class="w-16 h-16 text-blue-500"></i><span class="text-sm font-semibold">arquivo.txt</span>';
|
||||
arena.appendChild(file);
|
||||
|
||||
const menu = document.createElement('div');
|
||||
menu.className = 'absolute hidden bg-white rounded-lg shadow-2xl border border-gray-200 py-2 min-w-[180px]';
|
||||
menu.innerHTML = `
|
||||
<button class="w-full px-4 py-2 text-left hover:bg-gray-100 flex items-center gap-2" data-action="open">
|
||||
<i data-lucide="folder-open" class="w-4 h-4"></i>Abrir
|
||||
</button>
|
||||
<button class="w-full px-4 py-2 text-left hover:bg-gray-100 flex items-center gap-2" data-action="copy">
|
||||
<i data-lucide="copy" class="w-4 h-4"></i>Copiar
|
||||
</button>
|
||||
<button class="w-full px-4 py-2 text-left hover:bg-gray-100 flex items-center gap-2" data-action="delete">
|
||||
<i data-lucide="trash-2" class="w-4 h-4 text-red-500"></i>Excluir
|
||||
</button>
|
||||
<button class="w-full px-4 py-2 text-left hover:bg-green-100 text-green-700 font-semibold flex items-center gap-2" data-action="properties">
|
||||
<i data-lucide="info" class="w-4 h-4"></i>Propriedades ✓
|
||||
</button>
|
||||
`;
|
||||
arena.appendChild(menu);
|
||||
lucide.createIcons();
|
||||
|
||||
file.addEventListener('contextmenu', (e) => {
|
||||
e.preventDefault();
|
||||
menu.classList.remove('hidden');
|
||||
menu.style.left = `${e.clientX - arena.getBoundingClientRect().left}px`;
|
||||
menu.style.top = `${e.clientY - arena.getBoundingClientRect().top}px`;
|
||||
});
|
||||
|
||||
menu.querySelectorAll('button').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
if (btn.dataset.action === 'properties') {
|
||||
currentChallenge++;
|
||||
setTimeout(nextChallenge, 800);
|
||||
} else {
|
||||
menu.classList.add('hidden');
|
||||
btn.classList.add('bg-red-100');
|
||||
setTimeout(() => {
|
||||
btn.classList.remove('bg-red-100');
|
||||
menu.classList.add('hidden');
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
arena.addEventListener('click', () => {
|
||||
menu.classList.add('hidden');
|
||||
});
|
||||
}
|
||||
|
||||
// Fase 6: Arrastar 5 itens para pastas corretas
|
||||
function fileOrganizerChallenge() {
|
||||
const files = [
|
||||
{ name: 'foto.jpg', type: 'image', icon: 'image', folder: 'images' },
|
||||
{ name: 'musica.mp3', type: 'music', icon: 'music', folder: 'music' },
|
||||
{ name: 'video.mp4', type: 'video', icon: 'video', folder: 'videos' },
|
||||
{ name: 'doc.pdf', type: 'document', icon: 'file-text', folder: 'documents' },
|
||||
{ name: 'code.js', type: 'code', icon: 'code', folder: 'code' },
|
||||
];
|
||||
|
||||
const folders = [
|
||||
{ id: 'images', label: 'Imagens', color: 'blue' },
|
||||
{ id: 'music', label: 'Músicas', color: 'purple' },
|
||||
{ id: 'videos', label: 'Vídeos', color: 'red' },
|
||||
{ id: 'documents', label: 'Documentos', color: 'yellow' },
|
||||
{ id: 'code', label: 'Código', color: 'green' },
|
||||
];
|
||||
|
||||
let completed = 0;
|
||||
|
||||
// Criar pastas
|
||||
folders.forEach((folder, i) => {
|
||||
const div = document.createElement('div');
|
||||
div.className = `absolute w-28 h-28 border-4 border-dashed border-${folder.color}-400 bg-${folder.color}-50 rounded-xl flex flex-col items-center justify-center gap-1`;
|
||||
div.style.left = `${50 + i * 140}px`;
|
||||
div.style.bottom = '30px';
|
||||
div.innerHTML = `<i data-lucide="folder" class="w-8 h-8 text-${folder.color}-600"></i><span class="text-xs font-semibold text-${folder.color}-700">${folder.label}</span>`;
|
||||
div.dataset.folderId = folder.id;
|
||||
arena.appendChild(div);
|
||||
|
||||
div.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
div.classList.add('scale-110', 'shadow-xl');
|
||||
});
|
||||
|
||||
div.addEventListener('dragleave', () => {
|
||||
div.classList.remove('scale-110', 'shadow-xl');
|
||||
});
|
||||
|
||||
div.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
div.classList.remove('scale-110', 'shadow-xl');
|
||||
|
||||
const fileId = e.dataTransfer.getData('text/plain');
|
||||
const file = files.find(f => f.name === fileId);
|
||||
|
||||
if (file && file.folder === folder.id) {
|
||||
const dragged = document.querySelector(`[data-file="${fileId}"]`);
|
||||
if (dragged) {
|
||||
dragged.remove();
|
||||
completed++;
|
||||
|
||||
if (completed === files.length) {
|
||||
currentChallenge++;
|
||||
setTimeout(nextChallenge, 800);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
div.classList.add('animate-bounce');
|
||||
setTimeout(() => div.classList.remove('animate-bounce'), 500);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Criar arquivos
|
||||
files.forEach((file, i) => {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'absolute w-20 h-20 bg-white border-2 border-gray-300 rounded-lg cursor-grab flex flex-col items-center justify-center gap-1 hover:shadow-lg transition-shadow';
|
||||
div.style.left = `${100 + i * 100}px`;
|
||||
div.style.top = '80px';
|
||||
div.innerHTML = `<i data-lucide="${file.icon}" class="w-8 h-8 text-gray-700"></i><span class="text-xs font-medium truncate">${file.name}</span>`;
|
||||
div.draggable = true;
|
||||
div.dataset.file = file.name;
|
||||
|
||||
div.addEventListener('dragstart', (e) => {
|
||||
e.dataTransfer.setData('text/plain', file.name);
|
||||
div.classList.add('opacity-50');
|
||||
});
|
||||
|
||||
div.addEventListener('dragend', () => {
|
||||
div.classList.remove('opacity-50');
|
||||
});
|
||||
|
||||
arena.appendChild(div);
|
||||
});
|
||||
|
||||
lucide.createIcons();
|
||||
}
|
||||
|
||||
// Fase 7: Desenhar círculo com 60% de cobertura
|
||||
function drawCircleChallenge() {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = arena.clientWidth;
|
||||
canvas.height = arena.clientHeight;
|
||||
canvas.className = 'absolute inset-0';
|
||||
arena.appendChild(canvas);
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
const centerX = canvas.width / 2;
|
||||
const centerY = canvas.height / 2;
|
||||
const radius = 120;
|
||||
|
||||
// Desenha círculo guia pontilhado
|
||||
ctx.setLineDash([10, 10]);
|
||||
ctx.strokeStyle = '#9ca3af';
|
||||
ctx.lineWidth = 3;
|
||||
ctx.beginPath();
|
||||
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
|
||||
// Texto instrução
|
||||
ctx.fillStyle = '#6b7280';
|
||||
ctx.font = 'bold 16px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('Desenhe sobre o círculo (60% mínimo)', centerX, centerY - radius - 30);
|
||||
|
||||
let isDrawing = false;
|
||||
const segments = new Set();
|
||||
const totalSegments = 120;
|
||||
|
||||
canvas.addEventListener('mousedown', () => {
|
||||
isDrawing = true;
|
||||
});
|
||||
|
||||
canvas.addEventListener('mousemove', (e) => {
|
||||
if (!isDrawing) return;
|
||||
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
|
||||
const dist = Math.hypot(x - centerX, y - centerY);
|
||||
|
||||
if (Math.abs(dist - radius) < 25) {
|
||||
const angle = Math.atan2(y - centerY, x - centerX);
|
||||
const segmentId = Math.floor(((angle + Math.PI) / (2 * Math.PI)) * totalSegments);
|
||||
segments.add(segmentId);
|
||||
|
||||
// Desenha ponto
|
||||
ctx.fillStyle = '#ef4444';
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, 4, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
const coverage = (segments.size / totalSegments) * 100;
|
||||
|
||||
if (coverage >= 60) {
|
||||
isDrawing = false;
|
||||
setTimeout(() => {
|
||||
currentChallenge++;
|
||||
nextChallenge();
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
canvas.addEventListener('mouseup', () => {
|
||||
isDrawing = false;
|
||||
});
|
||||
}
|
||||
|
||||
// Fase 8: Desafio final - alvos móveis com tempo limitado
|
||||
function finalBossChallenge() {
|
||||
let score = 0;
|
||||
let timeLeft = 20;
|
||||
const targetCount = 25;
|
||||
|
||||
const timer = document.createElement('div');
|
||||
timer.className = 'absolute top-4 left-1/2 -translate-x-1/2 text-4xl font-bold text-red-600 bg-white px-6 py-2 rounded-full shadow-xl';
|
||||
arena.appendChild(timer);
|
||||
|
||||
const scoreDisplay = document.createElement('div');
|
||||
scoreDisplay.className = 'absolute top-4 right-4 text-2xl font-bold text-green-600 bg-white px-4 py-2 rounded-full shadow-xl';
|
||||
arena.appendChild(scoreDisplay);
|
||||
|
||||
function updateDisplay() {
|
||||
timer.textContent = `${timeLeft}s`;
|
||||
scoreDisplay.textContent = `${score}/${targetCount}`;
|
||||
}
|
||||
|
||||
function createMovingTarget() {
|
||||
const target = document.createElement('button');
|
||||
const size = 40 + Math.random() * 30;
|
||||
target.className = 'absolute bg-gradient-to-br from-red-500 to-orange-500 rounded-full shadow-xl hover:scale-125 transition-transform';
|
||||
target.style.width = `${size}px`;
|
||||
target.style.height = `${size}px`;
|
||||
|
||||
let x = Math.random() * (arena.clientWidth - size);
|
||||
let y = Math.random() * (arena.clientHeight - size);
|
||||
let vx = (Math.random() - 0.5) * 3;
|
||||
let vy = (Math.random() - 0.5) * 3;
|
||||
|
||||
target.style.left = `${x}px`;
|
||||
target.style.top = `${y}px`;
|
||||
|
||||
const moveInterval = setInterval(() => {
|
||||
x += vx;
|
||||
y += vy;
|
||||
|
||||
if (x < 0 || x > arena.clientWidth - size) vx *= -1;
|
||||
if (y < 0 || y > arena.clientHeight - size) vy *= -1;
|
||||
|
||||
target.style.left = `${x}px`;
|
||||
target.style.top = `${y}px`;
|
||||
}, 20);
|
||||
|
||||
target.addEventListener('click', () => {
|
||||
clearInterval(moveInterval);
|
||||
target.remove();
|
||||
score++;
|
||||
updateDisplay();
|
||||
|
||||
if (score >= targetCount) {
|
||||
clearInterval(gameInterval);
|
||||
setTimeout(() => {
|
||||
currentChallenge++;
|
||||
nextChallenge();
|
||||
}, 500);
|
||||
} else {
|
||||
createMovingTarget();
|
||||
}
|
||||
});
|
||||
|
||||
arena.appendChild(target);
|
||||
}
|
||||
|
||||
// Criar alvos iniciais
|
||||
for (let i = 0; i < 5; i++) {
|
||||
createMovingTarget();
|
||||
}
|
||||
|
||||
updateDisplay();
|
||||
|
||||
const gameInterval = setInterval(() => {
|
||||
timeLeft--;
|
||||
updateDisplay();
|
||||
|
||||
if (timeLeft <= 0) {
|
||||
clearInterval(gameInterval);
|
||||
if (score >= targetCount * 0.7) {
|
||||
alert(`Bom trabalho! ${score}/${targetCount} alvos`);
|
||||
currentChallenge++;
|
||||
nextChallenge();
|
||||
} else {
|
||||
alert(`Tempo esgotado! Você acertou ${score}/${targetCount}. Tente novamente!`);
|
||||
finalBossChallenge();
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function finishActivity() {
|
||||
arena.classList.add('hidden');
|
||||
banner.classList.remove('hidden');
|
||||
lucide.createIcons();
|
||||
notify('success', { score: 100 });
|
||||
notify('completed', { score: 100 });
|
||||
}
|
||||
|
||||
nextChallenge();
|
||||
@@ -0,0 +1,36 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Mouse - Desafio Completo</title>
|
||||
<link rel="stylesheet" href="../../shared/letramento.css">
|
||||
<script src="../../shared/lucide.js"></script>
|
||||
<style>
|
||||
body { overflow: hidden; height: 100vh; width: 100vw; }
|
||||
.dragging { opacity: 0.5; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gradient-to-br from-brand-50 to-gray-100 flex items-center justify-center h-screen w-screen overflow-hidden p-4">
|
||||
<div class="w-full h-full flex flex-col">
|
||||
<div class="bg-white/95 backdrop-blur-sm border border-white/30 rounded-xl shadow-xl flex-1 flex flex-col overflow-hidden">
|
||||
<div class="flex items-center justify-center gap-4 p-4 border-b border-white/20">
|
||||
<i data-lucide="trophy" class="w-12 h-12 text-red-600"></i>
|
||||
<h2 id="instruction" class="text-2xl font-bold text-gray-800">Desafio Completo</h2>
|
||||
</div>
|
||||
<div class="flex-1 p-6 overflow-hidden">
|
||||
<div id="arena" class="relative h-full bg-white/50 rounded-xl border-2 border-gray-200 shadow-inner overflow-hidden"></div>
|
||||
<div id="successBanner" class="hidden mt-6 bg-gradient-to-r from-green-50 to-emerald-50 border-2 border-green-300 rounded-xl p-8 flex items-center justify-center gap-4">
|
||||
<i data-lucide="check-circle" class="text-green-600 w-16 h-16"></i>
|
||||
<div>
|
||||
<h2 class="text-4xl font-bold text-green-700 mb-2">Parabéns!</h2>
|
||||
<p class="text-xl text-gray-700">Você dominou todas as habilidades do mouse!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="./activity.js"></script>
|
||||
<script>lucide.createIcons();</script>
|
||||
</body>
|
||||
</html>
|
||||
144
app/src/atividades/letramento/mouse/mouse-controle/activity.js
Normal file
@@ -0,0 +1,144 @@
|
||||
const channelToken = new URLSearchParams(window.location.hash.slice(1)).get('channelToken');
|
||||
|
||||
function notify(type, payload = {}) {
|
||||
window.parent.postMessage({ type, token: channelToken, ...payload }, '*');
|
||||
}
|
||||
|
||||
const arena = document.getElementById('arena');
|
||||
const svg = document.getElementById('pathSvg');
|
||||
const banner = document.getElementById('successBanner');
|
||||
|
||||
const width = arena.clientWidth;
|
||||
const height = arena.clientHeight;
|
||||
svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
|
||||
|
||||
let started = false;
|
||||
let onPath = true;
|
||||
let progress = 0;
|
||||
const TARGET_PROGRESS = 90;
|
||||
|
||||
notify('started');
|
||||
|
||||
// Create curved path
|
||||
const pathData = `M 50,${height/2}
|
||||
Q ${width/4},${height/4} ${width/2},${height/2}
|
||||
T ${width-50},${height/2}`;
|
||||
|
||||
// Background path (wider, gray)
|
||||
const bgPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
||||
bgPath.setAttribute('d', pathData);
|
||||
bgPath.setAttribute('stroke', '#e5e7eb');
|
||||
bgPath.setAttribute('stroke-width', '120');
|
||||
bgPath.setAttribute('fill', 'none');
|
||||
bgPath.setAttribute('stroke-linecap', 'round');
|
||||
svg.appendChild(bgPath);
|
||||
|
||||
// Progress path (green)
|
||||
const progressPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
||||
progressPath.setAttribute('d', pathData);
|
||||
progressPath.setAttribute('stroke', '#22c55e');
|
||||
progressPath.setAttribute('stroke-width', '100');
|
||||
progressPath.setAttribute('fill', 'none');
|
||||
progressPath.setAttribute('stroke-linecap', 'round');
|
||||
progressPath.setAttribute('stroke-dasharray', '1000');
|
||||
progressPath.setAttribute('stroke-dashoffset', '1000');
|
||||
svg.appendChild(progressPath);
|
||||
|
||||
// Start point with pulse animation
|
||||
const startCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
||||
startCircle.setAttribute('cx', '50');
|
||||
startCircle.setAttribute('cy', height/2);
|
||||
startCircle.setAttribute('r', '30');
|
||||
startCircle.setAttribute('fill', '#ef4444');
|
||||
startCircle.setAttribute('cursor', 'pointer');
|
||||
startCircle.classList.add('pulse-circle');
|
||||
svg.appendChild(startCircle);
|
||||
|
||||
// Pulse ring for extra visibility
|
||||
const pulseRing = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
||||
pulseRing.setAttribute('cx', '50');
|
||||
pulseRing.setAttribute('cy', height/2);
|
||||
pulseRing.setAttribute('r', '30');
|
||||
pulseRing.setAttribute('fill', 'none');
|
||||
pulseRing.setAttribute('stroke', '#ef4444');
|
||||
pulseRing.setAttribute('stroke-width', '3');
|
||||
svg.appendChild(pulseRing);
|
||||
|
||||
// Animate pulse ring
|
||||
const animatePulseRing = () => {
|
||||
pulseRing.innerHTML = '<animate attributeName="r" from="30" to="50" dur="1.5s" repeatCount="indefinite" /><animate attributeName="opacity" from="0.8" to="0" dur="1.5s" repeatCount="indefinite" />';
|
||||
};
|
||||
animatePulseRing();
|
||||
|
||||
// Add "Click here" text
|
||||
const startText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||||
startText.setAttribute('x', '50');
|
||||
startText.setAttribute('y', height/2 + 60);
|
||||
startText.setAttribute('text-anchor', 'middle');
|
||||
startText.setAttribute('fill', '#ef4444');
|
||||
startText.setAttribute('font-size', '16');
|
||||
startText.setAttribute('font-weight', 'bold');
|
||||
startText.textContent = 'Clique aqui';
|
||||
svg.appendChild(startText);
|
||||
|
||||
// Add click to start
|
||||
startCircle.addEventListener('click', () => {
|
||||
if (!started) {
|
||||
started = true;
|
||||
startCircle.setAttribute('fill', '#22c55e');
|
||||
startCircle.classList.remove('pulse-circle');
|
||||
pulseRing.remove(); // Remove o anel pulsante
|
||||
startText.remove(); // Remove o texto
|
||||
notify('running', { progress: 0 });
|
||||
}
|
||||
});
|
||||
|
||||
// End point
|
||||
const endCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
||||
endCircle.setAttribute('cx', width - 50);
|
||||
endCircle.setAttribute('cy', height/2);
|
||||
endCircle.setAttribute('r', '30');
|
||||
endCircle.setAttribute('fill', '#22c55e');
|
||||
svg.appendChild(endCircle);
|
||||
|
||||
// Track mouse movement
|
||||
svg.addEventListener('mousemove', (e) => {
|
||||
if (!started) return; // Só processa se a atividade foi iniciada
|
||||
|
||||
const rect = svg.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
|
||||
// Check if on path (simplified: check distance from path)
|
||||
const pathLength = bgPath.getTotalLength();
|
||||
let minDist = Infinity;
|
||||
|
||||
for (let i = 0; i < pathLength; i += 10) {
|
||||
const point = bgPath.getPointAtLength(i);
|
||||
const dist = Math.sqrt((x - point.x) ** 2 + (y - point.y) ** 2);
|
||||
if (dist < minDist) minDist = dist;
|
||||
}
|
||||
|
||||
if (minDist < 60) {
|
||||
// On path
|
||||
const progressPercent = (x / width) * 100;
|
||||
if (progressPercent > progress) {
|
||||
progress = progressPercent;
|
||||
const offset = 1000 - (progress / 100) * 1000;
|
||||
progressPath.setAttribute('stroke-dashoffset', offset);
|
||||
notify('running', { progress: Math.floor(progress) });
|
||||
|
||||
if (progress >= TARGET_PROGRESS) {
|
||||
finishActivity();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function finishActivity() {
|
||||
arena.classList.add('hidden');
|
||||
banner.classList.remove('hidden');
|
||||
lucide.createIcons();
|
||||
notify('success', { score: 100 });
|
||||
notify('completed', { score: 100 });
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Mouse - Controle</title>
|
||||
<link rel="stylesheet" href="../../shared/letramento.css">
|
||||
<script src="../../shared/lucide.js"></script>
|
||||
<style>
|
||||
body { overflow: hidden; height: 100vh; width: 100vw; }
|
||||
#path { cursor: pointer; }
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
r: 30;
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
r: 35;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
.pulse-circle {
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
@keyframes pulse-ring {
|
||||
0% {
|
||||
r: 30;
|
||||
opacity: 0.8;
|
||||
}
|
||||
100% {
|
||||
r: 50;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gradient-to-br from-brand-50 to-gray-100 flex items-center justify-center h-screen w-screen overflow-hidden p-4">
|
||||
<div class="w-full h-full flex flex-col">
|
||||
<div class="bg-white/95 backdrop-blur-sm border border-white/30 rounded-xl shadow-xl flex-1 flex flex-col overflow-hidden">
|
||||
<div class="flex items-center justify-center gap-4 p-4 border-b border-white/20">
|
||||
<i data-lucide="navigation" class="w-12 h-12 text-red-600"></i>
|
||||
<h2 class="text-2xl font-bold text-gray-800">Clique no círculo vermelho e siga o caminho</h2>
|
||||
</div>
|
||||
<div class="flex-1 p-6 overflow-hidden">
|
||||
<div id="arena" class="relative h-full bg-white/50 rounded-xl border-2 border-gray-200 shadow-inner overflow-hidden flex items-center justify-center">
|
||||
<svg id="pathSvg" class="w-full h-full"></svg>
|
||||
</div>
|
||||
<div id="successBanner" class="hidden mt-6 bg-gradient-to-r from-green-50 to-emerald-50 border-2 border-green-300 rounded-xl p-8 flex items-center justify-center gap-4">
|
||||
<i data-lucide="check-circle" class="text-green-600 w-16 h-16"></i>
|
||||
<div>
|
||||
<h2 class="text-4xl font-bold text-green-700 mb-2">Muito bem!</h2>
|
||||
<p class="text-xl text-gray-700">Você tem ótimo controle do mouse!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="./activity.js"></script>
|
||||
<script>lucide.createIcons();</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,97 @@
|
||||
const channelToken = new URLSearchParams(window.location.hash.slice(1)).get('channelToken');
|
||||
|
||||
function notify(type, payload = {}) {
|
||||
window.parent.postMessage({ type, token: channelToken, ...payload }, '*');
|
||||
}
|
||||
|
||||
const canvas = document.getElementById('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const banner = document.getElementById('successBanner');
|
||||
|
||||
canvas.width = canvas.offsetWidth;
|
||||
canvas.height = canvas.offsetHeight;
|
||||
|
||||
let drawing = false;
|
||||
let coverage = 0;
|
||||
const TARGET_COVERAGE = 60;
|
||||
const circleSegments = new Set(); // Track which parts of the circle were drawn
|
||||
|
||||
notify('started');
|
||||
|
||||
// Draw guide path (dotted circle)
|
||||
const centerX = canvas.width / 2;
|
||||
const centerY = canvas.height / 2;
|
||||
const radius = Math.min(canvas.width, canvas.height) * 0.3;
|
||||
|
||||
ctx.setLineDash([10, 10]);
|
||||
ctx.strokeStyle = '#ef4444';
|
||||
ctx.lineWidth = 6;
|
||||
ctx.beginPath();
|
||||
ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI);
|
||||
ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
|
||||
// User drawing
|
||||
let points = [];
|
||||
|
||||
canvas.addEventListener('mousedown', (e) => {
|
||||
drawing = true;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
points = [{ x: e.clientX - rect.left, y: e.clientY - rect.top }];
|
||||
});
|
||||
|
||||
canvas.addEventListener('mousemove', (e) => {
|
||||
if (!drawing) return;
|
||||
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
|
||||
points.push({ x, y });
|
||||
|
||||
// Draw line
|
||||
ctx.strokeStyle = '#22c55e';
|
||||
ctx.lineWidth = 8;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.lineJoin = 'round';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(points[points.length - 2].x, points[points.length - 2].y);
|
||||
ctx.lineTo(x, y);
|
||||
ctx.stroke();
|
||||
|
||||
// Check if close to guide circle and mark segment as covered
|
||||
const dist = Math.sqrt((x - centerX) ** 2 + (y - centerY) ** 2);
|
||||
if (Math.abs(dist - radius) < 30) {
|
||||
// Calculate angle (0-360) to determine which segment of the circle
|
||||
const angle = Math.atan2(y - centerY, x - centerX);
|
||||
const degrees = ((angle * 180 / Math.PI) + 360) % 360;
|
||||
const segment = Math.floor(degrees / 3); // Divide circle in 120 segments (360/3)
|
||||
|
||||
if (!circleSegments.has(segment)) {
|
||||
circleSegments.add(segment);
|
||||
coverage = (circleSegments.size / 120) * 100;
|
||||
notify('running', { coverage: Math.floor(coverage) });
|
||||
|
||||
if (coverage >= TARGET_COVERAGE) {
|
||||
finishActivity();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
canvas.addEventListener('mouseup', () => {
|
||||
drawing = false;
|
||||
});
|
||||
|
||||
canvas.addEventListener('mouseleave', () => {
|
||||
drawing = false;
|
||||
});
|
||||
|
||||
function finishActivity() {
|
||||
drawing = false;
|
||||
canvas.classList.add('hidden');
|
||||
banner.classList.remove('hidden');
|
||||
lucide.createIcons();
|
||||
notify('success', { score: 100 });
|
||||
notify('completed', { score: 100 });
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Mouse - Desenhar</title>
|
||||
<link rel="stylesheet" href="../../shared/letramento.css">
|
||||
<script src="../../shared/lucide.js"></script>
|
||||
<style>
|
||||
body { overflow: hidden; height: 100vh; width: 100vw; }
|
||||
#canvas { cursor: crosshair; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gradient-to-br from-brand-50 to-gray-100 flex items-center justify-center h-screen w-screen overflow-hidden p-4">
|
||||
<div class="w-full h-full flex flex-col">
|
||||
<div class="bg-white/95 backdrop-blur-sm border border-white/30 rounded-xl shadow-xl flex-1 flex flex-col overflow-hidden">
|
||||
<div class="flex items-center justify-center gap-4 p-4 border-b border-white/20">
|
||||
<i data-lucide="pencil" class="w-12 h-12 text-red-600"></i>
|
||||
<h2 class="text-2xl font-bold text-gray-800">Desenhe seguindo a linha pontilhada</h2>
|
||||
</div>
|
||||
<div class="flex-1 p-6 overflow-hidden">
|
||||
<canvas id="canvas" class="w-full h-full bg-white/50 rounded-xl border-2 border-gray-200 shadow-inner"></canvas>
|
||||
<div id="successBanner" class="hidden mt-6 bg-gradient-to-r from-green-50 to-emerald-50 border-2 border-green-300 rounded-xl p-8 flex items-center justify-center gap-4">
|
||||
<i data-lucide="check-circle" class="text-green-600 w-16 h-16"></i>
|
||||
<div>
|
||||
<h2 class="text-4xl font-bold text-green-700 mb-2">Muito bem!</h2>
|
||||
<p class="text-xl text-gray-700">Você completou o traçado!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="./activity.js"></script>
|
||||
<script>lucide.createIcons();</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,53 @@
|
||||
const channelToken = new URLSearchParams(window.location.hash.slice(1)).get('channelToken');
|
||||
|
||||
function notify(type, payload = {}) {
|
||||
window.parent.postMessage({ type, token: channelToken, ...payload }, '*');
|
||||
}
|
||||
|
||||
const arena = document.getElementById('arena');
|
||||
const banner = document.getElementById('successBanner');
|
||||
|
||||
let completed = 0;
|
||||
const TOTAL_TARGETS = 5;
|
||||
|
||||
notify('started');
|
||||
|
||||
function placeTarget() {
|
||||
const SIZE = 80;
|
||||
const maxX = arena.clientWidth - SIZE - 20;
|
||||
const maxY = arena.clientHeight - SIZE - 20;
|
||||
const x = Math.floor(Math.random() * maxX) + 10;
|
||||
const y = Math.floor(Math.random() * maxY) + 10;
|
||||
|
||||
const target = document.createElement('button');
|
||||
target.className = 'absolute w-16 h-16 bg-gradient-to-br from-red-500 to-red-600 rounded-full shadow-lg hover:scale-110 transition-transform';
|
||||
target.style.width = `${SIZE}px`;
|
||||
target.style.height = `${SIZE}px`;
|
||||
target.style.left = `${x}px`;
|
||||
target.style.top = `${y}px`;
|
||||
|
||||
const bullseye = document.createElement('div');
|
||||
bullseye.className = 'absolute inset-0 flex items-center justify-center';
|
||||
bullseye.innerHTML = '<div class="w-6 h-6 bg-white rounded-full"></div>';
|
||||
target.appendChild(bullseye);
|
||||
|
||||
arena.appendChild(target);
|
||||
|
||||
target.addEventListener('click', () => {
|
||||
target.remove();
|
||||
completed++;
|
||||
notify('running', { step: completed });
|
||||
|
||||
if (completed < TOTAL_TARGETS) {
|
||||
setTimeout(() => placeTarget(), 300);
|
||||
} else {
|
||||
arena.classList.add('hidden');
|
||||
banner.classList.remove('hidden');
|
||||
lucide.createIcons();
|
||||
notify('success', { score: 100 });
|
||||
notify('completed', { score: 100 });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
placeTarget();
|
||||
@@ -0,0 +1,37 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Mouse - Precisão</title>
|
||||
<link rel="stylesheet" href="../../shared/letramento.css">
|
||||
<script src="../../shared/lucide.js"></script>
|
||||
<style>
|
||||
body { overflow: hidden; height: 100vh; width: 100vw; }
|
||||
@keyframes shrink { 0% { transform: scale(1); } 100% { transform: scale(0.5); } }
|
||||
.shrinking { animation: shrink 5s linear infinite; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gradient-to-br from-brand-50 to-gray-100 flex items-center justify-center h-screen w-screen overflow-hidden p-4">
|
||||
<div class="w-full h-full flex flex-col">
|
||||
<div class="bg-white/95 backdrop-blur-sm border border-white/30 rounded-xl shadow-xl flex-1 flex flex-col overflow-hidden">
|
||||
<div class="flex items-center justify-center gap-4 p-4 border-b border-white/20">
|
||||
<i data-lucide="target" class="w-12 h-12 text-red-600"></i>
|
||||
<h2 class="text-2xl font-bold text-gray-800">Clique no centro dos alvos</h2>
|
||||
</div>
|
||||
<div class="flex-1 p-6 overflow-hidden">
|
||||
<div id="arena" class="relative h-full bg-white/50 rounded-xl border-2 border-gray-200 shadow-inner overflow-hidden"></div>
|
||||
<div id="successBanner" class="hidden mt-6 bg-gradient-to-r from-green-50 to-emerald-50 border-2 border-green-300 rounded-xl p-8 flex items-center justify-center gap-4">
|
||||
<i data-lucide="check-circle" class="text-green-600 w-16 h-16"></i>
|
||||
<div>
|
||||
<h2 class="text-4xl font-bold text-green-700 mb-2">Muito bem!</h2>
|
||||
<p class="text-xl text-gray-700">Sua precisão está excelente!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="./activity.js"></script>
|
||||
<script>lucide.createIcons();</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,70 @@
|
||||
const channelToken = new URLSearchParams(window.location.hash.slice(1)).get('channelToken');
|
||||
|
||||
function notify(type, payload = {}) {
|
||||
window.parent.postMessage({ type, token: channelToken, ...payload }, '*');
|
||||
}
|
||||
|
||||
const arena = document.getElementById('arena');
|
||||
const banner = document.getElementById('successBanner');
|
||||
|
||||
let currentTarget = 1;
|
||||
const TOTAL_TARGETS = 5;
|
||||
const targets = [];
|
||||
|
||||
notify('started');
|
||||
|
||||
function createTargets() {
|
||||
for (let i = 1; i <= TOTAL_TARGETS; i++) {
|
||||
const SIZE = 120;
|
||||
const maxX = arena.clientWidth - SIZE - 20;
|
||||
const maxY = arena.clientHeight - SIZE - 20;
|
||||
const x = Math.floor(Math.random() * maxX) + 10;
|
||||
const y = Math.floor(Math.random() * maxY) + 10;
|
||||
|
||||
const button = document.createElement('button');
|
||||
button.className = 'absolute bg-gradient-to-br from-gray-400 to-gray-500 rounded-full shadow-lg transition-all text-white font-bold text-4xl';
|
||||
button.style.width = `${SIZE}px`;
|
||||
button.style.height = `${SIZE}px`;
|
||||
button.style.left = `${x}px`;
|
||||
button.style.top = `${y}px`;
|
||||
button.textContent = i;
|
||||
button.dataset.number = i;
|
||||
|
||||
arena.appendChild(button);
|
||||
targets.push(button);
|
||||
|
||||
button.addEventListener('click', () => handleClick(parseInt(button.dataset.number)));
|
||||
}
|
||||
|
||||
updateActiveTarget();
|
||||
}
|
||||
|
||||
function updateActiveTarget() {
|
||||
targets.forEach((btn) => {
|
||||
const num = parseInt(btn.dataset.number);
|
||||
if (num === currentTarget) {
|
||||
btn.className = 'absolute bg-gradient-to-br from-red-500 to-red-600 rounded-full shadow-2xl text-white font-bold text-4xl pulse-active';
|
||||
} else if (num < currentTarget) {
|
||||
btn.className = 'absolute bg-gradient-to-br from-green-400 to-green-500 rounded-full shadow-lg text-white font-bold text-4xl opacity-50';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleClick(num) {
|
||||
if (num === currentTarget) {
|
||||
notify('running', { step: currentTarget });
|
||||
currentTarget++;
|
||||
|
||||
if (currentTarget > TOTAL_TARGETS) {
|
||||
arena.classList.add('hidden');
|
||||
banner.classList.remove('hidden');
|
||||
lucide.createIcons();
|
||||
notify('success', { score: 100 });
|
||||
notify('completed', { score: 100 });
|
||||
} else {
|
||||
updateActiveTarget();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
createTargets();
|
||||
@@ -0,0 +1,37 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Mouse - Sequência</title>
|
||||
<link rel="stylesheet" href="../../shared/letramento.css">
|
||||
<script src="../../shared/lucide.js"></script>
|
||||
<style>
|
||||
body { overflow: hidden; height: 100vh; width: 100vw; }
|
||||
@keyframes pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.1); } }
|
||||
.pulse-active { animation: pulse 1s ease-in-out infinite; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gradient-to-br from-brand-50 to-gray-100 flex items-center justify-center h-screen w-screen overflow-hidden p-4">
|
||||
<div class="w-full h-full flex flex-col">
|
||||
<div class="bg-white/95 backdrop-blur-sm border border-white/30 rounded-xl shadow-xl flex-1 flex flex-col overflow-hidden">
|
||||
<div class="flex items-center justify-center gap-4 p-4 border-b border-white/20">
|
||||
<i data-lucide="list-ordered" class="w-12 h-12 text-red-600"></i>
|
||||
<h2 class="text-2xl font-bold text-gray-800">Clique na ordem: 1 → 2 → 3 → 4 → 5</h2>
|
||||
</div>
|
||||
<div class="flex-1 p-6 overflow-hidden">
|
||||
<div id="arena" class="relative h-full bg-white/50 rounded-xl border-2 border-gray-200 shadow-inner overflow-hidden"></div>
|
||||
<div id="successBanner" class="hidden mt-6 bg-gradient-to-r from-green-50 to-emerald-50 border-2 border-green-300 rounded-xl p-8 flex items-center justify-center gap-4">
|
||||
<i data-lucide="check-circle" class="text-green-600 w-16 h-16"></i>
|
||||
<div>
|
||||
<h2 class="text-4xl font-bold text-green-700 mb-2">Muito bem!</h2>
|
||||
<p class="text-xl text-gray-700">Você acertou a sequência!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="./activity.js"></script>
|
||||
<script>lucide.createIcons();</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,80 @@
|
||||
const channelToken = new URLSearchParams(window.location.hash.slice(1)).get('channelToken');
|
||||
|
||||
function notify(type, payload = {}) {
|
||||
window.parent.postMessage({ type, token: channelToken, ...payload }, '*');
|
||||
}
|
||||
|
||||
const arena = document.getElementById('arena');
|
||||
const banner = document.getElementById('successBanner');
|
||||
const finalScoreEl = document.getElementById('finalScore');
|
||||
|
||||
let score = 0;
|
||||
let timeLeft = 30;
|
||||
let gameActive = true;
|
||||
|
||||
// Criar timer na cena
|
||||
const timerEl = document.createElement('div');
|
||||
timerEl.className = 'absolute top-4 left-1/2 -translate-x-1/2 text-4xl font-bold text-red-600 bg-white px-6 py-2 rounded-full shadow-xl';
|
||||
timerEl.textContent = `${timeLeft}s`;
|
||||
arena.appendChild(timerEl);
|
||||
|
||||
// Criar contador de score na cena
|
||||
const scoreEl = document.createElement('div');
|
||||
scoreEl.className = 'absolute top-4 right-4 text-2xl font-bold text-green-600 bg-white px-4 py-2 rounded-full shadow-xl';
|
||||
scoreEl.textContent = `0 alvos`;
|
||||
arena.appendChild(scoreEl);
|
||||
|
||||
notify('started');
|
||||
|
||||
const spawnInterval = setInterval(() => {
|
||||
if (gameActive) spawnTarget();
|
||||
}, 800);
|
||||
|
||||
const countdown = setInterval(() => {
|
||||
timeLeft--;
|
||||
timerEl.textContent = `${timeLeft}s`;
|
||||
|
||||
if (timeLeft <= 0) {
|
||||
endGame();
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
function spawnTarget() {
|
||||
const SIZE = 100;
|
||||
const maxX = arena.clientWidth - SIZE - 20;
|
||||
const maxY = arena.clientHeight - SIZE - 20;
|
||||
const x = Math.floor(Math.random() * maxX) + 10;
|
||||
const y = Math.floor(Math.random() * maxY) + 10;
|
||||
|
||||
const target = document.createElement('button');
|
||||
target.className = 'absolute bg-gradient-to-br from-red-500 to-red-600 rounded-full shadow-2xl fading';
|
||||
target.style.width = `${SIZE}px`;
|
||||
target.style.height = `${SIZE}px`;
|
||||
target.style.left = `${x}px`;
|
||||
target.style.top = `${y}px`;
|
||||
|
||||
arena.appendChild(target);
|
||||
|
||||
target.addEventListener('click', () => {
|
||||
target.remove();
|
||||
score++;
|
||||
scoreEl.textContent = `${score} alvos`;
|
||||
notify('running', { score });
|
||||
});
|
||||
|
||||
setTimeout(() => target.remove(), 1500);
|
||||
}
|
||||
|
||||
function endGame() {
|
||||
gameActive = false;
|
||||
clearInterval(spawnInterval);
|
||||
clearInterval(countdown);
|
||||
|
||||
arena.classList.add('hidden');
|
||||
finalScoreEl.textContent = `Você clicou ${score} alvos em 30 segundos!`;
|
||||
banner.classList.remove('hidden');
|
||||
lucide.createIcons();
|
||||
|
||||
notify('success', { score });
|
||||
notify('completed', { score });
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Mouse - Velocidade</title>
|
||||
<link rel="stylesheet" href="../../shared/letramento.css">
|
||||
<script src="../../shared/lucide.js"></script>
|
||||
<style>
|
||||
body { overflow: hidden; height: 100vh; width: 100vw; }
|
||||
@keyframes fadeOut { to { opacity: 0; transform: scale(0); } }
|
||||
.fading { animation: fadeOut 1.5s linear forwards; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gradient-to-br from-brand-50 to-gray-100 flex items-center justify-center h-screen w-screen overflow-hidden p-4">
|
||||
<div class="w-full h-full flex flex-col">
|
||||
<div class="bg-white/95 backdrop-blur-sm border border-white/30 rounded-xl shadow-xl flex-1 flex flex-col overflow-hidden">
|
||||
<div class="flex items-center justify-center gap-4 p-4 border-b border-white/20">
|
||||
<i data-lucide="zap" class="w-12 h-12 text-red-600"></i>
|
||||
<h2 class="text-2xl font-bold text-gray-800">Clique rápido antes que desapareçam!</h2>
|
||||
</div>
|
||||
<div class="flex-1 p-6 overflow-hidden">
|
||||
<div id="arena" class="relative h-full bg-white/50 rounded-xl border-2 border-gray-200 shadow-inner overflow-hidden"></div>
|
||||
<div id="successBanner" class="hidden mt-6 bg-gradient-to-r from-green-50 to-emerald-50 border-2 border-green-300 rounded-xl p-8 flex items-center justify-center gap-4">
|
||||
<i data-lucide="check-circle" class="text-green-600 w-16 h-16"></i>
|
||||
<div>
|
||||
<h2 class="text-4xl font-bold text-green-700 mb-2">Muito bem!</h2>
|
||||
<p class="text-xl text-gray-700" id="finalScore"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="./activity.js"></script>
|
||||
<script>lucide.createIcons();</script>
|
||||
</body>
|
||||
</html>
|
||||
152
app/src/atividades/letramento/mouse/mouseRegistry.js
Normal file
@@ -0,0 +1,152 @@
|
||||
export const MOUSE_ATIVIDADES_REGISTRY = {
|
||||
'mouse-basico': {
|
||||
id: 'mouse-basico',
|
||||
titulo: 'Mouse Básico',
|
||||
descricao: 'Aprenda a mover, clicar e fazer duplo clique.',
|
||||
categoria: 'mouse',
|
||||
dificuldade: 'iniciante',
|
||||
duracao: 3,
|
||||
htmlFile: '/atividades/letramento/mouse/mouse-basico/index.html',
|
||||
proxima: 'mouse-precisao',
|
||||
passos: [
|
||||
{ id: 1, label: 'Mover' },
|
||||
{ id: 2, label: 'Clicar' },
|
||||
{ id: 3, label: 'Duplo clique' },
|
||||
],
|
||||
},
|
||||
'mouse-precisao': {
|
||||
id: 'mouse-precisao',
|
||||
titulo: 'Precisão com Mouse',
|
||||
descricao: 'Clique no centro de alvos que diminuem de tamanho.',
|
||||
categoria: 'mouse',
|
||||
dificuldade: 'iniciante',
|
||||
duracao: 3,
|
||||
htmlFile: '/atividades/letramento/mouse/mouse-precisao/index.html',
|
||||
proxima: 'mouse-clique-multiplo',
|
||||
passos: [
|
||||
{ id: 1, label: 'Alvo 1' },
|
||||
{ id: 2, label: 'Alvo 2' },
|
||||
{ id: 3, label: 'Alvo 3' },
|
||||
{ id: 4, label: 'Alvo 4' },
|
||||
{ id: 5, label: 'Alvo 5' },
|
||||
],
|
||||
},
|
||||
// 'mouse-controle': {
|
||||
// id: 'mouse-controle',
|
||||
// titulo: 'Controle do Mouse',
|
||||
// descricao: 'Siga o caminho sem sair da linha.',
|
||||
// categoria: 'mouse',
|
||||
// dificuldade: 'iniciante',
|
||||
// duracao: 3,
|
||||
// htmlFile: '/atividades/letramento/mouse/mouse-controle/index.html',
|
||||
// proxima: 'mouse-clique-multiplo',
|
||||
// passos: [
|
||||
// { id: 1, label: 'Seguir caminho' },
|
||||
// ],
|
||||
// },
|
||||
'mouse-clique-multiplo': {
|
||||
id: 'mouse-clique-multiplo',
|
||||
titulo: 'Múltiplos Cliques',
|
||||
descricao: 'Clique em vários alvos que aparecem ao mesmo tempo.',
|
||||
categoria: 'mouse',
|
||||
dificuldade: 'iniciante',
|
||||
duracao: 2,
|
||||
htmlFile: '/atividades/letramento/mouse/mouse-clique-multiplo/index.html',
|
||||
proxima: 'mouse-sequencia',
|
||||
passos: [
|
||||
{ id: 1, label: 'Clicar todos' },
|
||||
],
|
||||
},
|
||||
'mouse-sequencia': {
|
||||
id: 'mouse-sequencia',
|
||||
titulo: 'Sequência Numérica',
|
||||
descricao: 'Clique nos números na ordem correta: 1, 2, 3...',
|
||||
categoria: 'mouse',
|
||||
dificuldade: 'intermediario',
|
||||
duracao: 3,
|
||||
htmlFile: '/atividades/letramento/mouse/mouse-sequencia/index.html',
|
||||
proxima: 'mouse-velocidade',
|
||||
passos: [
|
||||
{ id: 1, label: '1' },
|
||||
{ id: 2, label: '2' },
|
||||
{ id: 3, label: '3' },
|
||||
{ id: 4, label: '4' },
|
||||
{ id: 5, label: '5' },
|
||||
],
|
||||
},
|
||||
'mouse-velocidade': {
|
||||
id: 'mouse-velocidade',
|
||||
titulo: 'Velocidade e Reflexo',
|
||||
descricao: 'Clique o máximo de alvos em 30 segundos.',
|
||||
categoria: 'mouse',
|
||||
dificuldade: 'intermediario',
|
||||
duracao: 2,
|
||||
htmlFile: '/atividades/letramento/mouse/mouse-velocidade/index.html',
|
||||
proxima: 'mouse-botao-direito',
|
||||
passos: [
|
||||
{ id: 1, label: 'Desafio' },
|
||||
],
|
||||
},
|
||||
'mouse-botao-direito': {
|
||||
id: 'mouse-botao-direito',
|
||||
titulo: 'Botão Direito',
|
||||
descricao: 'Aprenda a usar o botão direito e menu de contexto.',
|
||||
categoria: 'mouse',
|
||||
dificuldade: 'intermediario',
|
||||
duracao: 2,
|
||||
htmlFile: '/atividades/letramento/mouse/mouse-botao-direito/index.html',
|
||||
proxima: 'mouse-arrastar',
|
||||
passos: [
|
||||
{ id: 1, label: 'Contexto' },
|
||||
{ id: 2, label: 'Selecionar' },
|
||||
],
|
||||
},
|
||||
'mouse-arrastar': {
|
||||
id: 'mouse-arrastar',
|
||||
titulo: 'Arrastar e Soltar',
|
||||
descricao: 'Aprenda a arrastar arquivos para uma pasta.',
|
||||
categoria: 'mouse',
|
||||
dificuldade: 'intermediario',
|
||||
duracao: 3,
|
||||
htmlFile: '/atividades/letramento/mouse/mouse-arrastar/index.html',
|
||||
proxima: 'mouse-desenhar',
|
||||
passos: [
|
||||
{ id: 1, label: 'Arquivo 1' },
|
||||
{ id: 2, label: 'Arquivo 2' },
|
||||
{ id: 3, label: 'Arquivo 3' },
|
||||
],
|
||||
},
|
||||
'mouse-desenhar': {
|
||||
id: 'mouse-desenhar',
|
||||
titulo: 'Desenhar Traçados',
|
||||
descricao: 'Siga a linha pontilhada desenhando com o mouse.',
|
||||
categoria: 'mouse',
|
||||
dificuldade: 'avancado',
|
||||
duracao: 4,
|
||||
htmlFile: '/atividades/letramento/mouse/mouse-desenhar/index.html',
|
||||
proxima: 'mouse-completo',
|
||||
passos: [
|
||||
{ id: 1, label: 'Traçar círculo' },
|
||||
],
|
||||
},
|
||||
'mouse-completo': {
|
||||
id: 'mouse-completo',
|
||||
titulo: 'Desafio Completo',
|
||||
descricao: 'Combinação de todas as habilidades do mouse.',
|
||||
categoria: 'mouse',
|
||||
dificuldade: 'avancado',
|
||||
duracao: 5,
|
||||
htmlFile: '/atividades/letramento/mouse/mouse-completo/index.html',
|
||||
proxima: null,
|
||||
passos: [
|
||||
{ id: 1, label: 'Precisão' },
|
||||
{ id: 2, label: 'Sequência' },
|
||||
{ id: 3, label: 'Trilha' },
|
||||
{ id: 4, label: 'Múltiplos' },
|
||||
{ id: 5, label: 'Contexto' },
|
||||
{ id: 6, label: 'Organizar' },
|
||||
{ id: 7, label: 'Desenhar' },
|
||||
{ id: 8, label: 'Final' },
|
||||
],
|
||||
},
|
||||
};
|
||||
8109
app/src/atividades/letramento/shared/letramento.css
Normal file
18134
app/src/atividades/letramento/shared/lucide.js
Normal file
3
app/src/atividades/letramento/shared/tailwind-input.css
Normal file
@@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@@ -0,0 +1,86 @@
|
||||
const channelToken = new URLSearchParams(window.location.hash.slice(1)).get('channelToken');
|
||||
|
||||
function notify(type, payload = {}) {
|
||||
window.parent.postMessage({ type, token: channelToken, ...payload }, '*');
|
||||
}
|
||||
|
||||
const challenges = [
|
||||
{ type: 'key', lucideIcon: 'keyboard', title: 'Etapa 1', target: 'G', hint: 'Encontre e pressione G', check: e => e.key.toUpperCase() === 'G' },
|
||||
{ type: 'number', lucideIcon: 'hash', title: 'Etapa 2', target: '5', hint: 'Encontre e pressione 5', check: e => e.key === '5' },
|
||||
{ type: 'upper', lucideIcon: 'arrow-big-up', title: 'Etapa 3', target: 'T', hint: 'Use Shift + T', check: e => e.key === 'T' && e.shiftKey },
|
||||
{ type: 'symbol', lucideIcon: 'at-sign', title: 'Etapa 4', target: '@', hint: 'Use Shift + 2', check: e => e.key === '@' },
|
||||
{ type: 'arrow', lucideIcon: 'move-vertical', title: 'Etapa 5', target: '↓', hint: 'Pressione a seta para baixo', check: e => e.key === 'ArrowDown' },
|
||||
{ type: 'enter', lucideIcon: 'corner-down-left', title: 'Etapa 6', target: 'Enter', hint: 'Pressione Enter', check: e => e.key === 'Enter' },
|
||||
{ type: 'esc', lucideIcon: 'x-circle', title: 'Etapa 7', target: 'Esc', hint: 'Pressione Esc', check: e => e.key === 'Escape' },
|
||||
{ type: 'key', lucideIcon: 'trophy', title: 'Etapa 8', target: 'Z', hint: 'Pressione Z para concluir', check: e => e.key.toUpperCase() === 'Z' },
|
||||
];
|
||||
|
||||
let idx = 0;
|
||||
let locked = false;
|
||||
notify('started');
|
||||
|
||||
const challengeIcon = document.getElementById('challengeIcon');
|
||||
const challengeTitle = document.getElementById('challengeTitle');
|
||||
const challengeTarget = document.getElementById('challengeTarget');
|
||||
const challengeHint = document.getElementById('challengeHint');
|
||||
const feedbackEl = document.getElementById('feedback');
|
||||
const progressFill = document.getElementById('progressFill');
|
||||
const progressLabel = document.getElementById('progressLabel');
|
||||
const successBanner = document.getElementById('successBanner');
|
||||
const challengeCard = document.getElementById('challengeCard');
|
||||
|
||||
const bgMap = {
|
||||
key: 'bg-fuchsia-50 border-fuchsia-300 text-fuchsia-700',
|
||||
upper: 'bg-orange-50 border-orange-300 text-orange-700',
|
||||
symbol: 'bg-amber-50 border-amber-300 text-amber-700',
|
||||
arrow: 'bg-teal-50 border-teal-300 text-teal-700',
|
||||
enter: 'bg-blue-50 border-blue-300 text-blue-700',
|
||||
esc: 'bg-gray-50 border-gray-300 text-gray-700',
|
||||
number: 'bg-green-50 border-green-300 text-green-700',
|
||||
};
|
||||
|
||||
function showChallenge() {
|
||||
locked = false;
|
||||
const c = challenges[idx];
|
||||
// challengeIcon.innerHTML = `<i data-lucide="${c.lucideIcon}" class="w-16 h-16"></i>`;
|
||||
// lucide.createIcons();
|
||||
challengeTitle.textContent = c.title;
|
||||
const bg = bgMap[c.type] || bgMap.key;
|
||||
challengeTarget.className = `text-[6rem] font-black rounded-2xl px-8 py-4 border-4 text-center leading-none ${bg}`;
|
||||
challengeTarget.textContent = c.target;
|
||||
challengeHint.textContent = c.hint;
|
||||
feedbackEl.classList.add('hidden');
|
||||
progressFill.style.width = `${(idx / challenges.length) * 100}%`;
|
||||
progressLabel.textContent = `Etapa ${idx} de ${challenges.length}`;
|
||||
}
|
||||
showChallenge();
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (locked) return;
|
||||
const c = challenges[idx];
|
||||
if (c.check(e)) {
|
||||
e.preventDefault();
|
||||
locked = true;
|
||||
feedbackEl.textContent = 'Correto!';
|
||||
feedbackEl.className = 'text-2xl font-bold py-1 text-center text-green-600';
|
||||
feedbackEl.classList.remove('hidden');
|
||||
idx++;
|
||||
notify('running', { step: idx });
|
||||
progressFill.style.width = `${(idx / challenges.length) * 100}%`;
|
||||
progressLabel.textContent = `Etapa ${idx} de ${challenges.length}`;
|
||||
setTimeout(() => {
|
||||
locked = false;
|
||||
if (idx >= challenges.length) {
|
||||
challengeCard.classList.add('hidden');
|
||||
progressFill.parentElement.parentElement.classList.add('hidden');
|
||||
successBanner.classList.remove('hidden');
|
||||
lucide.createIcons();
|
||||
notify('success', { score: 100 });
|
||||
notify('completed', { score: 100 });
|
||||
} else {
|
||||
showChallenge();
|
||||
}
|
||||
}, 700);
|
||||
}
|
||||
if (e.key === 'Enter' || e.key === 'Escape') e.preventDefault();
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Atividade Final</title>
|
||||
<link rel="stylesheet" href="../../shared/letramento.css">
|
||||
<script src="../../shared/lucide.js"></script>
|
||||
<style>body { overflow: hidden; height: 100vh; width: 100vw; }
|
||||
.challenge-badge { transition: all 0.2s; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gradient-to-br from-brand-50 to-gray-100 flex items-center justify-center h-screen w-screen overflow-hidden p-4">
|
||||
<div class="w-full h-full flex flex-col">
|
||||
<div class="bg-white/95 backdrop-blur-sm border border-white/30 rounded-xl shadow-xl flex-1 flex flex-col overflow-hidden">
|
||||
<div class="flex items-center justify-center gap-4 p-4 border-b border-gray-100">
|
||||
<i data-lucide="trophy" class="w-10 h-10 text-red-500"></i>
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-gray-800">Atividade Final</h2>
|
||||
<p class="text-lg text-gray-600">Complete todas as etapas e integre habilidades do teclado.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 p-4 flex flex-col gap-4 justify-center items-center overflow-hidden">
|
||||
<div id="challengeCard" class="flex flex-col items-center gap-3 w-full max-w-md">
|
||||
<span class="text-6xl" id="challengeIcon"></span>
|
||||
<p class="text-2xl font-bold text-gray-700 text-center" id="challengeTitle"></p>
|
||||
<div id="challengeTarget" class="text-[6rem] font-black rounded-2xl px-8 py-4 border-4 text-center bg-fuchsia-50 border-fuchsia-300 text-fuchsia-700 leading-none"></div>
|
||||
<p class="text-base text-gray-500 text-center" id="challengeHint"></p>
|
||||
</div>
|
||||
<div id="feedback" class="hidden text-2xl font-bold py-1 text-center"></div>
|
||||
<div class="w-full max-w-xs">
|
||||
<div class="h-3 bg-gray-200 rounded-full">
|
||||
<div id="progressFill" class="h-full bg-fuchsia-500 rounded-full transition-all" style="width:0%"></div>
|
||||
</div>
|
||||
<p class="text-center text-gray-500 text-sm mt-1" id="progressLabel">Etapa 0 de 8</p>
|
||||
</div>
|
||||
<div id="successBanner" class="hidden bg-gradient-to-r from-green-50 to-emerald-50 border-2 border-green-300 rounded-xl p-6 flex items-center justify-center gap-4 w-full max-w-lg">
|
||||
<i data-lucide="trophy" class="text-yellow-500 w-14 h-14"></i>
|
||||
<div>
|
||||
<h2 class="text-3xl font-bold text-green-700 mb-1">Parabéns!</h2>
|
||||
<p class="text-lg text-gray-700">Você concluiu a Atividade Final do teclado!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="./activity.js"></script>
|
||||
<script>lucide.createIcons();</script>
|
||||
</body>
|
||||
</html>
|
||||
331
app/src/atividades/letramento/teclado/chuva/activity.js
Normal file
@@ -0,0 +1,331 @@
|
||||
const channelToken = new URLSearchParams(window.location.hash.slice(1)).get('channelToken');
|
||||
|
||||
function notify(type, payload = {}) {
|
||||
window.parent.postMessage({ type, token: channelToken, ...payload }, '*');
|
||||
}
|
||||
|
||||
const SINGLE_CHARACTER_POOL = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-=+_'.split('');
|
||||
const TOKEN_POOLS_BY_LENGTH = {
|
||||
2: [
|
||||
'ab', 'cd', 'ef', 'hj', 'mn', 'pq', '44', '17', '29', '80',
|
||||
'GG', 'HJ', 'KL', 'MN', 'PR', 'TV', 'WW', 'XX', 'YY', 'ZZ',
|
||||
],
|
||||
3: [
|
||||
'sol', 'mar', 'ceu', 'rio', 'lua', 'dia', 'som', 'mel', 'paz', 'voz',
|
||||
'bom', 'fim', 'cor', 'lar', 'giz', 'asa', 'luz', 'rei', 'nuv', 'fio',
|
||||
],
|
||||
4: [
|
||||
'casa', 'mesa', 'jogo', 'foco', 'dado', 'bola', 'pato', 'gato', 'rede', 'nave',
|
||||
'copo', 'lago', 'tela', 'nota', 'rima', 'sapo', 'coro', 'vela', 'pulo', 'pipa',
|
||||
],
|
||||
};
|
||||
const STAGES = [
|
||||
{ goal: 5, speedMultiplier: 1, mode: 'single' },
|
||||
{ goal: 5, speedMultiplier: 1.45, mode: 'single' },
|
||||
{ goal: 5, speedMultiplier: 1.75, mode: 'single' },
|
||||
{ goal: 5, speedMultiplier: 1, mode: 'token', tokenLength: 2 },
|
||||
{ goal: 5, speedMultiplier: 1, mode: 'token', tokenLength: 3 },
|
||||
{ goal: 5, speedMultiplier: 1, mode: 'token', tokenLength: 4 },
|
||||
];
|
||||
|
||||
let stageIndex = 0;
|
||||
let score = 0;
|
||||
let gameActive = true;
|
||||
let falling = [];
|
||||
let spawnTimer = null;
|
||||
let totalInputs = 0;
|
||||
let correctInputs = 0;
|
||||
let inputBuffer = '';
|
||||
let tokenCycle = [];
|
||||
|
||||
notify('started');
|
||||
|
||||
const arena = document.getElementById('arena');
|
||||
const scoreEl = document.getElementById('score');
|
||||
const goalEl = document.getElementById('goal');
|
||||
const stageLabelEl = document.getElementById('stageLabel');
|
||||
const accuracyLabel = document.getElementById('accuracyLabel');
|
||||
const progressFill = document.getElementById('progressFill');
|
||||
const feedbackEl = document.getElementById('feedback');
|
||||
const successBanner = document.getElementById('successBanner');
|
||||
const gameoverBanner = document.getElementById('gameoverBanner');
|
||||
const gameoverStageLabel = document.getElementById('gameoverStageLabel');
|
||||
const retryStageBtn = document.getElementById('retryStageBtn');
|
||||
|
||||
const COLORS = [
|
||||
['bg-cyan-500 text-white border-cyan-300', 'cyan'],
|
||||
['bg-violet-500 text-white border-violet-300', 'violet'],
|
||||
['bg-orange-500 text-white border-orange-300', 'orange'],
|
||||
['bg-pink-500 text-white border-pink-300', 'pink'],
|
||||
];
|
||||
|
||||
function getCurrentStage() {
|
||||
return STAGES[stageIndex];
|
||||
}
|
||||
|
||||
function getAccuracy() {
|
||||
if (totalInputs === 0) return 100;
|
||||
return (correctInputs / totalInputs) * 100;
|
||||
}
|
||||
|
||||
function updateStageUi() {
|
||||
const stage = getCurrentStage();
|
||||
scoreEl.textContent = score;
|
||||
goalEl.textContent = stage.goal;
|
||||
stageLabelEl.textContent = `${stageIndex + 1} de ${STAGES.length}`;
|
||||
accuracyLabel.textContent = `${Math.round(getAccuracy())}%`;
|
||||
progressFill.style.width = `${(score / stage.goal) * 100}%`;
|
||||
}
|
||||
|
||||
function showFeedback(message, kind) {
|
||||
feedbackEl.textContent = message;
|
||||
feedbackEl.className = `text-2xl font-bold py-1 text-center ${kind === 'error' ? 'text-red-500' : 'text-green-600'}`;
|
||||
feedbackEl.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function clearFeedback() {
|
||||
feedbackEl.classList.add('hidden');
|
||||
}
|
||||
|
||||
function restartCurrentStage() {
|
||||
gameActive = true;
|
||||
score = 0;
|
||||
totalInputs = 0;
|
||||
correctInputs = 0;
|
||||
inputBuffer = '';
|
||||
tokenCycle = [];
|
||||
falling.forEach((item) => item.el.remove());
|
||||
falling = [];
|
||||
lastTime = null;
|
||||
gameoverBanner.classList.add('hidden');
|
||||
clearFeedback();
|
||||
updateStageUi();
|
||||
startSpawnLoop();
|
||||
spawnCharacter();
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
|
||||
function finishFailure() {
|
||||
gameActive = false;
|
||||
if (spawnTimer) clearInterval(spawnTimer);
|
||||
arena.innerHTML = '';
|
||||
falling = [];
|
||||
gameoverStageLabel.textContent = `Etapa ${stageIndex + 1}: você não atingiu 60% de precisão.`;
|
||||
gameoverBanner.classList.remove('hidden');
|
||||
lucide.createIcons();
|
||||
// Não notifica 'failure' ao host para evitar que o modal externo cause reload do iframe.
|
||||
// O retry é gerenciado internamente pelo botão desta tela.
|
||||
}
|
||||
|
||||
function finishSuccess() {
|
||||
gameActive = false;
|
||||
if (spawnTimer) clearInterval(spawnTimer);
|
||||
arena.innerHTML = '';
|
||||
successBanner.classList.remove('hidden');
|
||||
lucide.createIcons();
|
||||
notify('success', { score: 100 });
|
||||
notify('completed', { score: 100 });
|
||||
}
|
||||
|
||||
function shuffle(list) {
|
||||
const copy = [...list];
|
||||
for (let index = copy.length - 1; index > 0; index -= 1) {
|
||||
const swapIndex = Math.floor(Math.random() * (index + 1));
|
||||
[copy[index], copy[swapIndex]] = [copy[swapIndex], copy[index]];
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
function refillTokenCycle(length) {
|
||||
const pool = TOKEN_POOLS_BY_LENGTH[length] ?? [];
|
||||
tokenCycle = shuffle(pool);
|
||||
}
|
||||
|
||||
function pickRandomToken(length) {
|
||||
if (tokenCycle.length === 0) {
|
||||
refillTokenCycle(length);
|
||||
}
|
||||
|
||||
return tokenCycle.pop();
|
||||
}
|
||||
|
||||
function makeEntityForStage(stage) {
|
||||
if (stage.mode === 'single') {
|
||||
return SINGLE_CHARACTER_POOL[Math.floor(Math.random() * SINGLE_CHARACTER_POOL.length)];
|
||||
}
|
||||
|
||||
return pickRandomToken(stage.tokenLength);
|
||||
}
|
||||
|
||||
function getEntityFontClass(entity) {
|
||||
if (entity.length >= 4) return 'text-xl';
|
||||
if (entity.length === 3) return 'text-2xl';
|
||||
return 'text-3xl';
|
||||
}
|
||||
|
||||
function evaluateStageAndAdvance() {
|
||||
const accuracy = getAccuracy();
|
||||
if (accuracy < 60) {
|
||||
showFeedback('Precisão abaixo de 60%. Tente novamente.', 'error');
|
||||
finishFailure();
|
||||
return;
|
||||
}
|
||||
|
||||
stageIndex += 1;
|
||||
score = 0;
|
||||
falling.forEach((item) => item.el.remove());
|
||||
falling = [];
|
||||
inputBuffer = '';
|
||||
tokenCycle = [];
|
||||
|
||||
if (stageIndex >= STAGES.length) {
|
||||
finishSuccess();
|
||||
return;
|
||||
}
|
||||
|
||||
updateStageUi();
|
||||
showFeedback(`Etapa ${stageIndex + 1} liberada!`, 'success');
|
||||
notify('running', { step: stageIndex + 1 });
|
||||
spawnCharacter();
|
||||
}
|
||||
|
||||
function spawnCharacter() {
|
||||
if (!gameActive) return;
|
||||
const stage = getCurrentStage();
|
||||
const character = makeEntityForStage(stage);
|
||||
const color = COLORS[Math.floor(Math.random() * COLORS.length)];
|
||||
const arenaW = arena.clientWidth - 140;
|
||||
const x = Math.floor(Math.random() * arenaW);
|
||||
const id = `fall-${Date.now()}-${Math.random()}`;
|
||||
const speed = (60 + Math.random() * 40) * stage.speedMultiplier;
|
||||
|
||||
const el = document.createElement('div');
|
||||
el.id = id;
|
||||
el.className = `falling-char border-2 ${getEntityFontClass(character)} ${color[0]}`;
|
||||
el.textContent = character;
|
||||
el.style.left = `${x}px`;
|
||||
el.style.top = '0px';
|
||||
arena.appendChild(el);
|
||||
|
||||
const item = { id, character, el, top: 0, speed };
|
||||
falling.push(item);
|
||||
return item;
|
||||
}
|
||||
|
||||
let lastTime = null;
|
||||
function gameLoop(timestamp) {
|
||||
if (!gameActive) return;
|
||||
if (!lastTime) lastTime = timestamp;
|
||||
const dt = (timestamp - lastTime) / 1000;
|
||||
lastTime = timestamp;
|
||||
|
||||
const arenaH = arena.clientHeight;
|
||||
const stage = getCurrentStage();
|
||||
falling = falling.filter(item => {
|
||||
item.top += item.speed * dt;
|
||||
item.el.style.top = `${item.top}px`;
|
||||
if (item.top + 64 >= arenaH) {
|
||||
item.el.remove();
|
||||
totalInputs += 1;
|
||||
updateStageUi();
|
||||
showFeedback(stage.mode === 'token' ? 'Uma combinação caiu. Tente a próxima.' : 'Um caractere caiu. Tente o próximo.', 'error');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (!gameActive) return;
|
||||
if (e.key.length !== 1) return;
|
||||
|
||||
const stage = getCurrentStage();
|
||||
const key = e.key;
|
||||
|
||||
if (stage.mode === 'single') {
|
||||
totalInputs += 1;
|
||||
const idx = falling.findIndex((item) => item.character === key);
|
||||
|
||||
if (idx !== -1) {
|
||||
const item = falling[idx];
|
||||
item.el.style.background = '#22c55e';
|
||||
item.el.style.color = 'white';
|
||||
item.el.style.transform = 'scale(0.92)';
|
||||
setTimeout(() => item.el.remove(), 200);
|
||||
falling.splice(idx, 1);
|
||||
score += 1;
|
||||
correctInputs += 1;
|
||||
updateStageUi();
|
||||
clearFeedback();
|
||||
notify('running', { step: stageIndex + 1 });
|
||||
if (score >= stage.goal) {
|
||||
evaluateStageAndAdvance();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
updateStageUi();
|
||||
showFeedback('Esse não era o caractere da tela.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
inputBuffer += key;
|
||||
const tokenLength = stage.tokenLength ?? 2;
|
||||
if (inputBuffer.length < tokenLength) {
|
||||
return;
|
||||
}
|
||||
|
||||
const attempt = inputBuffer.slice(-tokenLength);
|
||||
inputBuffer = '';
|
||||
totalInputs += 1;
|
||||
const idx = falling.findIndex((item) => item.character === attempt);
|
||||
|
||||
if (idx !== -1) {
|
||||
const item = falling[idx];
|
||||
item.el.style.background = '#22c55e';
|
||||
item.el.style.color = 'white';
|
||||
item.el.style.transform = 'scale(0.92)';
|
||||
setTimeout(() => item.el.remove(), 200);
|
||||
falling.splice(idx, 1);
|
||||
score += 1;
|
||||
correctInputs += 1;
|
||||
updateStageUi();
|
||||
clearFeedback();
|
||||
notify('running', { step: stageIndex + 1 });
|
||||
if (score >= stage.goal) {
|
||||
evaluateStageAndAdvance();
|
||||
}
|
||||
} else {
|
||||
updateStageUi();
|
||||
showFeedback(`Combinação incorreta: ${attempt}`, 'error');
|
||||
}
|
||||
});
|
||||
|
||||
function startSpawnLoop() {
|
||||
if (spawnTimer) clearInterval(spawnTimer);
|
||||
|
||||
spawnTimer = setInterval(() => {
|
||||
if (!gameActive) {
|
||||
clearInterval(spawnTimer);
|
||||
return;
|
||||
}
|
||||
if (falling.length < 5) spawnCharacter();
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
retryStageBtn.addEventListener('click', restartCurrentStage);
|
||||
|
||||
updateStageUi();
|
||||
startSpawnLoop();
|
||||
spawnCharacter();
|
||||
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (!gameActive) { clearInterval(spawnTimer); return; }
|
||||
if (!document.hidden && falling.length === 0) {
|
||||
spawnCharacter();
|
||||
}
|
||||
});
|
||||
|
||||
requestAnimationFrame(gameLoop);
|
||||
83
app/src/atividades/letramento/teclado/chuva/index.html
Normal file
@@ -0,0 +1,83 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Chuva de Letras</title>
|
||||
<link rel="stylesheet" href="../../shared/letramento.css">
|
||||
<script src="../../shared/lucide.js"></script>
|
||||
<style>
|
||||
body { overflow: hidden; height: 100vh; width: 100vw; }
|
||||
.falling-char {
|
||||
position: absolute;
|
||||
min-width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 16px;
|
||||
padding: 0 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.7rem;
|
||||
font-weight: 900;
|
||||
user-select: none;
|
||||
box-shadow: 0 10px 25px rgba(15, 23, 42, 0.18);
|
||||
transition: transform 0.12s ease, opacity 0.12s ease, background 0.12s ease;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gradient-to-br from-brand-50 to-gray-100 flex items-center justify-center h-screen w-screen overflow-hidden p-4">
|
||||
<div class="w-full h-full flex flex-col">
|
||||
<div class="bg-white/95 backdrop-blur-sm border border-white/30 rounded-xl shadow-xl flex-1 flex flex-col overflow-hidden">
|
||||
<div class="flex items-center justify-center gap-4 p-4 border-b border-gray-100">
|
||||
<i data-lucide="cloud-rain" class="w-10 h-10 text-red-500"></i>
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-gray-800">Chuva de Letras</h2>
|
||||
<p class="text-lg text-gray-600">Digite exatamente o alvo da tela. A atividade é sensível a maiúsculas e minúsculas.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 p-4 flex flex-col gap-4 overflow-hidden">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<div class="rounded-xl bg-sky-50 border border-sky-200 px-4 py-3">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.18em] text-sky-700">Etapa</p>
|
||||
<p id="stageLabel" class="mt-1 text-xl font-black text-slate-800">1 de 6</p>
|
||||
</div>
|
||||
<div class="rounded-xl bg-violet-50 border border-violet-200 px-4 py-3">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.18em] text-violet-700">Meta da Etapa</p>
|
||||
<p class="mt-1 text-xl font-black text-slate-800"><span id="score">0</span>/<span id="goal">10</span></p>
|
||||
</div>
|
||||
<div class="rounded-xl bg-emerald-50 border border-emerald-200 px-4 py-3">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.18em] text-emerald-700">Precisão</p>
|
||||
<p id="accuracyLabel" class="mt-1 text-xl font-black text-slate-800">100%</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full h-4 bg-gray-200 rounded-full overflow-hidden shadow-inner">
|
||||
<div id="progressFill" class="h-full bg-sky-500 transition-all duration-300 rounded-full" style="width: 0%"></div>
|
||||
</div>
|
||||
<div id="arena" class="flex-1 relative bg-white/60 rounded-xl overflow-hidden border-2 border-gray-200 shadow-inner"></div>
|
||||
<div id="feedback" class="hidden text-2xl font-bold py-1 text-center"></div>
|
||||
<div id="successBanner" class="hidden bg-gradient-to-r from-green-50 to-emerald-50 border-2 border-green-300 rounded-xl p-6 flex items-center justify-center gap-4">
|
||||
<i data-lucide="check-circle" class="text-green-600 w-14 h-14"></i>
|
||||
<div>
|
||||
<h2 class="text-3xl font-bold text-green-700 mb-1">Você venceu!</h2>
|
||||
<p class="text-lg text-gray-700">Você concluiu a bateria completa da chuva de caracteres.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="gameoverBanner" class="hidden bg-gradient-to-r from-red-50 to-rose-50 border-2 border-red-300 rounded-xl p-6 flex flex-col items-center justify-center gap-4">
|
||||
<div class="flex items-center gap-4">
|
||||
<i data-lucide="x-circle" class="text-red-600 w-14 h-14"></i>
|
||||
<div>
|
||||
<h2 class="text-3xl font-bold text-red-700 mb-1">Precisão insuficiente!</h2>
|
||||
<p id="gameoverStageLabel" class="text-lg text-gray-700">Você não atingiu 60% nesta etapa.</p>
|
||||
</div>
|
||||
</div>
|
||||
<button id="retryStageBtn" class="mt-2 px-6 py-3 rounded-xl bg-red-500 hover:bg-red-600 text-white font-bold text-lg shadow transition">
|
||||
Tentar esta etapa novamente
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="./activity.js"></script>
|
||||
<script>lucide.createIcons();</script>
|
||||
</body>
|
||||
</html>
|
||||
97
app/src/atividades/letramento/teclado/enter-esc/activity.js
Normal file
@@ -0,0 +1,97 @@
|
||||
const channelToken = new URLSearchParams(window.location.hash.slice(1)).get('channelToken');
|
||||
|
||||
function notify(type, payload = {}) {
|
||||
window.parent.postMessage({ type, token: channelToken, ...payload }, '*');
|
||||
}
|
||||
|
||||
const dialogs = [
|
||||
{
|
||||
title: 'Salvar arquivo',
|
||||
question: 'Deseja salvar o arquivo?',
|
||||
instruction: 'Pressione Enter para SIM (salvar) ou Esc para NÃO (cancelar)',
|
||||
expected: 'Enter',
|
||||
feedback: 'Arquivo salvo! Enter confirma!',
|
||||
},
|
||||
{
|
||||
title: 'Fechar janela',
|
||||
question: 'Tem certeza que quer fechar?',
|
||||
instruction: 'Pressione Esc para fechar (cancelar a ação)',
|
||||
expected: 'Escape',
|
||||
feedback: 'Janela fechada! Esc cancela!',
|
||||
},
|
||||
{
|
||||
title: 'Enviar mensagem',
|
||||
question: 'Enviar esta mensagem?',
|
||||
instruction: 'Pressione Enter para confirmar o envio',
|
||||
expected: 'Enter',
|
||||
feedback: 'Mensagem enviada! Enter confirma!',
|
||||
},
|
||||
{
|
||||
title: 'Descartar rascunho',
|
||||
question: 'Apagar o rascunho sem salvar?',
|
||||
instruction: 'Pressione Esc para cancelar (não apagar)',
|
||||
expected: 'Escape',
|
||||
feedback: 'Rascunho mantido! Esc cancela a ação!',
|
||||
},
|
||||
];
|
||||
|
||||
let currentDialog = 0;
|
||||
notify('started');
|
||||
|
||||
const dialogTitle = document.getElementById('dialogTitle');
|
||||
const dialogQuestion = document.getElementById('dialogQuestion');
|
||||
const dialogInstruction = document.getElementById('dialogInstruction');
|
||||
const feedbackEl = document.getElementById('feedback');
|
||||
const progressDots = document.getElementById('progressDots');
|
||||
const successBanner = document.getElementById('successBanner');
|
||||
const dialogBox = document.getElementById('dialogBox');
|
||||
let locked = false;
|
||||
|
||||
dialogs.forEach((_, i) => {
|
||||
const dot = document.createElement('div');
|
||||
dot.id = `dot-${i}`;
|
||||
dot.className = 'w-8 h-8 rounded-full border-2 border-gray-300 bg-gray-100 transition-all';
|
||||
progressDots.appendChild(dot);
|
||||
});
|
||||
|
||||
function showDialog() {
|
||||
const d = dialogs[currentDialog];
|
||||
dialogTitle.textContent = d.title;
|
||||
dialogQuestion.textContent = d.question;
|
||||
dialogInstruction.textContent = d.instruction;
|
||||
feedbackEl.classList.add('hidden');
|
||||
locked = false;
|
||||
}
|
||||
showDialog();
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (locked) return;
|
||||
if (e.key !== 'Enter' && e.key !== 'Escape') return;
|
||||
const d = dialogs[currentDialog];
|
||||
if (e.key === d.expected) {
|
||||
locked = true;
|
||||
feedbackEl.textContent = d.feedback;
|
||||
feedbackEl.className = 'text-2xl font-bold py-1 text-center text-green-600';
|
||||
feedbackEl.classList.remove('hidden');
|
||||
const dot = document.getElementById(`dot-${currentDialog}`);
|
||||
if (dot) dot.className = 'w-8 h-8 rounded-full bg-green-400 border-2 border-green-500 transition-all';
|
||||
notify('running', { step: currentDialog + 1 });
|
||||
currentDialog++;
|
||||
setTimeout(() => {
|
||||
if (currentDialog >= dialogs.length) {
|
||||
dialogBox.classList.add('hidden');
|
||||
progressDots.classList.add('hidden');
|
||||
successBanner.classList.remove('hidden');
|
||||
lucide.createIcons();
|
||||
notify('success', { score: 100 });
|
||||
notify('completed', { score: 100 });
|
||||
} else {
|
||||
showDialog();
|
||||
}
|
||||
}, 1000);
|
||||
} else {
|
||||
feedbackEl.textContent = e.key === 'Enter' ? 'Aqui não é Enter! Use Esc.' : 'Aqui não é Esc! Use Enter.';
|
||||
feedbackEl.className = 'text-2xl font-bold py-1 text-center text-red-500';
|
||||
feedbackEl.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
59
app/src/atividades/letramento/teclado/enter-esc/index.html
Normal file
@@ -0,0 +1,59 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Confirmar e Cancelar</title>
|
||||
<link rel="stylesheet" href="../../shared/letramento.css">
|
||||
<script src="../../shared/lucide.js"></script>
|
||||
<style>body { overflow: hidden; height: 100vh; width: 100vw; }</style>
|
||||
</head>
|
||||
<body class="bg-gradient-to-br from-rose-50 to-pink-100 flex items-center justify-center h-screen w-screen overflow-hidden p-4">
|
||||
<div class="w-full h-full flex flex-col">
|
||||
<div class="bg-white/95 backdrop-blur-sm border border-white/30 rounded-xl shadow-xl flex-1 flex flex-col overflow-hidden">
|
||||
<div class="flex items-center justify-center gap-4 p-4 border-b border-gray-100">
|
||||
<i data-lucide="check-square" class="w-10 h-10 text-red-500"></i>
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-gray-800">Confirmar e Cancelar</h2>
|
||||
<p class="text-lg text-gray-600">Enter confirma. Esc cancela. Aprenda a diferença!</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 p-6 flex flex-col gap-4 justify-center items-center overflow-hidden">
|
||||
<div class="grid grid-cols-2 gap-4 max-w-lg w-full">
|
||||
<div class="bg-green-50 border-2 border-green-300 rounded-2xl p-4 text-center">
|
||||
<kbd class="text-3xl font-black text-green-700 block">Enter ↵</kbd>
|
||||
<p class="text-sm text-gray-600 mt-1">Confirma / Ok / Envia</p>
|
||||
</div>
|
||||
<div class="bg-red-50 border-2 border-red-300 rounded-2xl p-4 text-center">
|
||||
<kbd class="text-3xl font-black text-red-700 block">Esc</kbd>
|
||||
<p class="text-sm text-gray-600 mt-1">Cancela / Fecha / Não</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="dialogBox" class="w-full max-w-lg border-2 border-gray-300 rounded-2xl shadow-xl overflow-hidden">
|
||||
<div class="bg-gray-100 border-b border-gray-300 px-4 py-2 flex items-center gap-2">
|
||||
<div class="w-3 h-3 rounded-full bg-red-400"></div>
|
||||
<div class="w-3 h-3 rounded-full bg-yellow-400"></div>
|
||||
<div class="w-3 h-3 rounded-full bg-green-400"></div>
|
||||
<span class="text-sm text-gray-600 ml-2" id="dialogTitle">Janela de diálogo</span>
|
||||
</div>
|
||||
<div class="p-6 text-center">
|
||||
<p class="text-xl font-bold text-gray-700 mb-2" id="dialogQuestion"></p>
|
||||
<p class="text-gray-500 text-base" id="dialogInstruction"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="feedback" class="hidden text-2xl font-bold py-1 text-center"></div>
|
||||
<div class="flex gap-2 justify-center" id="progressDots"></div>
|
||||
<div id="successBanner" class="hidden bg-gradient-to-r from-green-50 to-emerald-50 border-2 border-green-300 rounded-xl p-6 flex items-center justify-center gap-4 w-full max-w-lg">
|
||||
<i data-lucide="check-circle" class="text-green-600 w-14 h-14"></i>
|
||||
<div>
|
||||
<h2 class="text-3xl font-bold text-green-700 mb-1">Muito bem!</h2>
|
||||
<p class="text-lg text-gray-700">Agora você sabe como confirmar e cancelar ações no computador!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="./activity.js"></script>
|
||||
<script>lucide.createIcons();</script>
|
||||
</body>
|
||||
</html>
|
||||
190
app/src/atividades/letramento/teclado/labirinto/activity.js
Normal file
@@ -0,0 +1,190 @@
|
||||
const channelToken = new URLSearchParams(window.location.hash.slice(1)).get('channelToken');
|
||||
|
||||
function notify(type, payload = {}) {
|
||||
window.parent.postMessage({ type, token: channelToken, ...payload }, '*');
|
||||
}
|
||||
|
||||
const LEVELS = [
|
||||
[
|
||||
'#######',
|
||||
'#S...E#',
|
||||
'#.###.#',
|
||||
'#.....#',
|
||||
'#######',
|
||||
],
|
||||
[
|
||||
'########',
|
||||
'#S....k#',
|
||||
'#.##D###',
|
||||
'#..#..E#',
|
||||
'########',
|
||||
],
|
||||
[
|
||||
'#########',
|
||||
'#S......#',
|
||||
'#.###D###',
|
||||
'#...#...#',
|
||||
'#.#.###.#',
|
||||
'#..k#..E#',
|
||||
'#########',
|
||||
],
|
||||
[
|
||||
'##########',
|
||||
'#S..#....#',
|
||||
'#.#.#.##D#',
|
||||
'#.#...#..#',
|
||||
'#.#####.##',
|
||||
'#.....#..#',
|
||||
'#.###k#E.#',
|
||||
'##########',
|
||||
],
|
||||
[
|
||||
'###########',
|
||||
'#S...#...E#',
|
||||
'#.#.#.#.###',
|
||||
'#.#...#...#',
|
||||
'#.#####.#.#',
|
||||
'#.....#.#.#',
|
||||
'###.#.###.#',
|
||||
'#k..#.D...#',
|
||||
'###########',
|
||||
],
|
||||
];
|
||||
|
||||
let levelIndex = 0;
|
||||
let grid = [];
|
||||
let pr = 0;
|
||||
let pc = 0;
|
||||
let hasKey = false;
|
||||
let openedDoors = 0;
|
||||
let levelHasDoor = false;
|
||||
let done = false;
|
||||
|
||||
notify('started');
|
||||
|
||||
const mazeEl = document.getElementById('maze');
|
||||
const feedbackEl = document.getElementById('feedback');
|
||||
const successBanner = document.getElementById('successBanner');
|
||||
const mazeInfo = document.getElementById('mazeInfo');
|
||||
|
||||
function loadLevel(index) {
|
||||
grid = LEVELS[index].map((row) => row.split(''));
|
||||
levelHasDoor = false;
|
||||
for (let r = 0; r < grid.length; r += 1) {
|
||||
for (let c = 0; c < grid[r].length; c += 1) {
|
||||
if (grid[r][c] === 'S') {
|
||||
pr = r;
|
||||
pc = c;
|
||||
}
|
||||
if (grid[r][c] === 'D') {
|
||||
levelHasDoor = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
hasKey = false;
|
||||
openedDoors = 0;
|
||||
feedbackEl.classList.add('hidden');
|
||||
mazeInfo.textContent = 'Use as setas para chegar à saída';
|
||||
}
|
||||
|
||||
function buildMaze() {
|
||||
mazeEl.innerHTML = '';
|
||||
grid.forEach((row, r) => {
|
||||
const rowEl = document.createElement('div');
|
||||
rowEl.style.display = 'flex';
|
||||
row.forEach((cell, c) => {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'cell';
|
||||
if (r === pr && c === pc) {
|
||||
el.innerHTML = '<div style="width:36px;height:36px;border-radius:50%;background:#14b8a6;display:flex;align-items:center;justify-content:center;color:white;font-weight:900;font-size:1rem;">P</div>';
|
||||
el.style.background = '#ecfdf5';
|
||||
} else if (cell === '#') {
|
||||
el.style.background = '#374151';
|
||||
} else if (cell === 'E') {
|
||||
el.innerHTML = '<div style="width:36px;height:36px;border-radius:8px;background:#22c55e;display:flex;align-items:center;justify-content:center;color:white;font-weight:900;font-size:1rem;">S</div>';
|
||||
el.style.background = '#fef9c3';
|
||||
} else if (cell === 'k') {
|
||||
el.innerHTML = '<div style="width:32px;height:32px;border-radius:8px;background:#fef9c3;border:1px solid #fcd34d;display:flex;align-items:center;justify-content:center;"><i data-lucide="key-round" style="width:18px;height:18px;color:#a16207"></i></div>';
|
||||
el.style.background = '#fffbeb';
|
||||
} else if (cell === 'D') {
|
||||
el.innerHTML = '<div style="width:32px;height:32px;border-radius:8px;background:#ffedd5;border:1px solid #fdba74;display:flex;align-items:center;justify-content:center;"><i data-lucide="lock" style="width:18px;height:18px;color:#b45309"></i></div>';
|
||||
el.style.background = '#fffbeb';
|
||||
} else {
|
||||
el.style.background = '#f9fafb';
|
||||
}
|
||||
rowEl.appendChild(el);
|
||||
});
|
||||
mazeEl.appendChild(rowEl);
|
||||
});
|
||||
lucide.createIcons();
|
||||
}
|
||||
loadLevel(levelIndex);
|
||||
buildMaze();
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (done) return;
|
||||
const moves = { ArrowUp: [-1,0], ArrowDown: [1,0], ArrowLeft: [0,-1], ArrowRight: [0,1] };
|
||||
if (!moves[e.key]) return;
|
||||
e.preventDefault();
|
||||
const [dr, dc] = moves[e.key];
|
||||
const nr = pr + dr, nc = pc + dc;
|
||||
if (nr < 0 || nr >= grid.length || nc < 0 || nc >= grid[0].length) return;
|
||||
const cell = grid[nr][nc];
|
||||
if (cell === '#') {
|
||||
feedbackEl.textContent = 'Parede! Escolha outra direção.';
|
||||
feedbackEl.className = 'text-xl font-bold py-1 text-center text-orange-500';
|
||||
feedbackEl.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
if (cell === 'D' && !hasKey) {
|
||||
feedbackEl.textContent = 'Porta trancada! Encontre a chave.';
|
||||
feedbackEl.className = 'text-xl font-bold py-1 text-center text-amber-600';
|
||||
feedbackEl.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
feedbackEl.classList.add('hidden');
|
||||
if (cell === 'k') {
|
||||
hasKey = true;
|
||||
mazeInfo.textContent = 'Chave coletada! Agora abra a porta.';
|
||||
grid[nr][nc] = '.';
|
||||
}
|
||||
if (cell === 'D' && hasKey) {
|
||||
openedDoors += 1;
|
||||
grid[nr][nc] = '.';
|
||||
mazeInfo.textContent = 'Porta desbloqueada! Siga até a saída.';
|
||||
}
|
||||
|
||||
if (cell === 'E' && levelHasDoor && openedDoors === 0) {
|
||||
feedbackEl.textContent = 'A saída está bloqueada. Abra a porta primeiro.';
|
||||
feedbackEl.className = 'text-xl font-bold py-1 text-center text-amber-600';
|
||||
feedbackEl.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
pr = nr; pc = nc;
|
||||
buildMaze();
|
||||
notify('running', { step: levelIndex + 1 });
|
||||
|
||||
if (cell === 'E') {
|
||||
if (levelIndex === LEVELS.length - 1) {
|
||||
done = true;
|
||||
mazeEl.classList.add('hidden');
|
||||
successBanner.classList.remove('hidden');
|
||||
lucide.createIcons();
|
||||
notify('success', { score: 100 });
|
||||
notify('completed', { score: 100 });
|
||||
return;
|
||||
}
|
||||
|
||||
levelIndex += 1;
|
||||
feedbackEl.textContent = `Fase ${levelIndex} concluída!`;
|
||||
feedbackEl.className = 'text-xl font-bold py-1 text-center text-green-600';
|
||||
feedbackEl.classList.remove('hidden');
|
||||
notify('running', { step: levelIndex + 1 });
|
||||
setTimeout(() => {
|
||||
loadLevel(levelIndex);
|
||||
buildMaze();
|
||||
}, 700);
|
||||
}
|
||||
});
|
||||
49
app/src/atividades/letramento/teclado/labirinto/index.html
Normal file
@@ -0,0 +1,49 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Labirinto das Setas</title>
|
||||
<link rel="stylesheet" href="../../shared/letramento.css">
|
||||
<script src="../../shared/lucide.js"></script>
|
||||
<style>body { overflow: hidden; height: 100vh; width: 100vw; }
|
||||
.cell { width: 42px; height: 42px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; font-size: 1.1rem; border: 2px solid #e5e7eb; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gradient-to-br from-brand-50 to-gray-100 flex items-center justify-center h-screen w-screen overflow-hidden p-4">
|
||||
<div class="w-full h-full flex flex-col">
|
||||
<div class="bg-white/95 backdrop-blur-sm border border-white/30 rounded-xl shadow-xl flex-1 flex flex-col overflow-hidden">
|
||||
<div class="flex items-center justify-center gap-4 p-4 border-b border-gray-100">
|
||||
<i data-lucide="map" class="w-10 h-10 text-red-500"></i>
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-gray-800">Labirinto das Setas</h2>
|
||||
<p class="text-lg text-gray-600">Complete 5 mapas com progressão de dificuldade, coletando chaves para abrir portas.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 p-3 flex flex-col gap-2 justify-center items-center overflow-hidden">
|
||||
<div class="w-full flex-1 flex items-center justify-center overflow-auto">
|
||||
<div id="maze" class="flex flex-col gap-0 border-2 border-teal-400 rounded-lg overflow-hidden shadow-lg"></div>
|
||||
</div>
|
||||
<div class="flex gap-6 items-center flex-wrap justify-center">
|
||||
<div class="flex items-center gap-2"><div style="width:36px;height:36px;border-radius:50%;background:#14b8a6;display:flex;align-items:center;justify-content:center;color:white;font-weight:900;font-size:1rem;">P</div><p class="text-gray-600">Você</p></div>
|
||||
<div class="flex items-center gap-2"><div style="width:36px;height:36px;border-radius:8px;background:#22c55e;display:flex;align-items:center;justify-content:center;color:white;font-weight:900;font-size:1rem;">S</div><p class="text-gray-600">Saída</p></div>
|
||||
<div class="flex items-center gap-2"><div class="w-6 h-6 bg-gray-700 rounded"></div><p class="text-gray-600">Parede</p></div>
|
||||
<div class="flex items-center gap-2"><div class="w-8 h-8 rounded-md bg-yellow-100 border border-yellow-300 flex items-center justify-center"><i data-lucide="key-round" class="w-4 h-4 text-yellow-700"></i></div><p class="text-gray-600">Chave</p></div>
|
||||
<div class="flex items-center gap-2"><div class="w-8 h-8 rounded-md bg-amber-100 border border-amber-300 flex items-center justify-center"><i data-lucide="lock" class="w-4 h-4 text-amber-700"></i></div><p class="text-gray-600">Porta</p></div>
|
||||
</div>
|
||||
<p class="text-gray-500 text-sm md:text-base" id="mazeInfo">Use as setas para chegar à saída</p>
|
||||
<div id="feedback" class="hidden text-2xl font-bold py-1 text-center"></div>
|
||||
<div id="successBanner" class="hidden bg-gradient-to-r from-green-50 to-emerald-50 border-2 border-green-300 rounded-xl p-6 flex items-center justify-center gap-4 w-full max-w-lg">
|
||||
<i data-lucide="check-circle" class="text-green-600 w-14 h-14"></i>
|
||||
<div>
|
||||
<h2 class="text-3xl font-bold text-green-700 mb-1">Labirintos concluídos!</h2>
|
||||
<p class="text-lg text-gray-700">Você dominou setas, chaves e portas.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="./activity.js"></script>
|
||||
<script>lucide.createIcons();</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,53 @@
|
||||
const channelToken = new URLSearchParams(window.location.hash.slice(1)).get('channelToken');
|
||||
|
||||
function notify(type, payload = {}) {
|
||||
window.parent.postMessage({ type, token: channelToken, ...payload }, '*');
|
||||
}
|
||||
notify('started');
|
||||
|
||||
const messageArea = document.getElementById('messageArea');
|
||||
const confirmBtn = document.getElementById('confirmBtn');
|
||||
const successBanner = document.getElementById('successBanner');
|
||||
const messagePreview = document.getElementById('messagePreview');
|
||||
const reqWords = document.getElementById('req-words');
|
||||
const reqUpper = document.getElementById('req-upper');
|
||||
const reqPeriod = document.getElementById('req-period');
|
||||
|
||||
messageArea.focus();
|
||||
|
||||
function setReq(el, ok) {
|
||||
el.className = ok
|
||||
? 'px-4 py-2 rounded-full border-2 border-green-400 text-sm font-bold text-green-700 bg-green-50 transition-all'
|
||||
: 'px-4 py-2 rounded-full border-2 border-gray-300 text-sm font-bold text-gray-600 transition-all';
|
||||
}
|
||||
|
||||
messageArea.addEventListener('input', () => {
|
||||
const v = messageArea.value.trim();
|
||||
const words = v.split(/\s+/).filter(w => w.length > 0);
|
||||
const hasWords = words.length >= 3;
|
||||
const hasUpper = v.length > 0 && v[0] === v[0].toUpperCase() && /[A-Za-zÀ-ú]/.test(v[0]);
|
||||
const hasPeriod = v.endsWith('.');
|
||||
|
||||
setReq(reqWords, hasWords);
|
||||
setReq(reqUpper, hasUpper);
|
||||
setReq(reqPeriod, hasPeriod);
|
||||
|
||||
if (hasWords && hasUpper && hasPeriod) {
|
||||
confirmBtn.classList.remove('hidden');
|
||||
notify('running', { step: 1 });
|
||||
} else {
|
||||
confirmBtn.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
confirmBtn.addEventListener('click', () => {
|
||||
const v = messageArea.value.trim();
|
||||
messagePreview.textContent = `"${v}"`;
|
||||
messageArea.parentElement.classList.add('hidden');
|
||||
document.getElementById('reqChecklist').classList.add('hidden');
|
||||
confirmBtn.classList.add('hidden');
|
||||
successBanner.classList.remove('hidden');
|
||||
lucide.createIcons();
|
||||
notify('success', { score: 100 });
|
||||
notify('completed', { score: 100 });
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Texto Coletivo</title>
|
||||
<link rel="stylesheet" href="../../shared/letramento.css">
|
||||
<script src="../../shared/lucide.js"></script>
|
||||
<style>body { overflow: hidden; height: 100vh; width: 100vw; }</style>
|
||||
</head>
|
||||
<body class="bg-gradient-to-br from-emerald-50 to-teal-100 flex items-center justify-center h-screen w-screen overflow-hidden p-4">
|
||||
<div class="w-full h-full flex flex-col">
|
||||
<div class="bg-white/95 backdrop-blur-sm border border-white/30 rounded-xl shadow-xl flex-1 flex flex-col overflow-hidden">
|
||||
<div class="flex items-center justify-center gap-4 p-4 border-b border-gray-100">
|
||||
<i data-lucide="mail" class="w-10 h-10 text-red-500"></i>
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-gray-800">Texto Coletivo</h2>
|
||||
<p class="text-lg text-gray-600">Escreva uma mensagem sobre tecnologia, comunidade e soberania digital.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 p-6 flex flex-col gap-4 overflow-hidden">
|
||||
<div class="bg-emerald-50 border-2 border-emerald-200 rounded-2xl p-4 text-center">
|
||||
<p class="text-lg text-emerald-800">
|
||||
Escreva uma mensagem curta com: <strong>letra maiúscula no início</strong>, <strong>pelo menos 3 palavras</strong> e <strong>ponto final</strong>.
|
||||
</p>
|
||||
</div>
|
||||
<div id="reqChecklist" class="flex gap-3 flex-wrap justify-center">
|
||||
<div id="req-words" class="px-4 py-2 rounded-full border-2 border-gray-300 text-sm font-bold text-gray-600 transition-all">Mínimo 3 palavras</div>
|
||||
<div id="req-upper" class="px-4 py-2 rounded-full border-2 border-gray-300 text-sm font-bold text-gray-600 transition-all">Começa com Maiúscula</div>
|
||||
<div id="req-period" class="px-4 py-2 rounded-full border-2 border-gray-300 text-sm font-bold text-gray-600 transition-all">Termina com ponto</div>
|
||||
</div>
|
||||
<div class="flex-1 flex flex-col">
|
||||
<textarea id="messageArea" rows="4" spellcheck="false" autocomplete="off"
|
||||
class="flex-1 text-xl font-mono border-2 border-emerald-400 rounded-2xl p-4 resize-none outline-none focus:border-emerald-600 bg-white shadow text-gray-700"
|
||||
placeholder="Exemplo: Tecnologia livre fortalece nossa comunidade."></textarea>
|
||||
</div>
|
||||
<button id="confirmBtn" class="hidden bg-emerald-500 hover:bg-emerald-600 text-white text-xl font-bold px-10 py-4 rounded-2xl shadow-lg transition-all active:scale-95 self-center">
|
||||
Enviar Recado ✓
|
||||
</button>
|
||||
<div id="successBanner" class="hidden bg-gradient-to-r from-green-50 to-emerald-50 border-2 border-green-300 rounded-xl p-6 flex flex-col items-center gap-3 w-full text-center">
|
||||
<i data-lucide="mail-check" class="text-green-600 w-16 h-16"></i>
|
||||
<h2 class="text-3xl font-bold text-green-700">Recado enviado!</h2>
|
||||
<p id="messagePreview" class="text-xl text-gray-700 italic max-w-lg"></p>
|
||||
<p class="text-lg text-gray-600">Você concluiu a etapa de escrita coletiva. Parabéns!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="./activity.js"></script>
|
||||
<script>lucide.createIcons();</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,84 @@
|
||||
const channelToken = new URLSearchParams(window.location.hash.slice(1)).get('channelToken');
|
||||
|
||||
function notify(type, payload = {}) {
|
||||
window.parent.postMessage({ type, token: channelToken, ...payload }, '*');
|
||||
}
|
||||
|
||||
const tasks = [
|
||||
{
|
||||
initial: 'Tecnologia livr fortalece comunidades',
|
||||
expected: 'Tecnologia livre fortalece comunidades',
|
||||
text: 'Falta uma letra no meio da palavra. Use as setas para posicionar o cursor e corrigir.',
|
||||
hint: 'Mova com → até depois de "livr" e adicione "e".',
|
||||
},
|
||||
{
|
||||
initial: 'Soberania digital protege os ddados da comunidade',
|
||||
expected: 'Soberania digital protege os dados da comunidade',
|
||||
text: 'Há uma letra repetida. Volte com as setas e corrija sem apagar tudo.',
|
||||
hint: 'Use ← para voltar até "ddados" e deixe apenas "dados".',
|
||||
},
|
||||
];
|
||||
|
||||
let currentTask = 0;
|
||||
notify('started');
|
||||
|
||||
const taskText = document.getElementById('taskText');
|
||||
const taskHint = document.getElementById('taskHint');
|
||||
const expectedText = document.getElementById('expectedText');
|
||||
const editInput = document.getElementById('editInput');
|
||||
const feedbackEl = document.getElementById('feedback');
|
||||
const cursorPos = document.getElementById('cursorPos');
|
||||
const progressDots = document.getElementById('progressDots');
|
||||
const successBanner = document.getElementById('successBanner');
|
||||
const taskBox = document.getElementById('taskBox');
|
||||
|
||||
tasks.forEach((_, i) => {
|
||||
const dot = document.createElement('div');
|
||||
dot.id = `dot-${i}`;
|
||||
dot.className = 'w-8 h-8 rounded-full border-2 border-gray-300 bg-gray-100 transition-all';
|
||||
progressDots.appendChild(dot);
|
||||
});
|
||||
|
||||
function showTask() {
|
||||
const t = tasks[currentTask];
|
||||
taskText.textContent = t.text;
|
||||
taskHint.textContent = t.hint;
|
||||
expectedText.textContent = t.expected;
|
||||
editInput.value = t.initial;
|
||||
editInput.setSelectionRange(0, 0);
|
||||
feedbackEl.classList.add('hidden');
|
||||
editInput.focus();
|
||||
}
|
||||
showTask();
|
||||
|
||||
editInput.addEventListener('keyup', () => {
|
||||
cursorPos.textContent = editInput.selectionStart;
|
||||
});
|
||||
editInput.addEventListener('click', () => {
|
||||
cursorPos.textContent = editInput.selectionStart;
|
||||
});
|
||||
|
||||
editInput.addEventListener('input', () => {
|
||||
const t = tasks[currentTask];
|
||||
if (editInput.value === t.expected) {
|
||||
feedbackEl.textContent = 'Correto!';
|
||||
feedbackEl.className = 'text-2xl font-bold py-1 text-center text-green-600';
|
||||
feedbackEl.classList.remove('hidden');
|
||||
const dot = document.getElementById(`dot-${currentTask}`);
|
||||
if (dot) dot.className = 'w-8 h-8 rounded-full bg-green-400 border-2 border-green-500 transition-all';
|
||||
notify('running', { step: currentTask + 1 });
|
||||
currentTask++;
|
||||
setTimeout(() => {
|
||||
if (currentTask >= tasks.length) {
|
||||
taskBox.classList.add('hidden');
|
||||
progressDots.classList.add('hidden');
|
||||
successBanner.classList.remove('hidden');
|
||||
lucide.createIcons();
|
||||
notify('success', { score: 100 });
|
||||
notify('completed', { score: 100 });
|
||||
} else {
|
||||
showTask();
|
||||
}
|
||||
}, 800);
|
||||
}
|
||||
});
|
||||
53
app/src/atividades/letramento/teclado/setas-texto/index.html
Normal file
@@ -0,0 +1,53 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Setas no Texto</title>
|
||||
<link rel="stylesheet" href="../../shared/letramento.css">
|
||||
<script src="../../shared/lucide.js"></script>
|
||||
<style>body { overflow: hidden; height: 100vh; width: 100vw; }</style>
|
||||
</head>
|
||||
<body class="bg-gradient-to-br from-brand-50 to-gray-100 flex items-center justify-center h-screen w-screen overflow-hidden p-4">
|
||||
<div class="w-full h-full flex flex-col">
|
||||
<div class="bg-white/95 backdrop-blur-sm border border-white/30 rounded-xl shadow-xl flex-1 flex flex-col overflow-hidden">
|
||||
<div class="flex items-center justify-center gap-4 p-4 border-b border-gray-100">
|
||||
<i data-lucide="move-horizontal" class="w-10 h-10 text-red-500"></i>
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-gray-800">Setas no Texto</h2>
|
||||
<p class="text-lg text-gray-600">Use ← e → para corrigir frases sobre tecnologia e cidadania digital.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 p-6 flex flex-col gap-4 justify-center items-center overflow-hidden">
|
||||
<div class="bg-teal-50 border-2 border-teal-200 rounded-2xl p-4 max-w-lg text-center">
|
||||
<p class="text-lg text-teal-800">
|
||||
As setas <strong>← →</strong> movem o cursor <em>sem apagar nada</em>. Volte ao meio do texto para corrigir letras com precisão.
|
||||
</p>
|
||||
</div>
|
||||
<div id="taskBox" class="w-full max-w-lg text-center">
|
||||
<p class="text-xl font-bold text-gray-700" id="taskText"></p>
|
||||
<p class="text-base text-gray-500 mt-1 mb-3" id="taskHint"></p>
|
||||
<div class="bg-gray-50 rounded-xl border-2 border-gray-200 p-3 mb-3">
|
||||
<p class="text-sm text-gray-400 mb-1">Resultado esperado:</p>
|
||||
<p id="expectedText" class="text-2xl font-mono text-green-700 font-bold"></p>
|
||||
</div>
|
||||
<input id="editInput" type="text" autocomplete="off" spellcheck="false"
|
||||
class="text-2xl font-mono text-center border-b-4 border-teal-400 bg-transparent outline-none w-full py-2 text-gray-700" />
|
||||
<p class="text-sm text-gray-400 mt-1">Posição do cursor: <span id="cursorPos">0</span></p>
|
||||
</div>
|
||||
<div id="feedback" class="hidden text-2xl font-bold py-1 text-center"></div>
|
||||
<div class="flex gap-2 justify-center" id="progressDots"></div>
|
||||
<div id="successBanner" class="hidden bg-gradient-to-r from-green-50 to-emerald-50 border-2 border-green-300 rounded-xl p-6 flex items-center justify-center gap-4 w-full max-w-lg">
|
||||
<i data-lucide="check-circle" class="text-green-600 w-14 h-14"></i>
|
||||
<div>
|
||||
<h2 class="text-3xl font-bold text-green-700 mb-1">Parabéns!</h2>
|
||||
<p class="text-lg text-gray-700">Você navegou no texto usando apenas as setas do teclado.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="./activity.js"></script>
|
||||
<script>lucide.createIcons();</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,129 @@
|
||||
const channelToken = new URLSearchParams(window.location.hash.slice(1)).get('channelToken');
|
||||
|
||||
function notify(type, payload = {}) {
|
||||
window.parent.postMessage({ type, token: channelToken, ...payload }, '*');
|
||||
}
|
||||
|
||||
const phases = [
|
||||
{ label: 'Fase 1 — Uma letra extra', badgeClass: 'bg-blue-100 text-blue-700', barClass: 'bg-blue-400' },
|
||||
{ label: 'Fase 2 — Letra no meio', badgeClass: 'bg-indigo-100 text-indigo-700', barClass: 'bg-indigo-400' },
|
||||
{ label: 'Fase 3 — Apagar e redigitar', badgeClass: 'bg-orange-100 text-orange-700', barClass: 'bg-orange-400' },
|
||||
{ label: 'Fase 4 — Duas correções', badgeClass: 'bg-red-100 text-red-700', barClass: 'bg-red-400' },
|
||||
{ label: 'Fase 5 — Frases longas', badgeClass: 'bg-purple-100 text-purple-700', barClass: 'bg-purple-500' },
|
||||
];
|
||||
|
||||
const tasks = [
|
||||
// Fase 1 – Uma letra extra no final
|
||||
{ error: 'Dadoss', correct: 'Dados', hint: 'Apague a letra repetida no final.' },
|
||||
{ error: 'Redee', correct: 'Rede', hint: 'Há um "e" extra no final.' },
|
||||
{ error: 'Codigoo', correct: 'Código', hint: 'Apague o "o" repetido, cuidado com o acento.' },
|
||||
{ error: 'Aprendisado', correct: 'Aprendizado', hint: 'Corrija a palavra "Aprendisado".' },
|
||||
{ error: 'Auulass', correct: 'Aulas', hint: 'Apague o "u" e "s" duplicados.' },
|
||||
{ error: 'Comunidadde', correct: 'Comunidade', hint: 'A palavra tem uma letra repetida no final.' },
|
||||
|
||||
// Fase 2 – Letra extra em posição variada
|
||||
{ error: 'Tecnoologia livre', correct: 'Tecnologia livre', hint: 'Corrija a palavra "Tecnoologia".' },
|
||||
{ error: 'Soberannia digital', correct: 'Soberania digital', hint: 'Há um "n" a mais em "Soberannia".' },
|
||||
{ error: 'Internet para toddos', correct: 'Internet para todos', hint: 'Corrija a palavra "toddos".' },
|
||||
{ error: 'Laboratóriio de código', correct: 'Laboratório de código', hint: 'Um "i" está sobrando.' },
|
||||
{ error: 'Educaçãao popular', correct: 'Educação popular', hint: 'Apague a letra extra em "Educaçãao".' },
|
||||
{ error: 'Tecnologia para a comunnidade', correct: 'Tecnologia para a comunidade', hint: 'Há um "n" extra em "comunnidade".' },
|
||||
|
||||
// Fase 3 – Apagar tudo e redigitar
|
||||
{ error: 'SOBERANIA DIGITAL', correct: 'soberania digital', hint: 'Apague tudo e redigite em minúsculas.' },
|
||||
{ error: 'dados abertos', correct: 'DADOS ABERTOS', hint: 'Reescreva em letras maiúsculas.' },
|
||||
{ error: 'REDE COMUNITÁRIA', correct: 'rede comunitária', hint: 'Reescreva em letras minúsculas.' },
|
||||
{ error: 'tecnologia social', correct: 'TECNOLOGIA SOCIAL', hint: 'Apague e redigite em MAIÚSCULAS.' },
|
||||
{ error: 'CIDADANIA DIGITAL', correct: 'cidadania digital', hint: 'Apague e redigite em minúsculas.' },
|
||||
|
||||
// Fase 4 – Frases com dois erros
|
||||
{ error: 'Tecnoologia para toddos', correct: 'Tecnologia para todos', hint: 'Corrija duas palavras com letras extras.' },
|
||||
{ error: 'Internet livre para a comunnidade', correct: 'Internet livre para a comunidade', hint: 'A palavra final tem uma letra repetida.' },
|
||||
{ error: 'Educaçãao digital fortalece o territórrio', correct: 'Educação digital fortalece o território', hint: 'Corrija as duas palavras com letras sobrando.' },
|
||||
{ error: 'Redee comunitária amplia vozess locais', correct: 'Rede comunitária amplia vozes locais', hint: 'Há duas letras repetidas em palavras diferentes.' },
|
||||
{ error: 'Soberannia digital protege daddos', correct: 'Soberania digital protege dados', hint: 'Corrija as duas palavras com repetição.' },
|
||||
|
||||
// Fase 5 – Frases mais longas
|
||||
{ error: 'A tecnoologia livre fortalece a autonomia da comunnidade.', correct: 'A tecnologia livre fortalece a autonomia da comunidade.', hint: 'Corrija as duas palavras com letras extras.' },
|
||||
{ error: 'Educaçãao popular e internet livre ampliam oportuniddades.', correct: 'Educação popular e internet livre ampliam oportunidades.', hint: 'Há duas palavras com letras repetidas.' },
|
||||
{ error: 'Dadoss abertos apoiam a participaçãao social.', correct: 'Dados abertos apoiam a participação social.', hint: 'Corrija as duas palavras com letras sobrando.' },
|
||||
{ error: 'A redee comunitária fortalece vínculoss no território.', correct: 'A rede comunitária fortalece vínculos no território.', hint: 'Remova as repetições em duas palavras.' },
|
||||
{ error: 'Soberannia digital exige cuidado com dadoss pessoais.', correct: 'Soberania digital exige cuidado com dados pessoais.', hint: 'Corrija duas palavras com letras extras.' },
|
||||
{ error: 'Você já domina o Backspace e corrige textoss com atenção.', correct: 'Você já domina o Backspace e corrige textos com atenção.', hint: 'Etapa final: encontre a letra repetida.' },
|
||||
];
|
||||
|
||||
let current = 0;
|
||||
let lastNotifiedPhase = -1;
|
||||
notify('started');
|
||||
|
||||
const phaseBadge = document.getElementById('phaseBadge');
|
||||
const stepCounter = document.getElementById('stepCounter');
|
||||
const progressBar = document.getElementById('progressBar');
|
||||
const errorText = document.getElementById('errorText');
|
||||
const correctText = document.getElementById('correctText');
|
||||
const taskHint = document.getElementById('taskHint');
|
||||
const editInput = document.getElementById('editInput');
|
||||
const feedbackEl = document.getElementById('feedback');
|
||||
const successBanner = document.getElementById('successBanner');
|
||||
const taskBox = document.getElementById('taskBox');
|
||||
|
||||
function getPhaseIdx(idx) { return Math.floor(idx / 6); }
|
||||
|
||||
function syncInputHeight() {
|
||||
editInput.style.height = 'auto';
|
||||
editInput.style.height = `${Math.max(editInput.scrollHeight, 120)}px`;
|
||||
}
|
||||
|
||||
function showTask() {
|
||||
const t = tasks[current];
|
||||
const phaseIdx = getPhaseIdx(current);
|
||||
const phase = phases[phaseIdx];
|
||||
|
||||
if (phaseIdx !== lastNotifiedPhase) {
|
||||
notify('running', { step: phaseIdx + 1 });
|
||||
lastNotifiedPhase = phaseIdx;
|
||||
}
|
||||
|
||||
phaseBadge.textContent = phase.label;
|
||||
phaseBadge.className = `text-xs font-bold px-3 py-1 rounded-full transition-all ${phase.badgeClass}`;
|
||||
progressBar.className = `h-2.5 rounded-full transition-all duration-500 ${phase.barClass}`;
|
||||
progressBar.style.width = `${(current / tasks.length) * 100}%`;
|
||||
stepCounter.textContent = `Passo ${current + 1} de ${tasks.length}`;
|
||||
|
||||
errorText.textContent = t.error;
|
||||
correctText.textContent = t.correct;
|
||||
taskHint.textContent = t.hint;
|
||||
editInput.value = t.error;
|
||||
editInput.disabled = false;
|
||||
syncInputHeight();
|
||||
editInput.focus();
|
||||
feedbackEl.classList.add('hidden');
|
||||
}
|
||||
showTask();
|
||||
|
||||
editInput.addEventListener('input', () => {
|
||||
syncInputHeight();
|
||||
if (editInput.value === tasks[current].correct) {
|
||||
feedbackEl.textContent = 'Correto!';
|
||||
feedbackEl.className = 'text-xl font-bold py-1 text-center text-green-600';
|
||||
feedbackEl.classList.remove('hidden');
|
||||
editInput.disabled = true;
|
||||
current++;
|
||||
setTimeout(() => {
|
||||
if (current >= tasks.length) {
|
||||
taskBox.classList.add('hidden');
|
||||
progressBar.style.width = '100%';
|
||||
successBanner.classList.remove('hidden');
|
||||
lucide.createIcons();
|
||||
notify('success', { score: 100 });
|
||||
notify('completed', { score: 100 });
|
||||
} else {
|
||||
showTask();
|
||||
}
|
||||
}, 700);
|
||||
}
|
||||
});
|
||||
|
||||
editInput.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') e.preventDefault();
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Backspace: Correções no Texto</title>
|
||||
<link rel="stylesheet" href="../../shared/letramento.css">
|
||||
<script src="../../shared/lucide.js"></script>
|
||||
<style>body { overflow: hidden; height: 100vh; width: 100vw; }</style>
|
||||
</head>
|
||||
<body class="bg-gradient-to-br from-brand-50 to-gray-100 flex items-center justify-center h-screen w-screen overflow-hidden p-4">
|
||||
<div class="w-full h-full flex flex-col">
|
||||
<div class="bg-white/95 backdrop-blur-sm border border-white/30 rounded-xl shadow-xl flex-1 flex flex-col overflow-hidden">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-center gap-4 p-4 border-b border-gray-100">
|
||||
<i data-lucide="eraser" class="w-10 h-10 text-red-500"></i>
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-gray-800">Backspace: Correções no Texto</h2>
|
||||
<p class="text-base text-gray-500">Posicione o cursor, apague erros e reescreva frases sobre tecnologia cidadã.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Main -->
|
||||
<div class="flex-1 p-5 flex flex-col gap-4 justify-center items-center overflow-hidden">
|
||||
<!-- Phase + progress -->
|
||||
<div class="w-full max-w-lg">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span id="phaseBadge" class="text-xs font-bold px-3 py-1 rounded-full transition-all"></span>
|
||||
<span id="stepCounter" class="text-sm font-semibold text-gray-500"></span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-2.5 overflow-hidden">
|
||||
<div id="progressBar" class="h-2.5 rounded-full transition-all duration-500 w-0"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Task box -->
|
||||
<div id="taskBox" class="w-full max-w-lg flex flex-col gap-3">
|
||||
<p id="taskHint" class="text-base text-gray-500 text-center font-medium"></p>
|
||||
<div class="flex gap-3">
|
||||
<div class="flex-1 bg-red-50 border-2 border-red-200 rounded-xl p-3 text-center">
|
||||
<p class="text-xs font-bold text-red-400 mb-1 uppercase tracking-wide">Com erro</p>
|
||||
<p id="errorText" class="text-xl font-mono text-red-700 font-bold break-all"></p>
|
||||
</div>
|
||||
<div class="flex-1 bg-green-50 border-2 border-green-200 rounded-xl p-3 text-center">
|
||||
<p class="text-xs font-bold text-green-400 mb-1 uppercase tracking-wide">Corrija para</p>
|
||||
<p id="correctText" class="text-xl font-mono text-green-700 font-bold break-all"></p>
|
||||
</div>
|
||||
</div>
|
||||
<textarea id="editInput" rows="3" autocomplete="off" spellcheck="false"
|
||||
class="text-xl font-mono text-center border-2 border-indigo-300 rounded-xl bg-white/80 outline-none w-full py-3 px-4 text-indigo-700 resize-none overflow-y-auto leading-relaxed"></textarea>
|
||||
<p class="text-xs text-gray-400 text-center">Use o Backspace para apagar, depois corrija o texto</p>
|
||||
</div>
|
||||
<div id="feedback" class="hidden text-xl font-bold py-1 text-center"></div>
|
||||
<!-- Success -->
|
||||
<div id="successBanner" class="hidden bg-gradient-to-r from-green-50 to-emerald-50 border-2 border-green-300 rounded-xl p-6 flex items-center justify-center gap-4 w-full max-w-lg">
|
||||
<i data-lucide="check-circle" class="text-green-600 w-14 h-14"></i>
|
||||
<div>
|
||||
<h2 class="text-3xl font-bold text-green-700 mb-1">Excelente!</h2>
|
||||
<p class="text-lg text-gray-700">Você concluiu 30 etapas de correção. O Backspace está na ponta dos dedos.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="./activity.js"></script>
|
||||
<script>lucide.createIcons();</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,75 @@
|
||||
const channelToken = new URLSearchParams(window.location.hash.slice(1)).get('channelToken');
|
||||
|
||||
function notify(type, payload = {}) {
|
||||
window.parent.postMessage({ type, token: channelToken, ...payload }, '*');
|
||||
}
|
||||
|
||||
notify('started');
|
||||
|
||||
const tasks = [
|
||||
{ prompt: 'Digite exatamente: computador', target: 'computador', placeholder: 'computador' },
|
||||
{ prompt: 'Digite exatamente: 2026', target: '2026', placeholder: '2026' },
|
||||
{ prompt: 'Digite exatamente: dados são poder', target: 'dados são poder', placeholder: 'dados são poder' },
|
||||
{ prompt: 'Digite exatamente: tecnologia popular', target: 'tecnologia popular', placeholder: 'tecnologia popular' },
|
||||
{ prompt: 'Digite exatamente: soberania digital', target: 'soberania digital', placeholder: 'soberania digital' },
|
||||
{ prompt: 'Digite exatamente: estou aprendendo tecnologia', target: 'estou aprendendo tecnologia', placeholder: 'estou aprendendo tecnologia' },
|
||||
];
|
||||
|
||||
let current = 0;
|
||||
|
||||
const taskLabel = document.getElementById('taskLabel');
|
||||
const stepLabel = document.getElementById('stepLabel');
|
||||
const textInput = document.getElementById('textInput');
|
||||
const feedback = document.getElementById('feedback');
|
||||
const confirmBtn = document.getElementById('confirmBtn');
|
||||
const successBanner = document.getElementById('successBanner');
|
||||
const formWrap = document.getElementById('formWrap');
|
||||
|
||||
function showTask() {
|
||||
const task = tasks[current];
|
||||
stepLabel.textContent = `Passo ${current + 1} de ${tasks.length}`;
|
||||
taskLabel.textContent = task.prompt;
|
||||
textInput.value = '';
|
||||
textInput.placeholder = task.placeholder;
|
||||
feedback.textContent = '';
|
||||
textInput.focus();
|
||||
}
|
||||
|
||||
function checkInput() {
|
||||
const task = tasks[current];
|
||||
const value = textInput.value;
|
||||
confirmBtn.disabled = value !== task.target;
|
||||
if (!value) {
|
||||
feedback.textContent = '';
|
||||
return;
|
||||
}
|
||||
feedback.textContent = value === task.target ? 'Correto! Pode avançar.' : 'Confira letras, acentos e espaços.';
|
||||
feedback.className = value === task.target ? 'text-base text-emerald-700 font-semibold' : 'text-base text-rose-600 font-semibold';
|
||||
}
|
||||
|
||||
textInput.addEventListener('input', checkInput);
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' && !confirmBtn.disabled) {
|
||||
e.preventDefault();
|
||||
confirmBtn.click();
|
||||
}
|
||||
});
|
||||
|
||||
confirmBtn.addEventListener('click', () => {
|
||||
notify('running', { step: current + 1 });
|
||||
current += 1;
|
||||
if (current >= tasks.length) {
|
||||
formWrap.classList.add('hidden');
|
||||
successBanner.classList.remove('hidden');
|
||||
lucide.createIcons();
|
||||
notify('success', { score: 100 });
|
||||
notify('completed', { score: 100 });
|
||||
return;
|
||||
}
|
||||
showTask();
|
||||
confirmBtn.disabled = true;
|
||||
});
|
||||
|
||||
confirmBtn.disabled = true;
|
||||
showTask();
|
||||
@@ -0,0 +1,47 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Prática de Escrita</title>
|
||||
<link rel="stylesheet" href="../../shared/letramento.css">
|
||||
<script src="../../shared/lucide.js"></script>
|
||||
<style>body { overflow: hidden; height: 100vh; width: 100vw; }</style>
|
||||
</head>
|
||||
<body class="bg-gradient-to-br from-brand-50 to-gray-100 flex items-center justify-center h-screen w-screen overflow-hidden p-4">
|
||||
<div class="w-full h-full flex flex-col">
|
||||
<div class="bg-white/95 backdrop-blur-sm border border-white/30 rounded-xl shadow-xl flex-1 flex flex-col overflow-hidden">
|
||||
<div class="flex items-center justify-center gap-4 p-4 border-b border-gray-100">
|
||||
<i data-lucide="type" class="w-10 h-10 text-red-500"></i>
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-gray-800">Prática de Escrita</h2>
|
||||
<p class="text-lg text-gray-600">Digite palavras, números e frases que serão solicitadas.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 p-6 flex flex-col gap-5 justify-center items-center overflow-hidden">
|
||||
<div id="formWrap" class="w-full max-w-2xl flex flex-col gap-4">
|
||||
<div class="text-sm font-bold uppercase tracking-wide text-brand-700 text-center" id="stepLabel">Passo 1 de 10</div>
|
||||
<div class="bg-brand-50 border-2 border-brand-200 rounded-xl p-4 text-center">
|
||||
<p id="taskLabel" class="text-xl text-brand-900 font-semibold"></p>
|
||||
</div>
|
||||
<input id="textInput" type="text" autocomplete="off" spellcheck="false"
|
||||
class="text-2xl font-mono text-center border-b-4 border-brand-400 bg-transparent outline-none w-full py-2 text-gray-700 placeholder:text-gray-400"
|
||||
placeholder="Digite aqui" />
|
||||
<p id="feedback" class="text-base text-gray-500 text-center"></p>
|
||||
<button id="confirmBtn" class="mt-2 bg-emerald-600 hover:bg-emerald-700 disabled:bg-gray-300 disabled:text-gray-500 disabled:cursor-not-allowed text-white text-xl font-bold px-10 py-4 rounded-2xl shadow-lg transition-all active:scale-95 self-center">
|
||||
Avançar
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="successBanner" class="hidden bg-gradient-to-r from-brand-50 to-brand-100 border-2 border-brand-300 rounded-xl p-8 flex flex-col items-center justify-center gap-3 w-full max-w-xl text-center">
|
||||
<i data-lucide="check-circle" class="text-brand-600 w-16 h-16"></i>
|
||||
<h2 class="text-3xl font-bold text-brand-700">Muito bem!</h2>
|
||||
<p class="text-lg text-gray-700">Você completou 10 passos de escrita com foco em tecnologia e cidadania digital.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="./activity.js"></script>
|
||||
<script>lucide.createIcons();</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,110 @@
|
||||
const channelToken = new URLSearchParams(window.location.hash.slice(1)).get('channelToken');
|
||||
|
||||
function notify(type, payload = {}) {
|
||||
window.parent.postMessage({ type, token: channelToken, ...payload }, '*');
|
||||
}
|
||||
|
||||
notify('started');
|
||||
|
||||
const steps = [
|
||||
{
|
||||
text: 'Passo 1: Pressione → três vezes',
|
||||
hint: 'A seta para direita move o cursor uma letra por vez.',
|
||||
action: 'arrow-right',
|
||||
count: 3,
|
||||
},
|
||||
{
|
||||
text: 'Passo 2: Pressione ← duas vezes',
|
||||
hint: 'A seta para esquerda volta o cursor.',
|
||||
action: 'arrow-left',
|
||||
count: 2,
|
||||
},
|
||||
{
|
||||
text: 'Passo 3: Pressione Home',
|
||||
hint: 'Home vai para o INÍCIO da linha.',
|
||||
action: 'home',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
text: 'Passo 4: Pressione End',
|
||||
hint: 'End vai para o FINAL da linha.',
|
||||
action: 'end',
|
||||
count: 1,
|
||||
},
|
||||
];
|
||||
|
||||
let currentStep = 0;
|
||||
let actionCount = 0;
|
||||
let locked = false;
|
||||
|
||||
const textArea = document.getElementById('textArea');
|
||||
const stepText = document.getElementById('stepText');
|
||||
const stepHint = document.getElementById('stepHint');
|
||||
const feedbackEl = document.getElementById('feedback');
|
||||
const cursorPos = document.getElementById('cursorPos');
|
||||
const progressDots = document.getElementById('progressDots');
|
||||
const successBanner = document.getElementById('successBanner');
|
||||
const stepBox = document.getElementById('stepBox');
|
||||
|
||||
textArea.value = 'Olá mundo!';
|
||||
textArea.readOnly = false;
|
||||
textArea.focus();
|
||||
textArea.setSelectionRange(0, 0);
|
||||
|
||||
steps.forEach((_, i) => {
|
||||
const dot = document.createElement('div');
|
||||
dot.id = `dot-${i}`;
|
||||
dot.className = 'w-8 h-8 rounded-full border-2 border-gray-300 bg-gray-100 transition-all';
|
||||
progressDots.appendChild(dot);
|
||||
});
|
||||
|
||||
function showStep() {
|
||||
const s = steps[currentStep];
|
||||
stepText.textContent = s.text;
|
||||
stepHint.textContent = s.hint;
|
||||
actionCount = 0;
|
||||
feedbackEl.classList.add('hidden');
|
||||
textArea.focus();
|
||||
}
|
||||
showStep();
|
||||
|
||||
textArea.addEventListener('keydown', (e) => {
|
||||
if (locked) return;
|
||||
const s = steps[currentStep];
|
||||
const pos = textArea.selectionStart;
|
||||
cursorPos.textContent = `Posição do cursor: ${pos}`;
|
||||
|
||||
let matched = false;
|
||||
if (s.action === 'arrow-right' && e.key === 'ArrowRight') matched = true;
|
||||
if (s.action === 'arrow-left' && e.key === 'ArrowLeft') matched = true;
|
||||
if (s.action === 'home' && e.key === 'Home') matched = true;
|
||||
if (s.action === 'end' && e.key === 'End') matched = true;
|
||||
|
||||
if (matched) {
|
||||
actionCount++;
|
||||
feedbackEl.textContent = `${actionCount} de ${s.count}`;
|
||||
feedbackEl.className = 'text-2xl font-bold py-1 text-green-600';
|
||||
feedbackEl.classList.remove('hidden');
|
||||
|
||||
if (actionCount >= s.count) {
|
||||
locked = true;
|
||||
const dot = document.getElementById(`dot-${currentStep}`);
|
||||
if (dot) dot.className = 'w-8 h-8 rounded-full bg-green-400 border-2 border-green-500 transition-all';
|
||||
notify('running', { step: currentStep + 1 });
|
||||
currentStep++;
|
||||
setTimeout(() => {
|
||||
locked = false;
|
||||
if (currentStep >= steps.length) {
|
||||
stepBox.classList.add('hidden');
|
||||
progressDots.classList.add('hidden');
|
||||
successBanner.classList.remove('hidden');
|
||||
lucide.createIcons();
|
||||
notify('success', { score: 100 });
|
||||
notify('completed', { score: 100 });
|
||||
} else {
|
||||
showStep();
|
||||
}
|
||||
}, 700);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Setas de Navegação</title>
|
||||
<link rel="stylesheet" href="../../shared/letramento.css">
|
||||
<script src="../../shared/lucide.js"></script>
|
||||
<style>body { overflow: hidden; height: 100vh; width: 100vw; }</style>
|
||||
</head>
|
||||
<body class="bg-gradient-to-br from-brand-50 to-gray-100 flex items-center justify-center h-screen w-screen overflow-hidden p-4">
|
||||
<div class="w-full h-full flex flex-col">
|
||||
<div class="bg-white/95 backdrop-blur-sm border border-white/30 rounded-xl shadow-xl flex-1 flex flex-col overflow-hidden">
|
||||
<div class="flex items-center justify-center gap-4 p-4 border-b border-gray-100">
|
||||
<i data-lucide="move" class="w-10 h-10 text-red-500"></i>
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-gray-800">Setas: Mova o Cursor</h2>
|
||||
<p class="text-lg text-gray-600">Use as setas para mover o cursor dentro do texto.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 p-4 flex flex-col gap-4 justify-center items-center overflow-hidden">
|
||||
<div class="bg-teal-50 border-2 border-teal-200 rounded-2xl p-4 max-w-lg text-center">
|
||||
<p class="text-lg text-teal-800">
|
||||
← → move o cursor <strong>para os lados</strong>. ↑ ↓ movem linha acima/abaixo.
|
||||
<strong>Home</strong> vai para o início da linha. <strong>End</strong> vai para o final.
|
||||
</p>
|
||||
</div>
|
||||
<div id="stepBox" class="w-full max-w-lg text-center">
|
||||
<p class="text-xl font-bold text-gray-700" id="stepText"></p>
|
||||
<p class="text-lg text-gray-500 mt-1" id="stepHint"></p>
|
||||
<div class="mt-3 relative inline-block">
|
||||
<textarea id="textArea" rows="3" spellcheck="false" autocomplete="off"
|
||||
class="text-xl font-mono border-2 border-teal-400 rounded-xl p-3 w-80 resize-none outline-none bg-white shadow focus:border-teal-600"
|
||||
></textarea>
|
||||
<div id="cursorPos" class="text-sm text-gray-400 mt-1">Posição do cursor: 0</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="feedback" class="hidden text-2xl font-bold py-1"></div>
|
||||
<div class="flex gap-2 justify-center" id="progressDots"></div>
|
||||
<div id="successBanner" class="hidden bg-gradient-to-r from-green-50 to-emerald-50 border-2 border-green-300 rounded-xl p-6 flex items-center justify-center gap-4 w-full max-w-lg">
|
||||
<i data-lucide="check-circle" class="text-green-600 w-14 h-14"></i>
|
||||
<div>
|
||||
<h2 class="text-3xl font-bold text-green-700 mb-1">Muito bem!</h2>
|
||||
<p class="text-lg text-gray-700">Agora você sabe navegar no texto sem o mouse!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="./activity.js"></script>
|
||||
<script>lucide.createIcons();</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,207 @@
|
||||
const channelToken = new URLSearchParams(window.location.hash.slice(1)).get('channelToken');
|
||||
|
||||
function notify(type, payload = {}) {
|
||||
window.parent.postMessage({ type, token: channelToken, ...payload }, '*');
|
||||
}
|
||||
|
||||
const DIGITS = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
|
||||
const STEP_SIZES = [2, 4, 6, 8, 10];
|
||||
const TOTAL_STEPS = STEP_SIZES.length;
|
||||
|
||||
function shuffle(list) {
|
||||
const copy = [...list];
|
||||
for (let i = copy.length - 1; i > 0; i -= 1) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[copy[i], copy[j]] = [copy[j], copy[i]];
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
function init() {
|
||||
const queryEls = () => ({
|
||||
instruction: document.getElementById('instruction'),
|
||||
hint: document.getElementById('hint'),
|
||||
arena: document.getElementById('arena'),
|
||||
successBanner: document.getElementById('successBanner'),
|
||||
successMsg: document.getElementById('successMsg'),
|
||||
});
|
||||
|
||||
let { instruction, hint, arena, successBanner, successMsg } = queryEls();
|
||||
|
||||
const required = { instruction, hint, arena, successBanner, successMsg };
|
||||
const missing = Object.entries(required)
|
||||
.filter(([, value]) => !value)
|
||||
.map(([name]) => name);
|
||||
|
||||
if (missing.length > 0) {
|
||||
console.warn(`[teclado-numeros] Layout incompleto. Aplicando fallback para: ${missing.join(', ')}`);
|
||||
document.body.innerHTML = `
|
||||
<div class="min-h-screen w-full bg-gradient-to-br from-sky-50 via-white to-indigo-50 p-4 flex items-center justify-center">
|
||||
<div class="w-full max-w-5xl bg-white rounded-xl shadow-xl border border-slate-200 p-5 flex flex-col gap-4">
|
||||
<div>
|
||||
<h2 id="instruction" class="text-2xl font-bold text-slate-800">Números do Teclado</h2>
|
||||
<p id="hint" class="text-slate-600"></p>
|
||||
</div>
|
||||
<div id="arena"></div>
|
||||
<div id="successBanner" class="hidden rounded-xl border-2 border-emerald-300 bg-emerald-50 p-6 text-center">
|
||||
<h2 class="text-2xl font-bold text-emerald-700">Muito bem!</h2>
|
||||
<p id="successMsg" class="text-slate-700"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
({ instruction, hint, arena, successBanner, successMsg } = queryEls());
|
||||
}
|
||||
|
||||
notify('started');
|
||||
|
||||
let step = 1;
|
||||
let sequence = [];
|
||||
let index = 0;
|
||||
let done = false;
|
||||
|
||||
function generateSequence(size) {
|
||||
if (size <= DIGITS.length) {
|
||||
return shuffle(DIGITS).slice(0, size);
|
||||
}
|
||||
|
||||
const base = shuffle(DIGITS);
|
||||
const extra = DIGITS[Math.floor(Math.random() * DIGITS.length)];
|
||||
return [...base, extra];
|
||||
}
|
||||
|
||||
function markSequence(indexToMark, state) {
|
||||
const chip = document.getElementById(`seq-chip-${indexToMark}`);
|
||||
if (!chip) return;
|
||||
|
||||
if (state === 'done') {
|
||||
chip.className = 'h-11 w-11 rounded-lg border-2 border-emerald-300 bg-emerald-100 text-emerald-700 font-black text-xl flex items-center justify-center transition-all';
|
||||
return;
|
||||
}
|
||||
|
||||
if (state === 'error') {
|
||||
chip.className = 'h-11 w-11 rounded-lg border-2 border-rose-300 bg-rose-100 text-rose-700 font-black text-xl flex items-center justify-center transition-all';
|
||||
window.setTimeout(() => {
|
||||
if (index === indexToMark) {
|
||||
chip.className = 'h-11 w-11 rounded-lg border-2 border-brand-300 bg-brand-100 text-brand-700 font-black text-xl flex items-center justify-center transition-all';
|
||||
}
|
||||
}, 220);
|
||||
return;
|
||||
}
|
||||
|
||||
chip.className = 'h-11 w-11 rounded-lg border-2 border-brand-300 bg-brand-100 text-brand-700 font-black text-xl flex items-center justify-center transition-all';
|
||||
}
|
||||
|
||||
function setFeedback(ok, typed, expected) {
|
||||
const status = document.getElementById('statusFeedback');
|
||||
if (!status) return;
|
||||
|
||||
if (ok) {
|
||||
status.className = 'mt-4 rounded-xl border border-emerald-300 bg-emerald-50 px-4 py-3 text-emerald-700 font-semibold';
|
||||
status.textContent = `Correto: ${typed}`;
|
||||
return;
|
||||
}
|
||||
|
||||
status.className = 'mt-4 rounded-xl border border-rose-300 bg-rose-50 px-4 py-3 text-rose-700 font-semibold';
|
||||
status.textContent = `Ops: você digitou ${typed}. Próximo esperado: ${expected}`;
|
||||
}
|
||||
|
||||
function renderSequenceChips() {
|
||||
const container = document.getElementById('sequenceChips');
|
||||
if (!container) return;
|
||||
|
||||
container.innerHTML = '';
|
||||
sequence.forEach((digit, i) => {
|
||||
const chip = document.createElement('div');
|
||||
chip.id = `seq-chip-${i}`;
|
||||
chip.className = 'h-11 w-11 rounded-lg border-2 border-slate-300 bg-white text-slate-700 font-black text-xl flex items-center justify-center transition-all';
|
||||
chip.textContent = digit;
|
||||
container.appendChild(chip);
|
||||
});
|
||||
|
||||
markSequence(0, 'current');
|
||||
}
|
||||
|
||||
function renderStep() {
|
||||
sequence = generateSequence(STEP_SIZES[step - 1]);
|
||||
index = 0;
|
||||
hint.textContent = `Digite ${sequence.length} número(s) na ordem mostrada.`;
|
||||
|
||||
arena.innerHTML = `
|
||||
<div class="mx-auto w-full max-w-5xl rounded-2xl bg-brand-50 border-2 border-brand-200 p-6 text-center">
|
||||
<p class="text-base text-slate-700 font-semibold">Sequência da vez</p>
|
||||
<div class="mt-3 overflow-x-auto pb-1">
|
||||
<div id="sequenceChips" class="mx-auto flex w-fit min-w-full flex-nowrap items-center justify-center gap-2"></div>
|
||||
</div>
|
||||
<p class="mt-4 text-sm text-slate-500">Acertos na rodada: <span id="seqState">0</span>/${sequence.length}</p>
|
||||
<p id="statusFeedback" class="mt-4 rounded-xl border border-slate-200 bg-white px-4 py-3 text-slate-600 font-medium">Digite o primeiro número destacado.</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
renderSequenceChips();
|
||||
}
|
||||
|
||||
function flashKey(digit, ok) {
|
||||
const el = document.getElementById(`kbd-${digit}`);
|
||||
if (!el) return;
|
||||
el.className = ok
|
||||
? 'w-12 h-12 rounded-lg border-2 border-emerald-300 bg-emerald-100 flex items-center justify-center font-bold text-emerald-700'
|
||||
: 'w-12 h-12 rounded-lg border-2 border-rose-300 bg-rose-100 flex items-center justify-center font-bold text-rose-700';
|
||||
setTimeout(() => {
|
||||
el.className = 'w-12 h-12 rounded-lg border-2 border-slate-300 bg-white flex items-center justify-center font-bold text-slate-700';
|
||||
}, 180);
|
||||
}
|
||||
|
||||
function handleDigit(digit) {
|
||||
if (done) return;
|
||||
const expected = sequence[index];
|
||||
const ok = digit === expected;
|
||||
flashKey(digit, ok);
|
||||
if (!ok) {
|
||||
markSequence(index, 'error');
|
||||
setFeedback(false, digit, expected);
|
||||
return;
|
||||
}
|
||||
|
||||
markSequence(index, 'done');
|
||||
setFeedback(true, digit, expected);
|
||||
index += 1;
|
||||
const seqState = document.getElementById('seqState');
|
||||
if (seqState) seqState.textContent = String(index);
|
||||
|
||||
if (index < sequence.length) {
|
||||
markSequence(index, 'current');
|
||||
}
|
||||
|
||||
if (index >= sequence.length) {
|
||||
notify('running', { step });
|
||||
if (step >= TOTAL_STEPS) {
|
||||
done = true;
|
||||
arena.classList.add('hidden');
|
||||
successBanner.classList.remove('hidden');
|
||||
successMsg.textContent = 'Você agora sabe onde estão os números no teclado!';
|
||||
lucide.createIcons();
|
||||
notify('success', { score: 100 });
|
||||
notify('completed', { score: 100 });
|
||||
return;
|
||||
}
|
||||
|
||||
step += 1;
|
||||
renderStep();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (!/^Digit[0-9]$/.test(event.code)) return;
|
||||
handleDigit(event.code.replace('Digit', ''));
|
||||
});
|
||||
|
||||
renderStep();
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init, { once: true });
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Números do Teclado</title>
|
||||
<link rel="stylesheet" href="../../shared/letramento.css">
|
||||
<script src="../../shared/lucide.js"></script>
|
||||
<style>
|
||||
body { overflow: hidden; height: 100vh; width: 100vw; }
|
||||
.key { transition: all 0.15s; cursor: pointer; }
|
||||
.key:hover { filter: brightness(1.1); transform: scale(1.05); }
|
||||
.key.active { transform: scale(0.95); filter: brightness(0.85); }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gradient-to-br from-brand-50 to-gray-100 flex items-center justify-center h-screen w-screen overflow-hidden p-4">
|
||||
<div class="w-full h-full flex flex-col">
|
||||
<div class="bg-white/95 backdrop-blur-sm border border-white/30 rounded-xl shadow-xl flex-1 flex flex-col overflow-hidden">
|
||||
<div class="flex items-center justify-center gap-4 p-4 border-b border-gray-100">
|
||||
<i data-lucide="calculator" class="w-10 h-10 text-red-500"></i>
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-gray-800" id="instruction">Números do Teclado</h2>
|
||||
<p class="text-lg text-gray-600" id="hint"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 p-4 flex flex-col gap-4 overflow-hidden">
|
||||
<div id="arena" ></div>
|
||||
<div id="successBanner" class="hidden rounded-xl border-2 border-emerald-300 bg-gradient-to-r from-emerald-50 to-green-50 p-8 text-center">
|
||||
<div class="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-emerald-100 text-emerald-700"><i data-lucide="check-circle-2" class="h-10 w-10"></i></div>
|
||||
<h2 id="successTitle" class="mb-2 text-3xl font-bold text-emerald-700">Muito bem!</h2>
|
||||
<p id="successMsg" class="text-lg text-slate-700"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="./activity.js"></script>
|
||||
<script>lucide.createIcons();</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,150 @@
|
||||
const channelToken = new URLSearchParams(window.location.hash.slice(1)).get('channelToken');
|
||||
|
||||
function notify(type, payload = {}) {
|
||||
window.parent.postMessage({ type, token: channelToken, ...payload }, '*');
|
||||
}
|
||||
|
||||
const regions = [
|
||||
{
|
||||
id: 'letras',
|
||||
label: 'Letras (A–Z)',
|
||||
color: 'bg-blue-400',
|
||||
textColor: 'text-white',
|
||||
title: 'Região das Letras',
|
||||
desc: 'Aqui ficam todas as 26 letras do alfabeto. São as teclas mais usadas para escrever palavras.',
|
||||
keys: ['Q','W','E','R','T','Y','U','I','O','P','A','S','D','F','G','H','J','K','L','Z','X','C','V','B','N','M'],
|
||||
},
|
||||
{
|
||||
id: 'numeros',
|
||||
label: 'Números (0–9)',
|
||||
color: 'bg-green-400',
|
||||
textColor: 'text-white',
|
||||
title: 'Região dos Números',
|
||||
desc: 'A linha superior tem os números de 1 a 0. Também há símbolos quando você usa o Shift.',
|
||||
keys: ['1','2','3','4','5','6','7','8','9','0'],
|
||||
},
|
||||
{
|
||||
id: 'especiais',
|
||||
label: 'Teclas Especiais',
|
||||
color: 'bg-orange-400',
|
||||
textColor: 'text-white',
|
||||
title: 'Teclas Especiais',
|
||||
desc: 'Enter, Backspace, Shift, Caps Lock, Esc, Tab — cada uma tem uma função importante!',
|
||||
keys: ['Enter','Backspace','Shift','Caps Lock','Esc','Tab'],
|
||||
},
|
||||
{
|
||||
id: 'setas',
|
||||
label: 'Setas de Navegação',
|
||||
color: 'bg-purple-400',
|
||||
textColor: 'text-white',
|
||||
title: 'Setas de Navegação',
|
||||
desc: 'As quatro setas movem o cursor no texto sem apagar nada. Home e End vão para início/fim da linha.',
|
||||
keys: ['←','↑','↓','→','Home','End'],
|
||||
},
|
||||
];
|
||||
|
||||
const visitedRegions = new Set();
|
||||
notify('started');
|
||||
|
||||
const keyboard = document.getElementById('keyboard');
|
||||
const infoBox = document.getElementById('infoBox');
|
||||
const infoTitle = document.getElementById('infoTitle');
|
||||
const infoDesc = document.getElementById('infoDesc');
|
||||
const progressArea = document.getElementById('progressArea');
|
||||
const instruction = document.getElementById('instruction');
|
||||
const hint = document.getElementById('hint');
|
||||
const successBanner = document.getElementById('successBanner');
|
||||
let isCompleted = false;
|
||||
|
||||
// Build visual keyboard rows
|
||||
const rows = [
|
||||
['Esc','1','2','3','4','5','6','7','8','9','0','Backspace'],
|
||||
['Tab','Q','W','E','R','T','Y','U','I','O','P'],
|
||||
['Caps Lock','A','S','D','F','G','H','J','K','L','Enter'],
|
||||
['Shift','Z','X','C','V','B','N','M','Shift'],
|
||||
['←','↑','↓','→','Home','End'],
|
||||
];
|
||||
|
||||
rows.forEach(row => {
|
||||
const rowEl = document.createElement('div');
|
||||
rowEl.className = 'flex gap-1 justify-center';
|
||||
row.forEach(key => {
|
||||
const region = regions.find(r => r.keys.includes(key));
|
||||
const keyEl = document.createElement('div');
|
||||
const isWide = ['Backspace','Tab','Caps Lock','Enter','Shift'].includes(key);
|
||||
keyEl.className = `key px-2 py-2 rounded-md text-sm font-bold shadow flex items-center justify-center min-w-[2.2rem] ${isWide ? 'px-3 min-w-[4rem]' : ''} ${region ? region.color + ' ' + region.textColor : 'bg-gray-200 text-gray-700'}`;
|
||||
keyEl.textContent = key;
|
||||
if (region) {
|
||||
keyEl.addEventListener('click', () => handleRegionClick(region));
|
||||
}
|
||||
rowEl.appendChild(keyEl);
|
||||
});
|
||||
keyboard.appendChild(rowEl);
|
||||
});
|
||||
|
||||
function normalizeKey(event) {
|
||||
const key = event.key;
|
||||
|
||||
if (key.length === 1) {
|
||||
return key.toUpperCase();
|
||||
}
|
||||
|
||||
const aliases = {
|
||||
ArrowLeft: '←',
|
||||
ArrowUp: '↑',
|
||||
ArrowDown: '↓',
|
||||
ArrowRight: '→',
|
||||
Escape: 'Esc',
|
||||
CapsLock: 'Caps Lock',
|
||||
' ': 'Space',
|
||||
};
|
||||
|
||||
return aliases[key] ?? key;
|
||||
}
|
||||
|
||||
function findRegionByKey(keyLabel) {
|
||||
return regions.find((region) => region.keys.includes(keyLabel));
|
||||
}
|
||||
|
||||
function handleRegionClick(region) {
|
||||
if (isCompleted) return;
|
||||
|
||||
// Highlight all keys of this region
|
||||
document.querySelectorAll('.key').forEach(k => {
|
||||
k.classList.remove('ring-4','ring-yellow-400');
|
||||
if (region.keys.includes(k.textContent.trim())) {
|
||||
k.classList.add('ring-4','ring-yellow-400');
|
||||
}
|
||||
});
|
||||
|
||||
infoTitle.textContent = region.title;
|
||||
infoDesc.textContent = region.desc;
|
||||
infoBox.classList.remove('hidden');
|
||||
|
||||
if (!visitedRegions.has(region.id)) {
|
||||
visitedRegions.add(region.id);
|
||||
notify('running', { step: visitedRegions.size });
|
||||
}
|
||||
|
||||
if (visitedRegions.size >= regions.length) {
|
||||
isCompleted = true;
|
||||
setTimeout(() => {
|
||||
keyboard.classList.add('hidden');
|
||||
infoBox.classList.add('hidden');
|
||||
successBanner.classList.remove('hidden');
|
||||
lucide.createIcons();
|
||||
notify('success', { score: 100 });
|
||||
notify('completed', { score: 100 });
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (isCompleted) return;
|
||||
|
||||
const normalizedKey = normalizeKey(event);
|
||||
const region = findRegionByKey(normalizedKey);
|
||||
if (!region) return;
|
||||
|
||||
handleRegionClick(region);
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Regiões do Teclado</title>
|
||||
<link rel="stylesheet" href="../../shared/letramento.css">
|
||||
<script src="../../shared/lucide.js"></script>
|
||||
<style>
|
||||
body { overflow: hidden; height: 100vh; width: 100vw; }
|
||||
.key { transition: all 0.15s; cursor: pointer; }
|
||||
.key:hover { filter: brightness(1.1); transform: scale(1.05); }
|
||||
.key.active { transform: scale(0.95); filter: brightness(0.85); }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gradient-to-br from-brand-50 to-gray-100 flex items-center justify-center h-screen w-screen overflow-hidden p-4">
|
||||
<div class="w-full h-full flex flex-col">
|
||||
<div class="bg-white/95 backdrop-blur-sm border border-white/30 rounded-xl shadow-xl flex-1 flex flex-col overflow-hidden">
|
||||
<div class="flex items-center justify-center gap-4 p-4 border-b border-gray-100">
|
||||
<i data-lucide="keyboard" class="w-10 h-10 text-red-500"></i>
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-gray-800" id="instruction">Conheça as regiões do teclado</h2>
|
||||
<p class="text-lg text-gray-600" id="hint">Clique em uma região colorida ou pressione qualquer tecla dela.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 p-4 flex flex-col gap-4 overflow-hidden">
|
||||
<div id="infoBox" class="hidden bg-indigo-50 border-2 border-indigo-300 rounded-xl p-4 text-center">
|
||||
<p class="text-xl font-bold text-indigo-700" id="infoTitle"></p>
|
||||
<p class="text-lg text-gray-700 mt-1" id="infoDesc"></p>
|
||||
</div>
|
||||
<div id="keyboard" class="flex-1 flex flex-col justify-center gap-2 select-none"></div>
|
||||
<div id="progressArea" class="flex gap-3 justify-center flex-wrap"></div>
|
||||
<div id="successBanner" class="hidden bg-gradient-to-r from-green-50 to-emerald-50 border-2 border-green-300 rounded-xl p-6 flex items-center justify-center gap-4">
|
||||
<i data-lucide="check-circle" class="text-green-600 w-14 h-14"></i>
|
||||
<div>
|
||||
<h2 class="text-3xl font-bold text-green-700 mb-1">Muito bem!</h2>
|
||||
<p class="text-lg text-gray-700">Você conheceu todas as regiões do teclado!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="./activity.js"></script>
|
||||
<script>lucide.createIcons();</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,74 @@
|
||||
const channelToken = new URLSearchParams(window.location.hash.slice(1)).get('channelToken');
|
||||
|
||||
function notify(type, payload = {}) {
|
||||
window.parent.postMessage({ type, token: channelToken, ...payload }, '*');
|
||||
}
|
||||
|
||||
// Map: symbol → key hint
|
||||
const sequence = [
|
||||
{ symbol: '!', hint: 'Shift + 1', key: '!' },
|
||||
{ symbol: '@', hint: 'Shift + 2', key: '@' },
|
||||
{ symbol: '#', hint: 'Shift + 3', key: '#' },
|
||||
{ symbol: '$', hint: 'Shift + 4', key: '$' },
|
||||
{ symbol: '%', hint: 'Shift + 5', key: '%' },
|
||||
{ symbol: '&', hint: 'Shift + 7', key: '&' },
|
||||
{ symbol: '*', hint: 'Shift + 8', key: '*' },
|
||||
{ symbol: '(', hint: 'Shift + 9', key: '(' },
|
||||
];
|
||||
|
||||
let currentIdx = 0;
|
||||
let locked = false;
|
||||
notify('started');
|
||||
|
||||
const targetEl = document.getElementById('target');
|
||||
const targetHintEl = document.getElementById('targetHint');
|
||||
const feedbackEl = document.getElementById('feedback');
|
||||
const progressDots = document.getElementById('progressDots');
|
||||
const successBanner = document.getElementById('successBanner');
|
||||
|
||||
sequence.forEach((_, i) => {
|
||||
const dot = document.createElement('div');
|
||||
dot.id = `dot-${i}`;
|
||||
dot.className = 'w-8 h-8 rounded-full border-2 border-gray-300 bg-gray-100 transition-all';
|
||||
progressDots.appendChild(dot);
|
||||
});
|
||||
|
||||
function showTarget() {
|
||||
const s = sequence[currentIdx];
|
||||
targetEl.textContent = s.symbol;
|
||||
targetHintEl.textContent = `Pressione: ${s.hint}`;
|
||||
feedbackEl.classList.add('hidden');
|
||||
}
|
||||
showTarget();
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (locked) return;
|
||||
const s = sequence[currentIdx];
|
||||
if (e.key === s.key) {
|
||||
locked = true;
|
||||
feedbackEl.textContent = 'Isso!';
|
||||
feedbackEl.className = 'text-2xl font-bold py-1 text-green-600';
|
||||
feedbackEl.classList.remove('hidden');
|
||||
const dot = document.getElementById(`dot-${currentIdx}`);
|
||||
if (dot) dot.className = 'w-8 h-8 rounded-full bg-green-400 border-2 border-green-500 transition-all';
|
||||
notify('running', { step: currentIdx + 1 });
|
||||
currentIdx++;
|
||||
setTimeout(() => {
|
||||
locked = false;
|
||||
if (currentIdx >= sequence.length) {
|
||||
targetEl.parentElement.classList.add('hidden');
|
||||
progressDots.classList.add('hidden');
|
||||
successBanner.classList.remove('hidden');
|
||||
lucide.createIcons();
|
||||
notify('success', { score: 100 });
|
||||
notify('completed', { score: 100 });
|
||||
} else {
|
||||
showTarget();
|
||||
}
|
||||
}, 800);
|
||||
} else if (e.key !== 'Shift') {
|
||||
feedbackEl.textContent = `Não é esse. Tente: ${s.hint}`;
|
||||
feedbackEl.className = 'text-2xl font-bold py-1 text-red-500';
|
||||
feedbackEl.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Símbolos com Shift</title>
|
||||
<link rel="stylesheet" href="../../shared/letramento.css">
|
||||
<script src="../../shared/lucide.js"></script>
|
||||
<style>body { overflow: hidden; height: 100vh; width: 100vw; }</style>
|
||||
</head>
|
||||
<body class="bg-gradient-to-br from-brand-50 to-gray-100 flex items-center justify-center h-screen w-screen overflow-hidden p-4">
|
||||
<div class="w-full h-full flex flex-col">
|
||||
<div class="bg-white/95 backdrop-blur-sm border border-white/30 rounded-xl shadow-xl flex-1 flex flex-col overflow-hidden">
|
||||
<div class="flex items-center justify-center gap-4 p-4 border-b border-gray-100">
|
||||
<i data-lucide="at-sign" class="w-10 h-10 text-red-500"></i>
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-gray-800">Símbolos com Shift + Número</h2>
|
||||
<p class="text-lg text-gray-600">Segure Shift e pressione o número para obter o símbolo.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 p-4 flex flex-col gap-4 justify-center items-center overflow-hidden">
|
||||
<div class="bg-purple-50 border-2 border-purple-200 rounded-2xl p-4 max-w-lg text-center">
|
||||
<p class="text-lg text-purple-800">
|
||||
Cada tecla de número tem um <strong>símbolo escondido</strong> em cima dela. Para acessá-lo, segure <kbd class="bg-gray-200 px-2 py-0.5 rounded font-mono font-bold">Shift</kbd> e pressione o número!
|
||||
</p>
|
||||
</div>
|
||||
<div id="target" class="text-8xl font-black text-purple-600 bg-purple-50 rounded-2xl w-44 h-44 flex items-center justify-center border-4 border-purple-300 shadow-lg"></div>
|
||||
<p class="text-xl text-gray-500" id="targetHint"></p>
|
||||
<div id="feedback" class="hidden text-2xl font-bold py-1"></div>
|
||||
<div class="flex gap-2 justify-center" id="progressDots"></div>
|
||||
<div id="successBanner" class="hidden bg-gradient-to-r from-green-50 to-emerald-50 border-2 border-green-300 rounded-xl p-6 flex items-center justify-center gap-4 w-full max-w-lg">
|
||||
<i data-lucide="check-circle" class="text-green-600 w-14 h-14"></i>
|
||||
<div>
|
||||
<h2 class="text-3xl font-bold text-green-700 mb-1">Incrível!</h2>
|
||||
<p class="text-lg text-gray-700">Agora você sabe como digitar símbolos!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="./activity.js"></script>
|
||||
<script>lucide.createIcons();</script>
|
||||
</body>
|
||||
</html>
|
||||
182
app/src/atividades/letramento/teclado/tecladoRegistry.js
Normal file
@@ -0,0 +1,182 @@
|
||||
export const TECLADO_ATIVIDADES_REGISTRY = {
|
||||
'teclado-regioes': {
|
||||
id: 'teclado-regioes',
|
||||
titulo: 'Regiões do Teclado',
|
||||
descricao: 'Aprenda onde ficam as letras e os grupos principais do teclado.',
|
||||
categoria: 'teclado',
|
||||
dificuldade: 'iniciante',
|
||||
duracao: 3,
|
||||
htmlFile: '/atividades/letramento/teclado/teclado-regioes/index.html',
|
||||
proxima: 'teclado-numeros',
|
||||
passos: [
|
||||
{ id: 1, label: 'Letras' },
|
||||
{ id: 2, label: 'Números' },
|
||||
{ id: 3, label: 'Teclas especiais' },
|
||||
{ id: 4, label: 'Setas, Home e End' },
|
||||
],
|
||||
},
|
||||
'teclado-numeros': {
|
||||
id: 'teclado-numeros',
|
||||
titulo: 'Números do Teclado',
|
||||
descricao: 'Pratique sequências numéricas progressivas, começando com 2 e chegando a 10 números.',
|
||||
categoria: 'teclado',
|
||||
dificuldade: 'iniciante',
|
||||
duracao: 5,
|
||||
htmlFile: '/atividades/letramento/teclado/teclado-numeros/index.html',
|
||||
proxima: 'teclado-escrita',
|
||||
passos: [
|
||||
{ id: 1, label: '2 números' },
|
||||
{ id: 3, label: '4 números' },
|
||||
{ id: 5, label: '6 números' },
|
||||
{ id: 7, label: '8 números' },
|
||||
{ id: 9, label: '10 números' },
|
||||
],
|
||||
},
|
||||
'teclado-escrita': {
|
||||
id: 'teclado-escrita',
|
||||
titulo: 'Prática de Escrita',
|
||||
descricao: 'Digite palavras e frases sobre tecnologia, cidadania e soberania digital.',
|
||||
categoria: 'teclado',
|
||||
dificuldade: 'iniciante',
|
||||
duracao: 5,
|
||||
htmlFile: '/atividades/letramento/teclado/teclado-escrever-nome/index.html',
|
||||
proxima: 'teclado-simbolos',
|
||||
passos: [
|
||||
{ id: 1, label: 'Palavra 1' },
|
||||
{ id: 3, label: 'Número' },
|
||||
{ id: 4, label: 'Frase 1' },
|
||||
{ id: 5, label: 'Frase 2' },
|
||||
{ id: 6, label: 'Frase 3' },
|
||||
{ id: 7, label: 'Frase 4' },
|
||||
],
|
||||
},
|
||||
'teclado-simbolos': {
|
||||
id: 'teclado-simbolos',
|
||||
titulo: 'Símbolos com Shift',
|
||||
descricao: 'Use Shift para formar símbolos comuns do teclado internacional.',
|
||||
categoria: 'teclado',
|
||||
dificuldade: 'iniciante',
|
||||
duracao: 3,
|
||||
htmlFile: '/atividades/letramento/teclado/teclado-simbolos/index.html',
|
||||
proxima: 'teclado-backspace',
|
||||
passos: [
|
||||
{ id: 1, label: 'Shift' },
|
||||
{ id: 2, label: 'Símbolos' },
|
||||
],
|
||||
},
|
||||
'teclado-backspace': {
|
||||
id: 'teclado-backspace',
|
||||
titulo: 'Backspace e Correção',
|
||||
descricao: 'Corrija frases sobre educação popular, tecnologia e soberania digital.',
|
||||
categoria: 'teclado',
|
||||
dificuldade: 'intermediario',
|
||||
duracao: 4,
|
||||
htmlFile: '/atividades/letramento/teclado/teclado-backspace/index.html',
|
||||
proxima: 'teclado-labirinto',
|
||||
passos: [
|
||||
{ id: 1, label: 'Correções simples' },
|
||||
{ id: 2, label: 'Correções no meio' },
|
||||
{ id: 3, label: 'Reescrita' },
|
||||
{ id: 4, label: 'Frases completas' },
|
||||
{ id: 5, label: 'Frases longas' },
|
||||
],
|
||||
},
|
||||
'teclado-labirinto': {
|
||||
id: 'teclado-labirinto',
|
||||
titulo: 'Labirinto das Setas',
|
||||
descricao: 'Pratique navegação com setas para ganhar fluidez no teclado.',
|
||||
categoria: 'teclado',
|
||||
dificuldade: 'intermediario',
|
||||
duracao: 3,
|
||||
htmlFile: '/atividades/letramento/teclado/labirinto/index.html',
|
||||
proxima: 'teclado-navegacao',
|
||||
passos: [
|
||||
{ id: 1, label: 'Fase 1' },
|
||||
{ id: 2, label: 'Fase 2' },
|
||||
{ id: 3, label: 'Fase 3' },
|
||||
{ id: 4, label: 'Fase 4' },
|
||||
{ id: 5, label: 'Fase 5' },
|
||||
],
|
||||
},
|
||||
'teclado-navegacao': {
|
||||
id: 'teclado-navegacao',
|
||||
titulo: 'Teclas de Navegação',
|
||||
descricao: 'Use setas, Home e End para revisar textos.',
|
||||
categoria: 'teclado',
|
||||
dificuldade: 'intermediario',
|
||||
duracao: 3,
|
||||
htmlFile: '/atividades/letramento/teclado/teclado-navegacao/index.html',
|
||||
proxima: 'teclado-setas-texto',
|
||||
passos: [
|
||||
{ id: 1, label: 'Setas' },
|
||||
{ id: 2, label: 'Home e End' },
|
||||
],
|
||||
},
|
||||
'teclado-setas-texto': {
|
||||
id: 'teclado-setas-texto',
|
||||
titulo: 'Setas no Texto',
|
||||
descricao: 'Edite frases sobre tecnologia e soberania digital sem apagar o conteúdo.',
|
||||
categoria: 'teclado',
|
||||
dificuldade: 'intermediario',
|
||||
duracao: 3,
|
||||
htmlFile: '/atividades/letramento/teclado/setas-texto/index.html',
|
||||
proxima: 'teclado-chuva',
|
||||
passos: [
|
||||
{ id: 1, label: 'Mover cursor' },
|
||||
{ id: 2, label: 'Inserir no lugar certo' },
|
||||
],
|
||||
},
|
||||
// 'teclado-recado': {
|
||||
// id: 'teclado-recado',
|
||||
// titulo: 'Texto Coletivo',
|
||||
// descricao: 'Escreva uma mensagem curta sobre tecnologia, comunidade e soberania digital.',
|
||||
// categoria: 'teclado',
|
||||
// dificuldade: 'intermediario',
|
||||
// duracao: 4,
|
||||
// htmlFile: '/atividades/letramento/teclado/recado-completo/index.html',
|
||||
// proxima: 'teclado-chuva',
|
||||
// passos: [
|
||||
// { id: 1, label: 'Escrever' },
|
||||
// { id: 2, label: 'Revisar' },
|
||||
// { id: 3, label: 'Concluir' },
|
||||
// ],
|
||||
// },
|
||||
'teclado-chuva': {
|
||||
id: 'teclado-chuva',
|
||||
titulo: 'Chuva de Letras',
|
||||
descricao: 'Digite caracteres, palavras e combinações em etapas com velocidade progressiva.',
|
||||
categoria: 'teclado',
|
||||
dificuldade: 'intermediario',
|
||||
duracao: 5,
|
||||
htmlFile: '/atividades/letramento/teclado/chuva/index.html',
|
||||
proxima: 'teclado-atividade-final',
|
||||
passos: [
|
||||
{ id: 1, label: '5 caracteres' },
|
||||
{ id: 2, label: '5 caracteres' },
|
||||
{ id: 3, label: '5 caracteres' },
|
||||
{ id: 4, label: '2 caracteres' },
|
||||
{ id: 5, label: '3 letras' },
|
||||
{ id: 6, label: '4 letras' },
|
||||
],
|
||||
},
|
||||
'teclado-atividade-final': {
|
||||
id: 'teclado-atividade-final',
|
||||
titulo: 'Atividade Final',
|
||||
descricao: 'Integre as habilidades de digitação em uma sequência completa de teclado.',
|
||||
categoria: 'teclado',
|
||||
dificuldade: 'avancado',
|
||||
duracao: 5,
|
||||
htmlFile: '/atividades/letramento/teclado/atividade-final/index.html',
|
||||
proxima: null,
|
||||
passos: [
|
||||
{ id: 1, label: 'Letra' },
|
||||
{ id: 2, label: 'Número' },
|
||||
{ id: 3, label: 'Maiúscula' },
|
||||
{ id: 4, label: 'Símbolo' },
|
||||
{ id: 5, label: 'Seta' },
|
||||
{ id: 6, label: 'Enter' },
|
||||
{ id: 7, label: 'Esc' },
|
||||
{ id: 8, label: 'Final' },
|
||||
],
|
||||
},
|
||||
};
|
||||
74
app/src/atividades/programacao/aspirador/AspiradorGame.jsx
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* @fileoverview Componente React principal para o jogo Aspirador
|
||||
* * @module games.aspirador.AspiradorGame
|
||||
*/
|
||||
|
||||
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";
|
||||
import { useAspiradorTour } from "./hooks/useAspiradorTour";
|
||||
import { debugSolutions } from "./config/debugSolutions";
|
||||
import "shepherd.js/dist/css/shepherd.css";
|
||||
import "../../../styles/shepherd-theme.css";
|
||||
import { starterBlocks } from "./config/starterBlocks";
|
||||
|
||||
/**
|
||||
* Componente interno que monta a cena e o editor do jogo Aspirador.
|
||||
* Registra blocos, configura toolbox dinâmico e injeta o `gameFactory`.
|
||||
* @returns {JSX.Element} Conteúdo do jogo (editor + canvas)
|
||||
*/
|
||||
function AspiradorContent() {
|
||||
const { setFailureMessage, isDebugMode } = useGameState();
|
||||
|
||||
// Hook para o tutorial passo a passo (será criado depois)
|
||||
useAspiradorTour();
|
||||
|
||||
// Registra os blocos customizados do Blockly ao montar o componente
|
||||
useEffect(() => {
|
||||
registerBlocks();
|
||||
}, []);
|
||||
|
||||
// Memoriza a função geradora do toolbox para evitar re-renderizações desnecessárias
|
||||
const toolboxGenerator = useMemo(() => {
|
||||
return (allowedBlocks) => generateDynamicToolbox(allowedBlocks);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<GameBase
|
||||
gameFactory={createGame}
|
||||
gameConfig={gameConfig}
|
||||
customFailureHandler={setFailureMessage}
|
||||
failureHandler={setFailureMessage}
|
||||
>
|
||||
<GameEditor>
|
||||
<BlocklyEditor
|
||||
toolboxGenerator={toolboxGenerator}
|
||||
debugSolutions={isDebugMode ? debugSolutions : null}
|
||||
starterBlocks={starterBlocks}
|
||||
/>
|
||||
</GameEditor>
|
||||
</GameBase>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Componente de página que fornece o contexto de estado do jogo Aspirador.
|
||||
* Envolve `AspiradorContent` com o `GameStateProvider` configurado.
|
||||
* @returns {JSX.Element} Página completa do jogo Aspirador
|
||||
*/
|
||||
export default function AspiradorGame() {
|
||||
return (
|
||||
<GameStateProvider gameConfig={gameConfig}>
|
||||
<AspiradorContent />
|
||||
</GameStateProvider>
|
||||
);
|
||||
}
|
||||
|
||||
AspiradorContent.propTypes = {};
|
||||
AspiradorGame.propTypes = {};
|
||||
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 32 KiB |
BIN
app/src/atividades/programacao/aspirador/assets/image/piso.png
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
app/src/atividades/programacao/aspirador/assets/image/piso_2.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 58 KiB |
BIN
app/src/atividades/programacao/aspirador/assets/sound/pop.mp3
Normal file
170
app/src/atividades/programacao/aspirador/blocks/blocks.js
Normal file
@@ -0,0 +1,170 @@
|
||||
/**
|
||||
* @fileoverview Definição de blocos customizados e geradores para o jogo Aspirador
|
||||
* @module games.aspirador.blocks.blocks
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
import * as Blockly from "blockly/core";
|
||||
import "blockly/blocks";
|
||||
import { javascriptGenerator } from "blockly/javascript";
|
||||
import { CORES_BLOCKLY, CORES_CUSTOMIZADAS } from "@/blockly/blocklyColors";
|
||||
import { configurarGerador, gerarExpressao,gerarStatement, gerarStatementInline, gerarStatementComCampo, gerarExpressaoComCampo, gerarStatementComValor } from "@/blockly/generator";
|
||||
import { gerarToolboxDeEstrutura } from "@/blockly/toolbox";
|
||||
import { criarBlocoStatementSimples, criarBlocoStatementComDropdown, criarBlocoStatementComValor, criarBlocoExpressaoSimples, criarBlocoExpressaoComDropdown, criarBlocoCondicional, criarBlocoNegacao } from "@/blockly/blockFactory";
|
||||
|
||||
const ESTRUTURA_TOOLBOX = [
|
||||
{
|
||||
nome: "Lógica",
|
||||
cssContainer: "cat_logica",
|
||||
blocos: ["robo_if", "robo_if_else", "robo_not"]
|
||||
},
|
||||
{
|
||||
nome: "Repetição",
|
||||
cssContainer: "cat_repeticao",
|
||||
blocos: ["controls_whileUntil", "controls_repeat_ext"]
|
||||
},
|
||||
{
|
||||
nome: "Movimento",
|
||||
cor: CORES_CUSTOMIZADAS.MOVIMENTO,
|
||||
cssContainer: "cat_movimento",
|
||||
blocos: ["robo_mover", "robo_virar"]
|
||||
},
|
||||
{
|
||||
nome: "Sensores",
|
||||
cor: CORES_CUSTOMIZADAS.SENSORES,
|
||||
cssContainer: "cat_sensores",
|
||||
blocos: ["robo_ainda_tem_sujeira", "robo_bloqueado"]
|
||||
},
|
||||
{
|
||||
nome: "Variáveis",
|
||||
cssContainer: "cat_variaveis",
|
||||
blocos: ["robo_passos_set", "robo_passos_get", "robo_passos_change"]
|
||||
},
|
||||
{
|
||||
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 = () => {
|
||||
// LÓGICA
|
||||
criarBlocoCondicional("robo_if", {
|
||||
textoCondicao: "se",
|
||||
statements: [{ nome: "FACA", texto: "faça" }],
|
||||
cor: CORES_BLOCKLY.LOGICA
|
||||
});
|
||||
|
||||
criarBlocoCondicional("robo_if_else", {
|
||||
textoCondicao: "se",
|
||||
statements: [
|
||||
{ nome: "FACA", texto: "faça" },
|
||||
{ nome: "SENAO", texto: "senão" }
|
||||
],
|
||||
cor: CORES_BLOCKLY.LOGICA
|
||||
});
|
||||
|
||||
criarBlocoNegacao(
|
||||
"robo_not",
|
||||
"não",
|
||||
CORES_BLOCKLY.LOGICA,
|
||||
"Inverte o resultado do sensor (ex: de 'bloqueado' para 'não bloqueado')."
|
||||
);
|
||||
|
||||
// MOVIMENTO
|
||||
criarBlocoStatementSimples(
|
||||
"robo_mover",
|
||||
"mover para FRENTE",
|
||||
CORES_CUSTOMIZADAS.MOVIMENTO
|
||||
);
|
||||
|
||||
criarBlocoStatementComDropdown(
|
||||
"robo_virar",
|
||||
"virar para a",
|
||||
[["DIREITA", "DIREITA"], ["ESQUERDA", "ESQUERDA"]],
|
||||
"DIRECAO",
|
||||
CORES_CUSTOMIZADAS.MOVIMENTO
|
||||
);
|
||||
|
||||
// SENSORES
|
||||
criarBlocoExpressaoSimples(
|
||||
"robo_ainda_tem_sujeira",
|
||||
"ainda tem sujeira?",
|
||||
"Boolean",
|
||||
CORES_CUSTOMIZADAS.SENSORES
|
||||
);
|
||||
|
||||
criarBlocoExpressaoComDropdown(
|
||||
"robo_bloqueado",
|
||||
"caminho bloqueado à",
|
||||
[["FRENTE", "FRENTE"], ["DIREITA", "DIREITA"], ["ESQUERDA", "ESQUERDA"]],
|
||||
"SENTIDO",
|
||||
"Boolean",
|
||||
CORES_CUSTOMIZADAS.SENSORES
|
||||
);
|
||||
|
||||
// VARIÁVEIS
|
||||
criarBlocoStatementComValor(
|
||||
"robo_passos_set",
|
||||
"definir PASSOS para",
|
||||
"VALOR",
|
||||
"Number",
|
||||
CORES_BLOCKLY.VARIAVEIS
|
||||
);
|
||||
|
||||
criarBlocoExpressaoSimples(
|
||||
"robo_passos_get",
|
||||
"PASSOS",
|
||||
"Number",
|
||||
CORES_BLOCKLY.VARIAVEIS
|
||||
);
|
||||
|
||||
criarBlocoStatementSimples(
|
||||
"robo_passos_change",
|
||||
"aumentar PASSOS em 1",
|
||||
CORES_BLOCKLY.VARIAVEIS
|
||||
);
|
||||
};
|
||||
|
||||
const defineGenerators = () => {
|
||||
configurarGerador();
|
||||
|
||||
// LÓGICA - Geradores complexos mantidos explícitos
|
||||
javascriptGenerator.forBlock["robo_if"] = function (block) {
|
||||
let condicao = javascriptGenerator.valueToCode(block, "CONDICAO", javascriptGenerator.ORDER_NONE) || "false";
|
||||
return `if (${condicao}) {\n${javascriptGenerator.statementToCode(block, "FACA")}}\n`;
|
||||
};
|
||||
|
||||
javascriptGenerator.forBlock["robo_if_else"] = function (block) {
|
||||
let condicao = javascriptGenerator.valueToCode(block, "CONDICAO", javascriptGenerator.ORDER_NONE) || "false";
|
||||
return `if (${condicao}) {\n${javascriptGenerator.statementToCode(block, "FACA")}} else {\n${javascriptGenerator.statementToCode(block, "SENAO")}}\n`;
|
||||
};
|
||||
|
||||
javascriptGenerator.forBlock["robo_not"] = function (block) {
|
||||
let innerCode = javascriptGenerator.valueToCode(block, "BOOL", javascriptGenerator.ORDER_LOGICAL_NOT) || "false";
|
||||
return [`!${innerCode}`, javascriptGenerator.ORDER_LOGICAL_NOT];
|
||||
};
|
||||
|
||||
// MOVIMENTO - Agora com helpers
|
||||
gerarStatement("robo_mover", "mover");
|
||||
gerarStatementComCampo("robo_virar", "virar", "DIRECAO");
|
||||
|
||||
// SENSORES - Agora com helpers
|
||||
gerarExpressao("robo_ainda_tem_sujeira", "aindaTemSujeira()", javascriptGenerator.ORDER_FUNCTION_CALL);
|
||||
gerarExpressaoComCampo("robo_bloqueado", "caminhoBloqueado", "SENTIDO", javascriptGenerator.ORDER_FUNCTION_CALL);
|
||||
|
||||
// VARIÁVEIS - Agora com helpers
|
||||
gerarStatementComValor("robo_passos_set", "VALOR", (valor) => `var passos = ${valor}`);
|
||||
gerarExpressao("robo_passos_get", "passos");
|
||||
gerarStatementInline("robo_passos_change", "passos = passos + 1");
|
||||
};
|
||||
271
app/src/atividades/programacao/aspirador/config/config.js
Normal file
@@ -0,0 +1,271 @@
|
||||
/**
|
||||
* @fileoverview Configuração completa para o jogo Aspirador (Lógica e Navegação)
|
||||
* Com progressão pedagógica de 10 Fases (Nível 1 ao Nível 4)
|
||||
* @module games.aspirador.config.config
|
||||
*/
|
||||
|
||||
export const gameConfig = {
|
||||
gameId: "aspirador",
|
||||
gameName: "Aspirador",
|
||||
type: "blocks",
|
||||
icon: "🧹",
|
||||
thumbnail: "/images/atividades/programacao/aspirador-thumbnail.png",
|
||||
descricao:
|
||||
"Programe um aspirador robô reativo. Aprenda do básico aos algoritmos avançados de navegação e variáveis.",
|
||||
dificuldade: "Iniciante",
|
||||
categoria: "Lógica",
|
||||
tempoEstimado: "30-45 min",
|
||||
conceitos: [
|
||||
"Sequenciamento",
|
||||
"Laços de Repetição (While/For)",
|
||||
"Condicionais (If/Else)",
|
||||
"Sensores de Colisão",
|
||||
"Variáveis (Incremento)",
|
||||
"Lógica de Negação (Não)"
|
||||
],
|
||||
route: "/atividades/programacao/aspirador",
|
||||
component: "AspiradorGame",
|
||||
objectives: [
|
||||
"Compreender sequenciamento e repetições",
|
||||
"Utilizar sensores para tomada de decisão em tempo real",
|
||||
"Criar padrões expansivos usando variáveis numéricas",
|
||||
"Desenvolver algoritmos de cobertura de área (Zigue-zague e Espiral)"
|
||||
],
|
||||
metadata: {
|
||||
lastUpdated: "2026-03-06",
|
||||
version: "2.0.0",
|
||||
},
|
||||
|
||||
fases: [
|
||||
// ==========================================
|
||||
// NÍVEL 1: MOVIMENTO E PADRÕES
|
||||
// ==========================================
|
||||
{
|
||||
id: 1,
|
||||
nome: "Fase 1: A Linha Reta",
|
||||
descricao: "Use o bloco 'Enquanto houver sujeira' e o comando 'Mover' para ligá-lo.",
|
||||
timeout: 10,
|
||||
maxBlocks: 3,
|
||||
background: "piso_claro",
|
||||
allowedBlocks: ["controls_whileUntil", "robo_mover", "robo_ainda_tem_sujeira"],
|
||||
direcao: 90,
|
||||
matriz: [
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[4, 1, 2, 2, 2, 2, 2, 3, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
],
|
||||
js: "while (aindaTemSujeira()) {\n mover();\n}"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
nome: "Fase 2: A Curva no Corredor",
|
||||
descricao: "Use o bloco de 'Repita X vezes' para andar, virar e andar de novo.",
|
||||
timeout: 15,
|
||||
maxBlocks: 7,
|
||||
background: "piso_claro",
|
||||
// Restringe ao laço numérico para treinar sequenciamento sem sensores ainda
|
||||
allowedBlocks: ["controls_repeat_ext", "math_number", "robo_mover", "robo_virar"],
|
||||
direcao: 90,
|
||||
matriz: [
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[4, 1, 2, 2, 2, 2, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 2, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 2, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 2, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 3, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
],
|
||||
js: "for (var i = 0; i < 4; i++) {\n mover();\n}\nvirar('direita');\nfor (var j = 0; j < 3; j++) {\n mover();\n}"
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
nome: "Fase 3: A Escadinha",
|
||||
descricao: "Crie uma escada colocando um 'Mover' e 'Virar' repetidas vezes.",
|
||||
timeout: 20,
|
||||
maxBlocks: 7,
|
||||
background: "piso_claro",
|
||||
allowedBlocks: ["controls_repeat_ext", "math_number", "robo_mover", "robo_virar"],
|
||||
direcao: 90,
|
||||
matriz: [
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[4, 1, 2, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 2, 2, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 2, 2, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 2, 2, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 2, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 4, 0, 0, 0, 0]
|
||||
],
|
||||
js: "for (var i = 0; i < 4; i++) {\n mover();\n virar('direita');\n mover();\n virar('esquerda');\n}"
|
||||
},
|
||||
|
||||
// ==========================================
|
||||
// NÍVEL 2: SENSORES E DECISÕES
|
||||
// ==========================================
|
||||
{
|
||||
id: 4,
|
||||
nome: "Fase 4: O Sensor de Impacto",
|
||||
descricao: "Use 'Se / Senão' e o Sensor: Se a frente estiver bloqueada por um vaso, vire. Senão, mova-se.",
|
||||
timeout: 20,
|
||||
maxBlocks: 6,
|
||||
background: "piso_claro",
|
||||
allowedBlocks: ["controls_whileUntil", "robo_if_else", "robo_mover", "robo_virar", "robo_bloqueado", "robo_ainda_tem_sujeira"],
|
||||
direcao: 90,
|
||||
matriz: [
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[4, 1, 2, 2, 2, 2, 2, 2, 4, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 2, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 2, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 2, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 3, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
],
|
||||
js: "while (aindaTemSujeira()) {\n if (caminhoBloqueado('frente')) {\n virar('direita');\n } else {\n mover();\n }\n}"
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
nome: "Fase 5: Modo Bordas",
|
||||
descricao: "Limpe apenas os cantos da sala seguindo a parede até dar a volta.",
|
||||
timeout: 30,
|
||||
maxBlocks: 6,
|
||||
background: "piso_escuro",
|
||||
allowedBlocks: ["controls_whileUntil", "robo_if_else", "robo_mover", "robo_virar", "robo_bloqueado", "robo_ainda_tem_sujeira"],
|
||||
msgErroValidacao: "No modo bordas, o robô deve seguir a parede virando sempre para o mesmo lado (90° constantes).",
|
||||
direcao: 90,
|
||||
matriz: [
|
||||
[4, 3, 4, 3, 4, 3, 4, 3, 4, 3],
|
||||
[3, 1, 2, 2, 2, 2, 2, 2, 2, 4],
|
||||
[4, 2, 0, 0, 0, 0, 0, 0, 2, 3],
|
||||
[3, 2, 0, 0, 0, 0, 0, 0, 2, 4],
|
||||
[4, 2, 0, 0, 0, 0, 0, 0, 2, 3],
|
||||
[3, 2, 2, 2, 2, 2, 2, 2, 2, 4],
|
||||
[4, 3, 4, 3, 4, 3, 4, 3, 4, 3]
|
||||
],
|
||||
js: "while (aindaTemSujeira()) {\n if (caminhoBloqueado('frente')) {\n virar('direita');\n } else {\n mover();\n }\n}"
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
nome: "Fase 6: Desvio de Obstáculo",
|
||||
descricao: "Se o caminho à frente bloquear, você precisa dar a volta por fora e voltar ao trilho.",
|
||||
timeout: 35,
|
||||
maxBlocks: 12,
|
||||
background: "piso_claro",
|
||||
allowedBlocks: ["controls_whileUntil", "robo_if_else", "robo_mover", "robo_virar", "robo_bloqueado", "robo_ainda_tem_sujeira"],
|
||||
direcao: 90,
|
||||
matriz: [
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 2, 2, 2, 0, 0, 0, 0],
|
||||
[4, 1, 2, 4, 0, 2, 2, 2, 3, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
],
|
||||
js: "while (aindaTemSujeira()) {\n if (caminhoBloqueado('frente')) {\n virar('esquerda');\n mover();\n virar('direita');\n mover();\n mover();\n virar('direita');\n mover();\n virar('esquerda');\n } else {\n mover();\n }\n}"
|
||||
},
|
||||
|
||||
// ==========================================
|
||||
// NÍVEL 3: VARIÁVEIS (PADRÕES CRESCENTES)
|
||||
// ==========================================
|
||||
{
|
||||
id: 7,
|
||||
nome: "Fase 7: A Escada Crescente",
|
||||
descricao: "Use a variável [passos] para fazer o robô acompanhar esse crescimento!",
|
||||
timeout: 30,
|
||||
maxBlocks: 15,
|
||||
background: "piso_claro",
|
||||
allowedBlocks: ["controls_repeat_ext", "math_number", "robo_passos_set", "robo_passos_get", "robo_passos_change", "robo_mover", "robo_virar"],
|
||||
direcao: 180, // Começa olhando para baixo
|
||||
validationRegex: /passos\s*(?:=\s*passos\s*\+\s*1|\+=\s*1|\+\+)/,
|
||||
msgErroIncremento: "Você precisa usar o bloco 'aumentar [passos] em 1' para o degrau crescer!",
|
||||
matriz: [
|
||||
[1, 4, 0, 0, 0, 0, 0, 0, 0, 3],
|
||||
[2, 2, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 2, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 2, 2, 2, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 2, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 2, 0, 0, 0, 0, 0, 0],
|
||||
[4, 0, 0, 2, 2, 2, 2, 3, 0, 4]
|
||||
],
|
||||
js: "var passos = 1;\nfor (var i = 0; i < 3; i++) {\n for (var j = 0; j < passos; j++) {\n mover();\n }\n virar('esquerda');\n for (var k = 0; k < passos; k++) {\n mover();\n }\n virar('direita');\n passos = passos + 1;\n}"
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
nome: "Fase 8: A Espiral de Limpeza",
|
||||
descricao: "Junte o que aprendeu sobre repetições com o aumento da variável para limpar do centro para as bordas.",
|
||||
timeout: 45,
|
||||
maxBlocks: 15,
|
||||
background: "piso_claro",
|
||||
allowedBlocks: ["controls_whileUntil", "controls_repeat_ext", "math_number", "robo_passos_set", "robo_passos_get", "robo_passos_change", "robo_mover", "robo_virar", "robo_ainda_tem_sujeira", "robo_bloqueado", "robo_if", "robo_not"],
|
||||
direcao: 0,
|
||||
msgErroIncremento: "A espiral exige que a distância (variável passos) aumente a cada volta!",
|
||||
validationRegex: /passos\s*(?:=\s*passos\s*\+\s*1|\+=\s*1|\+\+)/,
|
||||
matriz: [
|
||||
[4, 0, 0, 0, 0, 0, 0, 0, 0, 3],
|
||||
[0, 0, 2, 2, 2, 2, 2, 0, 0, 0],
|
||||
[0, 0, 2, 2, 2, 2, 2, 0, 0, 0],
|
||||
[0, 0, 2, 2, 1, 2, 2, 0, 0, 0],
|
||||
[0, 0, 2, 2, 2, 2, 2, 0, 0, 0],
|
||||
[0, 0, 2, 2, 2, 2, 2, 0, 0, 0],
|
||||
[3, 0, 0, 0, 0, 0, 0, 0, 0, 4]
|
||||
],
|
||||
js: "var passos = 1;\nwhile (aindaTemSujeira()) {\n for (var i = 0; i < 2; i++) {\n for (var j = 0; j < passos; j++) {\n if (!caminhoBloqueado('frente')) {\n mover();\n }\n }\n virar('direita');\n }\n passos = passos + 1;\n}"
|
||||
},
|
||||
|
||||
// ==========================================
|
||||
// NÍVEL 4: LÓGICA AVANÇADA
|
||||
// ==========================================
|
||||
{
|
||||
id: 9,
|
||||
nome: "Fase 9: O Labirinto Cego",
|
||||
descricao: "Se a frente estiver bloqueada, teste a direita! Se a direita também bloquear, vire à esquerda.",
|
||||
timeout: 45,
|
||||
maxBlocks: 10,
|
||||
background: "piso_claro",
|
||||
allowedBlocks: ["controls_whileUntil", "robo_if_else", "robo_if", "robo_mover", "robo_virar", "robo_bloqueado", "robo_ainda_tem_sujeira"],
|
||||
direcao: 90,
|
||||
matriz: [
|
||||
[1, 2, 2, 2, 2, 2, 2, 2, 2, 2],
|
||||
[4, 3, 4, 3, 4, 3, 4, 3, 4, 2], // Parede força a descer
|
||||
[2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
|
||||
[2, 4, 3, 4, 3, 4, 3, 4, 3, 4], // Parede força a descer pela esquerda
|
||||
[2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
|
||||
[4, 3, 4, 3, 4, 3, 4, 3, 4, 2], // Parede força a descer pela direita
|
||||
[2, 2, 2, 2, 2, 2, 2, 2, 2, 2]
|
||||
],
|
||||
js: "while (aindaTemSujeira()) {\n if (caminhoBloqueado('frente')) {\n if (caminhoBloqueado('direita')) {\n virar('esquerda');\n } else {\n virar('direita');\n }\n } else {\n mover();\n }\n}"
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
nome: "Fase 10: Zigue-Zague",
|
||||
descricao: "Crie um algoritmo de espelhamento (Zigue-Zague) combinando o bloco 'NÃO' e os sensores.",
|
||||
timeout: 60,
|
||||
maxBlocks: 21,
|
||||
background: "piso_claro",
|
||||
allowedBlocks: ["controls_whileUntil", "robo_if", "robo_if_else", "robo_not", "robo_mover", "robo_virar", "robo_bloqueado", "robo_ainda_tem_sujeira"],
|
||||
direcao: 90,
|
||||
msgErroValidacao: "Para limpar tudo sem gastar bateria, você deve alternar as viradas (Esquerda e Direita) num padrão de espelho perfeito!",
|
||||
matriz: [
|
||||
[1, 2, 2, 2, 2, 2, 2, 2, 2, 2],
|
||||
[2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
|
||||
[2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
|
||||
[2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
|
||||
[2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
|
||||
[2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
|
||||
[2, 2, 2, 2, 2, 2, 2, 2, 2, 2]
|
||||
],
|
||||
js: "while (aindaTemSujeira()) {\n if (caminhoBloqueado('frente')) {\n virar('direita');\n if (!caminhoBloqueado('frente')) {\n mover();\n virar('direita');\n while (!caminhoBloqueado('frente')) {\n mover();\n }\n virar('esquerda');\n if (!caminhoBloqueado('frente')) {\n mover();\n virar('esquerda');\n }\n }\n } else {\n mover();\n }\n}"
|
||||
}
|
||||
],
|
||||
|
||||
mensagens: {
|
||||
entradaIncorreta: "Seu robô está parado! Verifique se você usou os blocos de Movimento.",
|
||||
saidaIncorreta: "O robô bateu ou ficou preso! Revise sua lógica de sensores e curvas.",
|
||||
erroGeral: "O sistema de navegação falhou. Reinicie os blocos e tente uma nova estratégia.",
|
||||
sucessoGenerico: "Excelente! Missão concluída.",
|
||||
timeoutExcedido: "Bateria esgotada! O robô não conseguiu limpar tudo a tempo. Tente um caminho mais eficiente.",
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* @fileoverview Utility module for debugSolutions.js
|
||||
* * @module games.aspirador.config.debugSolutions
|
||||
*/
|
||||
|
||||
export const debugSolutions = {
|
||||
1: { "blocks": { "languageVersion": 0, "blocks": [{ "type": "controls_whileUntil", "id": "z`+9FERBc[#C{DF0qLKo", "x": 38, "y": 63, "fields": { "MODE": "WHILE" }, "inputs": { "BOOL": { "block": { "type": "robo_ainda_tem_sujeira", "id": "Q9{W.6:H}mhGgHaW#Jl$" } }, "DO": { "block": { "type": "robo_mover", "id": "GY]faaY{o.T,X3z:ke,[" } } } }] } },
|
||||
2: { "blocks": { "languageVersion": 0, "blocks": [{ "type": "controls_repeat_ext", "id": "qQiC857Z8^744|;;=#Rl", "x": 88, "y": 38, "inputs": { "TIMES": { "block": { "type": "math_number", "id": "G,6SEx#|Xi`{/=Y7z9%X", "fields": { "NUM": 2 } } }, "DO": { "block": { "type": "controls_repeat_ext", "id": "1)h4Hs##Qlgp~Q3)}{u9", "inputs": { "TIMES": { "block": { "type": "math_number", "id": "ZQ59,M3MoT/ixV#4~MLO", "fields": { "NUM": 4 } } }, "DO": { "block": { "type": "robo_mover", "id": "yU252A1/y]]f3]lFaQnz" } } }, "next": { "block": { "type": "robo_virar", "id": "YiYbJRF7NX,bKc8XmS8H", "fields": { "DIRECAO": "DIREITA" } } } } } } }] } },
|
||||
3: { "blocks": { "languageVersion": 0, "blocks": [{ "type": "controls_repeat_ext", "id": ".PM/Ok.U-LI9zBcN`Tff", "x": 63, "y": 38, "inputs": { "TIMES": { "block": { "type": "math_number", "id": "uwvH}w+GG%^/-4e_3~Nb", "fields": { "NUM": 4 } } }, "DO": { "block": { "type": "robo_mover", "id": "w%AD5UlheXiD3~+{M4Du", "next": { "block": { "type": "robo_virar", "id": "8(J:{mas+h]+u2BDG]V*", "fields": { "DIRECAO": "DIREITA" }, "next": { "block": { "type": "robo_mover", "id": "wQ?)#`vK[GZVBT:d1w./", "next": { "block": { "type": "robo_virar", "id": "eM.S;LJ!aLO3$itXe#W-", "fields": { "DIRECAO": "ESQUERDA" } } } } } } } } } } }] } },
|
||||
4: { "blocks": { "languageVersion": 0, "blocks": [{ "type": "controls_whileUntil", "id": "yIMZ``.@EH/4.([Fe#Y{", "x": 13, "y": 38, "fields": { "MODE": "WHILE" }, "inputs": { "BOOL": { "block": { "type": "robo_ainda_tem_sujeira", "id": "(!.D]lmG3$!+X3ZUolVC" } }, "DO": { "block": { "type": "robo_if_else", "id": "vteh/xjeNmGn+nepnTMz", "inputs": { "CONDICAO": { "block": { "type": "robo_bloqueado", "id": "%oNA;tN,!/^?K0`ddBj~", "fields": { "SENTIDO": "FRENTE" } } }, "FACA": { "block": { "type": "robo_virar", "id": "I[mh-8|eV8*l093y8kXM", "fields": { "DIRECAO": "DIREITA" } } }, "SENAO": { "block": { "type": "robo_mover", "id": "x-%P*)*_;H;2@UsUo;|," } } } } } } }] } },
|
||||
5: { "blocks": { "languageVersion": 0, "blocks": [{ "type": "controls_whileUntil", "id": "xT~lXFK.CqT/w7x*!lHX", "x": 31, "y": 80, "fields": { "MODE": "WHILE" }, "inputs": { "BOOL": { "block": { "type": "robo_ainda_tem_sujeira", "id": "fSn99zjFK[9YB|Qt*qiF" } }, "DO": { "block": { "type": "robo_if_else", "id": "FPFLw`jCzRq+99Ox6]9;", "inputs": { "CONDICAO": { "block": { "type": "robo_bloqueado", "id": "]/`EA1Gb@Fhug7/89YY|", "fields": { "SENTIDO": "FRENTE" } } }, "FACA": { "block": { "type": "robo_virar", "id": "$(%,l|Mi542;nAzm]5(M", "fields": { "DIRECAO": "DIREITA" } } }, "SENAO": { "block": { "type": "robo_mover", "id": "|LEg;*WF_2H|RiM?0nak" } } } } } } }] } },
|
||||
6: { "blocks": { "languageVersion": 0, "blocks": [{ "type": "robo_mover", "id": "!beDf|CKAdE]Pq+u#!+s", "x": 63, "y": 38, "next": { "block": { "type": "robo_virar", "id": "3RBd?a0c)||Ct2#6}S:$", "fields": { "DIRECAO": "ESQUERDA" }, "next": { "block": { "type": "robo_mover", "id": "aYI2OD+D@~W6ZIl9$1~W", "next": { "block": { "type": "robo_virar", "id": "!G|RISODQu5[{tzI_!5`", "fields": { "DIRECAO": "DIREITA" }, "next": { "block": { "type": "robo_mover", "id": "I/F:Ona`]5A?h[7[r$},", "next": { "block": { "type": "robo_mover", "id": "@*mPCkXi83Wru)rKZ$;M", "next": { "block": { "type": "robo_mover", "id": "bAMMIR~u1Cxzq4nhqU.p", "next": { "block": { "type": "robo_virar", "id": "hBp68K1fY5+vp}m]*Hn*", "fields": { "DIRECAO": "DIREITA" }, "next": { "block": { "type": "robo_mover", "id": "na+Y)U@2SkSDR06^Hjg0", "next": { "block": { "type": "robo_virar", "id": "t]VH-I19H1hV7K2Q==Zx", "fields": { "DIRECAO": "ESQUERDA" }, "next": { "block": { "type": "robo_mover", "id": "L/.4K.fQTt:piUuPJ_7W", "next": { "block": { "type": "robo_mover", "id": "X[:(])AH5E9p3M#vbs^F" } } } } } } } } } } } } } } } } } } } } } } }] } },
|
||||
7: { "blocks": { "languageVersion": 0, "blocks": [{ "type": "robo_passos_set", "id": "X+}=8*(nLRcpL!9r2N(~", "x": 29, "y": 140, "inputs": { "VALOR": { "block": { "type": "math_number", "id": "+08WKoLsuMH7vadc5=`@", "fields": { "NUM": 1 } } } }, "next": { "block": { "type": "controls_repeat_ext", "id": "1F(h5%9CMjU@V7fWUIW4", "inputs": { "TIMES": { "block": { "type": "math_number", "id": "W^[`yC.})u!FLB{wdMXT", "fields": { "NUM": 3 } } }, "DO": { "block": { "type": "controls_repeat_ext", "id": "uh2W($TTLO2sKIq99RM~", "inputs": { "TIMES": { "block": { "type": "robo_passos_get", "id": ":Q.w_C,5(%Qe{OUxbXFD" } }, "DO": { "block": { "type": "robo_mover", "id": "T(5A].GBtgT4^+ia,@Nv" } } }, "next": { "block": { "type": "robo_virar", "id": "VLRg3eC|t08ur8+k$=zi", "fields": { "DIRECAO": "ESQUERDA" }, "next": { "block": { "type": "controls_repeat_ext", "id": "zbY3CMg}q`0(Yl/$wxjl", "inputs": { "TIMES": { "block": { "type": "robo_passos_get", "id": "|qswpY1=VH.OhAOQ=0X?" } }, "DO": { "block": { "type": "robo_mover", "id": "B/]oj60w6i/Z:qW^^h!x" } } }, "next": { "block": { "type": "robo_virar", "id": "S9hZUx[A~3zDYFaO*mk`", "fields": { "DIRECAO": "DIREITA" }, "next": { "block": { "type": "robo_passos_change", "id": ".^E5@/q]9e?:)=lk!sT@" } } } } } } } } } } } } } }] } },
|
||||
8: { "blocks": { "languageVersion": 0, "blocks": [{ "type": "robo_passos_set", "id": ")2S?VQvKtDbSYH|)[.h6", "x": -12, "y": -362, "inputs": { "VALOR": { "block": { "type": "math_number", "id": "C)o;-^.7+;g#PMc^K1;r", "fields": { "NUM": 1 } } } }, "next": { "block": { "type": "controls_whileUntil", "id": ";+%[|n.,d4~!0}^|Ey3~", "fields": { "MODE": "WHILE" }, "inputs": { "BOOL": { "block": { "type": "robo_ainda_tem_sujeira", "id": "A=FlJKG;4KP#4eKS#U$e" } }, "DO": { "block": { "type": "controls_repeat_ext", "id": "(,y.Ls53b9_5-|3/E_B*", "inputs": { "TIMES": { "block": { "type": "math_number", "id": "I$|G5*7L?BWN]A,~mf,h", "fields": { "NUM": 2 } } }, "DO": { "block": { "type": "controls_repeat_ext", "id": "]VPOvSUj%m!N*YcsNtSb", "inputs": { "TIMES": { "block": { "type": "robo_passos_get", "id": "#o{aHXke[Vrxfa)cuPJr" } }, "DO": { "block": { "type": "robo_if", "id": "feY|w[y*GnGKT5Q#5E@h", "inputs": { "CONDICAO": { "block": { "type": "robo_not", "id": "|26~VFf)cim+/OztPIKV", "inputs": { "BOOL": { "block": { "type": "robo_bloqueado", "id": "sRmx!]c576uF3e#-{_J1", "fields": { "SENTIDO": "FRENTE" } } } } } }, "FACA": { "block": { "type": "robo_mover", "id": "hvJse`aK-rxytP:($|3X" } } } } } }, "next": { "block": { "type": "robo_virar", "id": "!1)|e0-^d`+lG:]7c*r*", "fields": { "DIRECAO": "DIREITA" } } } } } }, "next": { "block": { "type": "robo_passos_change", "id": "@1wYZzVADKI}T8qokuWB" } } } } } } } }] } },
|
||||
9: { "blocks": { "languageVersion": 0, "blocks": [{ "type": "controls_whileUntil", "id": "xsk|NgFG//v)LLzoMDG!", "x": 50, "y": 87, "fields": { "MODE": "WHILE" }, "inputs": { "BOOL": { "block": { "type": "robo_ainda_tem_sujeira", "id": "23G7/4N(HIL}92H~(x!~" } }, "DO": { "block": { "type": "robo_if_else", "id": "p,VxRb5M#^9U-Rw+[CIr", "inputs": { "CONDICAO": { "block": { "type": "robo_bloqueado", "id": "WB/+t9+Z(oCnuq7;iEdz", "fields": { "SENTIDO": "FRENTE" } } }, "FACA": { "block": { "type": "robo_if_else", "id": "XPtT|?Xcx:p]`wO:^X/w", "inputs": { "CONDICAO": { "block": { "type": "robo_bloqueado", "id": "l:f6*K[/tx,u)uji(0VE", "fields": { "SENTIDO": "ESQUERDA" } } }, "FACA": { "block": { "type": "robo_virar", "id": "+L*?[V=BlPt,rBO6Az7G", "fields": { "DIRECAO": "DIREITA" } } }, "SENAO": { "block": { "type": "robo_virar", "id": "/OklKuNamD7^y0WljsKV", "fields": { "DIRECAO": "ESQUERDA" } } } } } }, "SENAO": { "block": { "type": "robo_mover", "id": "X#Rr35W]JJoh[F;|1*YZ" } } } } } } }] } },
|
||||
10: { "blocks": { "languageVersion": 0, "blocks": [{ "type": "controls_whileUntil", "id": "/3Q8xl3RGfxhGe86AF7m", "x": 13, "y": 13, "fields": { "MODE": "WHILE" }, "inputs": { "BOOL": { "block": { "type": "robo_ainda_tem_sujeira", "id": "yvRE91],$![+nEW3C2-;" } }, "DO": { "block": { "type": "robo_if_else", "id": "htM_jv|S#2o~[YV*hGR|", "inputs": { "CONDICAO": { "block": { "type": "robo_bloqueado", "id": "*G8n[NSiy!N*?nk2U-7P", "fields": { "SENTIDO": "FRENTE" } } }, "FACA": { "block": { "type": "robo_virar", "id": "I/Rbq(Z5Skl*R{9,cdk$", "fields": { "DIRECAO": "DIREITA" }, "next": { "block": { "type": "robo_if", "id": ";j,eV*TRdTUa4=ND9M/!", "inputs": { "CONDICAO": { "block": { "type": "robo_not", "id": "7cO.F;Q3/I{UZSm4M|lh", "inputs": { "BOOL": { "block": { "type": "robo_bloqueado", "id": "g/ZLELf;{y{LkX1j)GFb", "fields": { "SENTIDO": "FRENTE" } } } } } }, "FACA": { "block": { "type": "robo_mover", "id": "SUtlAI09Jy}ExT}LD2H]", "next": { "block": { "type": "robo_virar", "id": "aaNewY:Hdt8`!x8+.V{F", "fields": { "DIRECAO": "DIREITA" }, "next": { "block": { "type": "controls_whileUntil", "id": "4J_#S0)[q9f6iUs?vQa:", "fields": { "MODE": "WHILE" }, "inputs": { "BOOL": { "block": { "type": "robo_not", "id": "@:+j*`]Xa*k#e(CwNqN`", "inputs": { "BOOL": { "block": { "type": "robo_bloqueado", "id": "Xb4mo/4x@Wz17r]CgI[{", "fields": { "SENTIDO": "FRENTE" } } } } } }, "DO": { "block": { "type": "robo_mover", "id": "YeBrVnP8(S*YW2[aPv!u" } } }, "next": { "block": { "type": "robo_virar", "id": "}_J_g;Y^d~jrp9m26Rmd", "fields": { "DIRECAO": "ESQUERDA" }, "next": { "block": { "type": "robo_if", "id": "7L.|=-gEc7U-z8r:eG?+", "inputs": { "CONDICAO": { "block": { "type": "robo_not", "id": "`3IlM;WJApe@AWi6y9k8", "inputs": { "BOOL": { "block": { "type": "robo_bloqueado", "id": "QwOui[*+]`YYx$KsQePj", "fields": { "SENTIDO": "FRENTE" } } } } } }, "FACA": { "block": { "type": "robo_mover", "id": "LV=OVlA|~R|^i5].f?E=", "next": { "block": { "type": "robo_virar", "id": ":yg*F*z$X4@a24TIxM#(", "fields": { "DIRECAO": "ESQUERDA" } } } } } } } } } } } } } } } } } } } } }, "SENAO": { "block": { "type": "robo_mover", "id": ")E4emztur3[Sq?^:UC{(" } } } } } } }] } },
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* @fileoverview Utility module for starterBlocks.js
|
||||
*
|
||||
* @module games.aspirador.config.starterBlocks
|
||||
*/
|
||||
|
||||
export const starterBlocks = {
|
||||
1: {"blocks":{"languageVersion":0,"blocks":[{"type":"controls_whileUntil","id":"z`+9FERBc[#C{DF0qLKo","x":13,"y":38,"fields":{"MODE":"WHILE"},"inputs":{"DO":{"block":{"type":"robo_mover","id":"GY]faaY{o.T,X3z:ke,["}}}}]}},
|
||||
2: {"blocks":{"languageVersion":0,"blocks":[{"type":"controls_repeat_ext","id":"qQiC857Z8^744|;;=#Rl","x":13,"y":13,"inputs":{"TIMES":{"block":{"type":"math_number","id":"G,6SEx#|Xi`{/=Y7z9%X","fields":{"NUM":0}}}}}]}},
|
||||
4: {"blocks":{"languageVersion":0,"blocks":[{"type":"robo_if_else","id":"vteh/xjeNmGn+nepnTMz","x":38,"y":38,"inputs":{"CONDICAO":{"block":{"type":"robo_bloqueado","id":"%oNA;tN,!/^?K0`ddBj~","fields":{"SENTIDO":"FRENTE"}}}}}]}},
|
||||
7: {"blocks":{"languageVersion":0,"blocks":[{"type":"robo_passos_set","id":"X+}=8*(nLRcpL!9r2N(~","x":29,"y":140,"inputs":{"VALOR":{"block":{"type":"math_number","id":"+08WKoLsuMH7vadc5=`@","fields":{"NUM":1}}}},"next":{"block":{"type":"controls_repeat_ext","id":"1F(h5%9CMjU@V7fWUIW4","inputs":{"TIMES":{"block":{"type":"math_number","id":"W^[`yC.})u!FLB{wdMXT","fields":{"NUM":3}}},"DO":{"block":{"type":"controls_repeat_ext","id":"uh2W($TTLO2sKIq99RM~","inputs":{"TIMES":{"block":{"type":"robo_passos_get","id":":Q.w_C,5(%Qe{OUxbXFD"}}},"next":{"block":{"type":"robo_passos_change","id":".^E5@/q]9e?:)=lk!sT@"}}}}}}}}]}},
|
||||
8: {"blocks":{"languageVersion":0,"blocks":[{"type":"robo_if","id":"feY|w[y*GnGKT5Q#5E@h","x":363,"y":-287,"inputs":{"CONDICAO":{"block":{"type":"robo_not","id":"|26~VFf)cim+/OztPIKV","inputs":{"BOOL":{"block":{"type":"robo_bloqueado","id":"sRmx!]c576uF3e#-{_J1","fields":{"SENTIDO":"FRENTE"}}}}}}}}]}}
|
||||
};
|
||||
73
app/src/atividades/programacao/aspirador/config/tourSteps.js
Normal file
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* @fileoverview Utility module for tourSteps.js
|
||||
*
|
||||
* @module games.aspirador.config.tourSteps
|
||||
*/
|
||||
|
||||
import {
|
||||
createWelcomeStep,
|
||||
createGameAreaStep,
|
||||
createToolboxStep,
|
||||
createWorkspaceStep,
|
||||
createRunButtonStep,
|
||||
createResetInfoStep,
|
||||
createPhaseSelectorStep,
|
||||
createPhaseInfoStep,
|
||||
createHelpButtonStep,
|
||||
gameIcons,
|
||||
defaultGameTourOptions,
|
||||
} from "../../../../utils/tourHelpers";
|
||||
|
||||
export const aspiradorTourSteps = [
|
||||
createWelcomeStep({
|
||||
gameName: "Jogo Aspirador",
|
||||
description:
|
||||
"Bem-vindo ao mundo da programação! Aqui você vai aprender os fundamentos de lógica e como controlar um robô aspirador.",
|
||||
challenge:
|
||||
"Use programação em blocos para controlar o robô aspirador e limpar toda a sujeira!",
|
||||
iconSvg: gameIcons.lock,
|
||||
}),
|
||||
|
||||
createGameAreaStep({
|
||||
title: "Área de Jogo",
|
||||
description:
|
||||
"Aqui você vê a sala com pisos, sujeira (pontos marrons) e obstáculos (vasos). O robô aspirador (azul) precisa limpar toda a sujeira sem bater nos obstáculos!",
|
||||
}),
|
||||
|
||||
createToolboxStep({
|
||||
description:
|
||||
"Use blocos de Movimento (mover, virar), Sensores (ainda tem sujeira?, caminho bloqueado?), Lógica (se/senão, não) e Repetição (enquanto, repetir). Arraste-os para programar o robô!",
|
||||
}),
|
||||
|
||||
createWorkspaceStep({
|
||||
description:
|
||||
"Monte sua sequência de comandos aqui. Encaixe os blocos na ordem correta para fazer o robô se movimentar e limpar toda a sujeira da sala.",
|
||||
}),
|
||||
|
||||
createRunButtonStep({
|
||||
description:
|
||||
"Execute seu código! Você verá o robô se mover pela sala, limpando a sujeira passo a passo. Ouça o som do motor enquanto ele trabalha!",
|
||||
}),
|
||||
|
||||
createResetInfoStep({
|
||||
description:
|
||||
"Se o robô bater em obstáculos ou não limpar tudo, use o reset para recolocar a sujeira e tentar uma nova estratégia de limpeza.",
|
||||
}),
|
||||
|
||||
createPhaseSelectorStep({
|
||||
description:
|
||||
"O jogo tem 10 fases progressivas: desde linha reta simples até algoritmos complexos de espiral e zigue-zague usando variáveis!",
|
||||
}),
|
||||
|
||||
createPhaseInfoStep({
|
||||
description:
|
||||
"Acompanhe seu progresso, veja o objetivo da fase atual e quantos blocos você pode usar neste desafio.",
|
||||
}),
|
||||
|
||||
createHelpButtonStep({
|
||||
description:
|
||||
"Acesse este tour novamente clicando no botão de ajuda sempre que precisar relembrar os controles.",
|
||||
}),
|
||||
];
|
||||
|
||||
export const aspiradorTourOptions = defaultGameTourOptions;
|
||||
355
app/src/atividades/programacao/aspirador/game.js
Normal file
@@ -0,0 +1,355 @@
|
||||
import Phaser from "phaser";
|
||||
import { BaseGameScene } from "../../../shared/BaseGameScene.js";
|
||||
import { setupAspiradorAPI } from "./hooks/setupAspiradorAPI.js";
|
||||
import { validationSolution } from "./validation/validators.js";
|
||||
import { gameConfig } from "./config/config.js";
|
||||
import { ConstantesJogo, ConstantesAssets } from "./ui/constants.js";
|
||||
import { inicializarLayout } from "./ui/layout.js";
|
||||
|
||||
export class AspiradorScene extends BaseGameScene {
|
||||
constructor() {
|
||||
super("AspiradorScene");
|
||||
this.matrizAtiva = [];
|
||||
this.totalSujeiras = 0;
|
||||
this.sujeirasSprites = {};
|
||||
this.roboLogico = { col: 0, lin: 0, angulo: 0 };
|
||||
this.obstaculos = [];
|
||||
this.shouldValidate = false; // Flag para validação manual
|
||||
this.executionStopped = false; // Flag separada para parar loops
|
||||
}
|
||||
|
||||
create() {
|
||||
this.validatorFunc = (historico) => validationSolution(historico, this.configFase, gameConfig, this);
|
||||
|
||||
this.setupStandardController(
|
||||
() => setupAspiradorAPI(this, { animationSpeed: 250 }),
|
||||
this.validatorFunc
|
||||
);
|
||||
|
||||
inicializarLayout(this);
|
||||
this.montarFase();
|
||||
}
|
||||
|
||||
preload() {
|
||||
this.preloadGlobalAssets(); // Som global (BaseGameScene)
|
||||
|
||||
const chaves = ConstantesAssets.CHAVES;
|
||||
const paths = ConstantesAssets.PATHS;
|
||||
|
||||
this.load.image(chaves.PISO, paths.PISO);
|
||||
this.load.image(chaves.ASPIRADOR, paths.ASPIRADOR);
|
||||
this.load.image(chaves.SUJEIRA, paths.SUJEIRA);
|
||||
this.load.image(chaves.OBSTACULO1, paths.OBSTACULO1);
|
||||
this.load.image(chaves.OBSTACULO2, paths.OBSTACULO2);
|
||||
|
||||
// Carregar sons
|
||||
this.load.audio(chaves.SOM_POP, paths.SOM_POP);
|
||||
this.load.audio(chaves.SOM_BG, paths.SOM_BG);
|
||||
}
|
||||
|
||||
onReset() {
|
||||
this.isRunning = false;
|
||||
this.executionStopped = true; // Para qualquer execução pendente
|
||||
this.shouldValidate = false; // Limpa flag de validação pendente
|
||||
|
||||
// Para o som do aspirador imediatamente
|
||||
this.stopAudio(ConstantesAssets.CHAVES.SOM_BG);
|
||||
|
||||
if (this._timerTimeout) {
|
||||
clearTimeout(this._timerTimeout);
|
||||
this._timerTimeout = null;
|
||||
}
|
||||
this.montarFase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook Assíncrono de Sucesso: O robô comemora.
|
||||
*/
|
||||
async onSuccess() {
|
||||
if (this._timerTimeout) clearTimeout(this._timerTimeout);
|
||||
this.isRunning = false;
|
||||
|
||||
// Para o som do aspirador ao completar
|
||||
this.stopAudio(ConstantesAssets.CHAVES.SOM_BG);
|
||||
|
||||
return new Promise(resolve => {
|
||||
this.tweens.add({
|
||||
targets: this.aspiradorSprite,
|
||||
angle: '+=360',
|
||||
scale: 1,
|
||||
duration: 300,
|
||||
yoyo: true,
|
||||
ease: 'Back.easeOut',
|
||||
onComplete: resolve
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook Assíncrono de Falha: O robô treme em pane.
|
||||
*/
|
||||
async onFailure() {
|
||||
if (this._timerTimeout) clearTimeout(this._timerTimeout);
|
||||
this.isRunning = false;
|
||||
|
||||
// Para o som do aspirador ao falhar
|
||||
this.stopAudio(ConstantesAssets.CHAVES.SOM_BG);
|
||||
|
||||
return new Promise(resolve => {
|
||||
this.aspiradorSprite.setTint(0xff0000); // Fica vermelho de erro
|
||||
this.tweens.add({
|
||||
targets: this.aspiradorSprite,
|
||||
x: '+=5',
|
||||
yoyo: true,
|
||||
repeat: 10,
|
||||
duration: 50,
|
||||
onComplete: () => {
|
||||
this.aspiradorSprite.clearTint();
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
onBeforeRun() {
|
||||
this.isRunning = true;
|
||||
this.historico = [];
|
||||
this.shouldValidate = false; // Reset flag de validação manual
|
||||
this.executionStopped = false; // Reset flag de parada
|
||||
|
||||
// Som do aspirador em loop durante execução
|
||||
this.playAudio(ConstantesAssets.CHAVES.SOM_BG, { loop: true, volume: 0.5 });
|
||||
|
||||
// 1. Limpa resquícios de execuções anteriores
|
||||
if (this._timerTimeout) clearTimeout(this._timerTimeout);
|
||||
|
||||
// 2. Inicia o Cronômetro (Timeout)
|
||||
const limiteSegundos = this.configFase?.timeout || 30;
|
||||
|
||||
this._timerTimeout = setTimeout(() => {
|
||||
if (this.isRunning) {
|
||||
this.isRunning = false;
|
||||
|
||||
// Força a falha por tempo
|
||||
const msg = this.gameConfig?.mensagens?.timeoutExcedido || "Tempo esgotado!";
|
||||
this.handleFailure(msg);
|
||||
}
|
||||
}, limiteSegundos * 1000);
|
||||
}
|
||||
|
||||
montarFase() {
|
||||
if (this.aspiradorSprite) this.aspiradorSprite.destroy();
|
||||
|
||||
Object.values(this.sujeirasSprites).forEach(s => s.destroy());
|
||||
this.obstaculos.forEach(o => o.destroy());
|
||||
|
||||
this.sujeirasSprites = {};
|
||||
this.obstaculos = [];
|
||||
this.totalSujeiras = 0;
|
||||
|
||||
const cfg = this.configFase;
|
||||
this.matrizAtiva = JSON.parse(JSON.stringify(cfg.matriz));
|
||||
|
||||
for (let lin = 0; lin < this.matrizAtiva.length; lin++) {
|
||||
for (let col = 0; col < this.matrizAtiva[lin].length; col++) {
|
||||
let v = this.matrizAtiva[lin][col];
|
||||
let pX = col * 80 + 40;
|
||||
let pY = lin * 80 + 40;
|
||||
|
||||
if (v === 1) {
|
||||
// Aspirador
|
||||
this.aspiradorSprite = this.add.image(pX, pY, ConstantesAssets.CHAVES.ASPIRADOR).setDisplaySize(80, 80).setDepth(10);
|
||||
this.aspiradorSprite.angle = cfg.direcao || 0;
|
||||
this.roboLogico = { col, lin, angulo: cfg.direcao || 0 };
|
||||
}
|
||||
else if (v === 2) {
|
||||
// Sujeira
|
||||
this.sujeirasSprites[`${lin}-${col}`] = this.add.image(pX, pY, ConstantesAssets.CHAVES.SUJEIRA).setDisplaySize(64, 64).setDepth(5);
|
||||
this.totalSujeiras++;
|
||||
}
|
||||
else if (v === 3 || v === 4) {
|
||||
// Obstáculos normais (1x1) - Vasos ou Sofás
|
||||
const chave = v === 3 ? ConstantesAssets.CHAVES.OBSTACULO1 : ConstantesAssets.CHAVES.OBSTACULO2;
|
||||
this.obstaculos.push(this.add.image(pX, pY, chave).setDisplaySize(80, 80).setDepth(6));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- API para o Interpretador (Usado via ApiHelpers) ---
|
||||
|
||||
mover() {
|
||||
if (this.executionStopped) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// 1. Mapeamento estrito de direção (Sem trigonometria)
|
||||
const angNorm = ((this.roboLogico.angulo % 360) + 360) % 360;
|
||||
let dc = 0, dl = 0;
|
||||
|
||||
if (angNorm === 0) dl = -1; // Olhando para Cima (Subindo linha)
|
||||
else if (angNorm === 90) dc = 1; // Olhando para Direita (Avançando coluna)
|
||||
else if (angNorm === 180) dl = 1; // Olhando para Baixo (Descendo linha)
|
||||
else if (angNorm === 270) dc = -1; // Olhando para Esquerda (Voltando coluna)
|
||||
|
||||
let nC = this.roboLogico.col + dc;
|
||||
let nL = this.roboLogico.lin + dl;
|
||||
|
||||
// 2. Proteção: Se a frente está bloqueada, cancela o movimento (NÃO registra no histórico)
|
||||
if (this.checarBloqueio('FRENTE')) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// Só registra movimento no histórico se efetivamente vai se mover
|
||||
this.historico.push({ tipo: "mover", l: nL, c: nC });
|
||||
|
||||
// 3. Atualiza posição lógica e limpa sujeira ANTES da animação
|
||||
// Corrige condição de corrida com aindaTemSujeira()
|
||||
this.roboLogico.col = nC;
|
||||
this.roboLogico.lin = nL;
|
||||
|
||||
// Limpa a matriz IMEDIATAMENTE para que aindaTemSujeira() veja o estado correto
|
||||
if (this.matrizAtiva[nL] && this.matrizAtiva[nL][nC] === 2) {
|
||||
this.matrizAtiva[nL][nC] = 0;
|
||||
this.totalSujeiras--;
|
||||
|
||||
// Toca som pop ao coletar sujeira
|
||||
this.playAudio(ConstantesAssets.CHAVES.SOM_POP, { volume: 0.7 });
|
||||
|
||||
// INTERRUPÇÃO AUTOMÁTICA: Se limpou a última sujeira, para imediatamente
|
||||
if (this.totalSujeiras === 0) {
|
||||
this.executionStopped = true; // Para loops imediatamente
|
||||
this.shouldValidate = true;
|
||||
|
||||
this.stopAudio(ConstantesAssets.CHAVES.SOM_BG);
|
||||
|
||||
// Para o interpretador imediatamente mas sem marcar como parado pelo utilizador,
|
||||
// para que o fluxo de validação em handleValidation() não seja abortado.
|
||||
if (this.gameInterpreter) {
|
||||
this.gameInterpreter.stopInternal();
|
||||
}
|
||||
|
||||
// Agenda validação (deixa um tempo para tweens existentes terminarem)
|
||||
this.time.delayedCall(300, () => {
|
||||
if (this.shouldValidate && this.validatorFunc) {
|
||||
this.handleValidation(this.validatorFunc);
|
||||
this.shouldValidate = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise(res => {
|
||||
this.tweens.add({
|
||||
targets: this.aspiradorSprite,
|
||||
x: nC * 80 + 40,
|
||||
y: nL * 80 + 40,
|
||||
duration: 100, // 2x mais rápido (do 200ms para 100ms) para compensar som de 1s
|
||||
onComplete: () => {
|
||||
// Remove sprite visual após animação
|
||||
let key = `${nL}-${nC}`;
|
||||
if (this.sujeirasSprites[key]) {
|
||||
this.sujeirasSprites[key].destroy();
|
||||
delete this.sujeirasSprites[key];
|
||||
}
|
||||
res();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
virar(dir) {
|
||||
if (this.executionStopped) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
const incremento = (dir === 'DIREITA' ? 90 : -90);
|
||||
this.roboLogico.angulo += incremento;
|
||||
this.historico.push({ tipo: "virar", valor: dir });
|
||||
return new Promise(res => {
|
||||
this.tweens.add({ targets: this.aspiradorSprite, angle: this.roboLogico.angulo, duration: 100, onComplete: res });
|
||||
});
|
||||
}
|
||||
|
||||
checarBloqueio(sentido) {
|
||||
// Se execução parou, sempre retorna true (bloqueado) para evitar mais movimentos
|
||||
if (this.executionStopped) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 1. Calcula o ângulo que o sensor quer olhar
|
||||
let anguloBase = ((this.roboLogico.angulo % 360) + 360) % 360;
|
||||
let anguloTeste = anguloBase;
|
||||
|
||||
if (sentido === 'DIREITA') anguloTeste += 90;
|
||||
else if (sentido === 'ESQUERDA') anguloTeste -= 90;
|
||||
|
||||
const angNorm = ((anguloTeste % 360) + 360) % 360;
|
||||
|
||||
// 2. Projeta onde o sensor está "batendo"
|
||||
let dc = 0, dl = 0;
|
||||
if (angNorm === 0) dl = -1;
|
||||
else if (angNorm === 90) dc = 1;
|
||||
else if (angNorm === 180) dl = 1;
|
||||
else if (angNorm === 270) dc = -1;
|
||||
|
||||
const nC = this.roboLogico.col + dc;
|
||||
const nL = this.roboLogico.lin + dl;
|
||||
|
||||
// 3. Leitura DINÂMICA do tamanho real da sua fase atual
|
||||
const maxLinhas = this.matrizAtiva.length;
|
||||
const maxCols = this.matrizAtiva[0]?.length || 0;
|
||||
|
||||
let bloqueado = false;
|
||||
|
||||
// Se saiu dos limites da matriz, é parede invisível!
|
||||
if (nC < 0 || nC >= maxCols || nL < 0 || nL >= maxLinhas) {
|
||||
bloqueado = true;
|
||||
}
|
||||
// Se encontrou um obstáculo interno (vaso, sofá, etc)
|
||||
else if (this.matrizAtiva[nL] && this.matrizAtiva[nL][nC] >= 3) {
|
||||
bloqueado = true;
|
||||
}
|
||||
|
||||
this.historico.push({ tipo: "sensor", sentido, resultado: bloqueado });
|
||||
return bloqueado;
|
||||
}
|
||||
|
||||
aindaTemSujeira() {
|
||||
// Se já paramos a execução, sempre retorna false para sair dos loops
|
||||
if (this.executionStopped) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Usa o contador otimizado
|
||||
const tem = this.totalSujeiras > 0;
|
||||
|
||||
this.historico.push({ tipo: "sensor", acao: "check_sujeira", resultado: tem });
|
||||
return tem;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory para criar a configuração do Phaser para o jogo Aspirador.
|
||||
* @param {HTMLElement} elementoPai - Container DOM
|
||||
* @param {Object} configFaseAtual - Dados da fase selecionada
|
||||
* @returns {Object} Configuração do Phaser
|
||||
*/
|
||||
export const createGame = (elementoPai, configFaseAtual) => {
|
||||
const scene = new AspiradorScene();
|
||||
|
||||
return {
|
||||
type: Phaser.AUTO,
|
||||
width: ConstantesJogo.LARGURA_TELA || 800,
|
||||
height: ConstantesJogo.ALTURA_TELA || 560,
|
||||
backgroundColor: "#2d2d2d",
|
||||
parent: elementoPai,
|
||||
scale: { mode: Phaser.Scale.FIT, autoCenter: Phaser.Scale.CENTER_BOTH },
|
||||
scene: scene,
|
||||
callbacks: {
|
||||
preBoot: function (game) {
|
||||
game.registry.set("configFase", configFaseAtual);
|
||||
game.registry.set("gameConfig", gameConfig);
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import { ApiHelpers } from "../../../../interpreters/ApiHelpers.js";
|
||||
|
||||
export const setupAspiradorAPI = (scene, config) => {
|
||||
const delay = (config && config.animationSpeed) || 100;
|
||||
|
||||
return function(interpreter, globalScope) {
|
||||
// Registra como funções globais para o aluno usar mover() em vez de Robo.mover()
|
||||
ApiHelpers.registerFunction(interpreter, globalScope, "mover",
|
||||
ApiHelpers.createActionWrapper(scene, "mover", delay), true);
|
||||
|
||||
ApiHelpers.registerFunction(interpreter, globalScope, "virar",
|
||||
ApiHelpers.createActionWrapper(scene, "virar", delay), true);
|
||||
|
||||
ApiHelpers.registerFunction(interpreter, globalScope, "caminhoBloqueado",
|
||||
ApiHelpers.createConditionWrapper(scene, "checarBloqueio"), false);
|
||||
|
||||
ApiHelpers.registerFunction(interpreter, globalScope, "aindaTemSujeira",
|
||||
ApiHelpers.createConditionWrapper(scene, "aindaTemSujeira"), false);
|
||||
|
||||
// Necessário para o highlight do Blockly
|
||||
ApiHelpers.registerFunction(interpreter, globalScope, "highlightBlock",
|
||||
ApiHelpers.createHighlightWrapper(scene), false);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* @fileoverview Utility module for useAspiradorTour.js
|
||||
*
|
||||
* @module games.aspirador.hooks.useAspiradorTour
|
||||
*/
|
||||
|
||||
import { useGameTour } from "../../../../hooks/useGameTour";
|
||||
import { aspiradorTourSteps, aspiradorTourOptions } from "../config/tourSteps";
|
||||
|
||||
export const useAspiradorTour = () => {
|
||||
/**
|
||||
* Hook que retorna o controlador de tour para o jogo Aspirador.
|
||||
* Encapsula `useGameTour` com os passos e opções específicos.
|
||||
* @returns {Object} API do tour (start, stop, etc.)
|
||||
*/
|
||||
return useGameTour("aspirador", aspiradorTourSteps, aspiradorTourOptions);
|
||||
};
|
||||
39
app/src/atividades/programacao/aspirador/ui/constants.js
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* @fileoverview Utility module for constants.js
|
||||
*/
|
||||
|
||||
import imgPiso from "../assets/image/piso.png";
|
||||
import imgAspirador from "../assets/image/aspirador.png";
|
||||
import imgSujeira from "../assets/image/sujeira.png";
|
||||
import imgObstaculo1 from "../assets/image/obstaculo1.png";
|
||||
import imgObstaculo2 from "../assets//image/obstaculo2.png";
|
||||
|
||||
export const ConstantesJogo = {
|
||||
LARGURA_TELA: 800,
|
||||
ALTURA_TELA: 560,
|
||||
TILE_SIZE: 80,
|
||||
};
|
||||
|
||||
import sndPop from "../assets/sound/pop.mp3";
|
||||
import sndSomBg from "../assets/sound/bg_sound.mp3";
|
||||
|
||||
export const ConstantesAssets = {
|
||||
CHAVES: {
|
||||
PISO: "decoda_piso",
|
||||
ASPIRADOR: "decoda_aspirador",
|
||||
SUJEIRA: "decoda_sujeira",
|
||||
OBSTACULO1: "decoda_obstaculo1",
|
||||
OBSTACULO2: "decoda_obstaculo2",
|
||||
SOM_POP: "decoda_som_pop",
|
||||
SOM_BG: "decoda_som_bg"
|
||||
},
|
||||
PATHS: {
|
||||
PISO: imgPiso,
|
||||
ASPIRADOR: imgAspirador,
|
||||
SUJEIRA: imgSujeira,
|
||||
OBSTACULO1: imgObstaculo1,
|
||||
OBSTACULO2: imgObstaculo2,
|
||||
SOM_POP: sndPop,
|
||||
SOM_BG: sndSomBg
|
||||
}
|
||||
};
|
||||
22
app/src/atividades/programacao/aspirador/ui/layout.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import { ConstantesJogo, ConstantesAssets } from "./constants.js";
|
||||
|
||||
/**
|
||||
* Inicializa o layout base do jogo
|
||||
* @param {Phaser.Scene} scene
|
||||
*/
|
||||
export function inicializarLayout(scene) {
|
||||
const { LARGURA_TELA, ALTURA_TELA, TILE_SIZE } = ConstantesJogo;
|
||||
const { CHAVES } = ConstantesAssets;
|
||||
|
||||
// Piso
|
||||
const piso = scene.add.tileSprite(0, 0, LARGURA_TELA, ALTURA_TELA, CHAVES.PISO).setOrigin(0, 0);
|
||||
|
||||
// Grade de debug suave
|
||||
const grade = scene.add.graphics();
|
||||
grade.lineStyle(1, 0x000000, 0.05);
|
||||
for (let x = 0; x <= LARGURA_TELA; x += TILE_SIZE) grade.moveTo(x, 0).lineTo(x, ALTURA_TELA);
|
||||
for (let y = 0; y <= ALTURA_TELA; y += TILE_SIZE) grade.moveTo(0, y).lineTo(LARGURA_TELA, y);
|
||||
grade.strokePath();
|
||||
|
||||
return { piso, grade };
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { BaseGameValidator } from "../../../../shared/BaseGameValidator";
|
||||
|
||||
/**
|
||||
* Validador do jogo Aspirador (Atualizado para 10 Fases).
|
||||
* @class AspiradorValidator
|
||||
* @extends BaseGameValidator
|
||||
*/
|
||||
export class AspiradorValidator extends BaseGameValidator {
|
||||
validatePhase(history, config, gameConfig, sceneRef) {
|
||||
// 1. Sanity Check: O robô se mexeu?
|
||||
if (!history || history.length === 0) {
|
||||
return this.failure(gameConfig?.mensagens?.semMovimento || "O robô não saiu do lugar!");
|
||||
}
|
||||
|
||||
// 2. Validação Específica por ID de Fase (Regras Pedagógicas)
|
||||
if (config.id === 5) {
|
||||
// Bordas: deve ter virado sempre para o mesmo lado para fazer o contorno
|
||||
const viradas = history.filter(h => h.tipo === 'virar');
|
||||
const soUmLado = new Set(viradas.map(v => v.valor)).size === 1;
|
||||
if (!soUmLado) return this.failure(config.msgErroValidacao);
|
||||
}
|
||||
|
||||
if (config.id === 7 || config.id === 8) {
|
||||
// Verifica se o aluno usou o bloco de criar/incrementar variável
|
||||
const codigo = sceneRef?.currentCode || '';
|
||||
const temDeclaracao = /(?:var|let|const)\s+passos/.test(codigo);
|
||||
const temIncremento = /passos\s*(?:=\s*passos\s*\+\s*1|\+=\s*1|\+\+)/.test(codigo);
|
||||
|
||||
if (!temDeclaracao || !temIncremento) {
|
||||
return this.failure(config.msgErroIncremento || "Você precisa usar a variável e incrementá-la!");
|
||||
}
|
||||
}
|
||||
|
||||
if (config.id === 10) {
|
||||
// Zigue-Zague: deve ter virado para ambos os lados no padrão de espelho
|
||||
const viradas = history.filter(h => h.tipo === 'virar').map(h => h.valor);
|
||||
const usouAmbos = viradas.includes('ESQUERDA') && viradas.includes('DIREITA');
|
||||
if (!usouAmbos) return this.failure(config.msgErroValidacao);
|
||||
}
|
||||
|
||||
// 3. Check Final Universal: Sobrou sujeira?
|
||||
if (sceneRef && sceneRef.aindaTemSujeira()) {
|
||||
return this.failure("Ainda há sujeira na sala! O algoritmo não cobriu toda a área.");
|
||||
}
|
||||
|
||||
return this.success();
|
||||
}
|
||||
}
|
||||
|
||||
export function validationSolution(history, config, gameConfig, sceneRef) {
|
||||
const validator = new AspiradorValidator();
|
||||
return validator.validate(history, config, gameConfig, sceneRef);
|
||||
}
|
||||
84
app/src/atividades/programacao/automato/AutomatoGame.jsx
Normal file
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* @fileoverview React component for AutomatoGame.jsx
|
||||
*
|
||||
* @module games.automato.AutomatoGame
|
||||
*/
|
||||
|
||||
|
||||
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 { registerBlocks, generateDynamicToolbox } from "./blocks/blocks";
|
||||
import {
|
||||
GameStateProvider,
|
||||
useGameState,
|
||||
} from "../../../contexts/GameStateContext";
|
||||
import { useAutomatoTour } from "./hooks/useAutomatoTour";
|
||||
import { debugSolutions } from "./config/debugSolutions";
|
||||
import "shepherd.js/dist/css/shepherd.css";
|
||||
import "../../../styles/shepherd-theme.css";
|
||||
|
||||
function AutomatoGameContent() {
|
||||
const { isDebugMode, setFailureMessage } = useGameState();
|
||||
const { startTour } = useAutomatoTour();
|
||||
|
||||
useEffect(() => {
|
||||
registerBlocks();
|
||||
}, []);
|
||||
|
||||
const toolboxGenerator = useMemo(() => {
|
||||
|
||||
|
||||
return (allowedBlocks) => generateDynamicToolbox(allowedBlocks);
|
||||
}, []);
|
||||
|
||||
const renderEditor = () => {
|
||||
return (
|
||||
<BlocklyEditor
|
||||
toolboxGenerator={toolboxGenerator}
|
||||
debugSolutions={isDebugMode ? debugSolutions : null}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<GameBase
|
||||
gameFactory={createGame}
|
||||
gameConfig={gameConfig}
|
||||
onHelpClick={startTour}
|
||||
helpHandler={startTour}
|
||||
customFailureHandler={setFailureMessage}
|
||||
failureHandler={setFailureMessage}
|
||||
>
|
||||
<GameEditor>{renderEditor()}</GameEditor>
|
||||
</GameBase>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Conteúdo principal do jogo Automato.
|
||||
* Configura toolbox, registra blocos e injeta o `gameFactory` no `GameBase`.
|
||||
* @returns {JSX.Element} Conteúdo do editor e canvas do Automato
|
||||
*/
|
||||
|
||||
|
||||
export default function AutomatoGame() {
|
||||
return (
|
||||
<GameStateProvider gameConfig={gameConfig}>
|
||||
<AutomatoGameContent />
|
||||
</GameStateProvider>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Componente de página que provê o contexto para o jogo Automato
|
||||
* e monta `AutomatoGameContent` dentro do `GameStateProvider`.
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
|
||||
AutomatoGameContent.propTypes = {};
|
||||
AutomatoGame.propTypes = {};
|
||||
@@ -0,0 +1,520 @@
|
||||
/**
|
||||
* @fileoverview Utility module for integration.test.js
|
||||
*
|
||||
* @module games.automato.__tests__.integration.test
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { GameInterpreter } from "../../../../interpreters/GameInterpreter";
|
||||
import { setupAutomatoAPI } from "../hooks/interpreterSetup";
|
||||
import { validateSolution } from "../validation/validators";
|
||||
import { gameConfig } from "../config/config";
|
||||
|
||||
// Mock de soluções para teste
|
||||
const SOLUTIONS = {
|
||||
fase1: `
|
||||
moverParaFrente();
|
||||
moverParaFrente();
|
||||
`,
|
||||
fase1_fail: `
|
||||
moverParaFrente();
|
||||
`,
|
||||
fase2: `
|
||||
moverParaFrente();
|
||||
virarEsquerda();
|
||||
moverParaFrente();
|
||||
virarDireita();
|
||||
moverParaFrente();
|
||||
`,
|
||||
fase2_fail: `
|
||||
moverParaFrente();
|
||||
virarDireita();
|
||||
moverParaFrente();
|
||||
`,
|
||||
fase3: `
|
||||
while (!chegouNoAlvo()) {
|
||||
moverParaFrente();
|
||||
}
|
||||
`,
|
||||
fase3_fail: `
|
||||
moverParaFrente();
|
||||
moverParaFrente();
|
||||
`,
|
||||
fase4: `
|
||||
while (!chegouNoAlvo()) {
|
||||
moverParaFrente();
|
||||
virarEsquerda();
|
||||
moverParaFrente();
|
||||
virarDireita();
|
||||
}
|
||||
`,
|
||||
fase4_fail: `
|
||||
while (!chegouNoAlvo()) {
|
||||
moverParaFrente();
|
||||
virarEsquerda();
|
||||
virarDireita();
|
||||
}
|
||||
`,
|
||||
fase5: `
|
||||
moverParaFrente();
|
||||
moverParaFrente();
|
||||
virarEsquerda();
|
||||
while (!chegouNoAlvo()) {
|
||||
moverParaFrente();
|
||||
}
|
||||
`,
|
||||
fase5_fail: `
|
||||
while (!chegouNoAlvo()) {
|
||||
moverParaFrente();
|
||||
virarEsquerda();
|
||||
moverParaFrente();
|
||||
virarDireita();
|
||||
}
|
||||
`,
|
||||
fase6: `
|
||||
while (!chegouNoAlvo()) {
|
||||
if (haCaminho('frente')) {
|
||||
moverParaFrente();
|
||||
} else {
|
||||
virarEsquerda();
|
||||
}
|
||||
}
|
||||
`,
|
||||
fase6_fail: `
|
||||
while (!chegouNoAlvo()) {
|
||||
moverParaFrente();
|
||||
}
|
||||
`,
|
||||
fase7: `
|
||||
while (!chegouNoAlvo()) {
|
||||
if (haCaminho('esquerda')) {
|
||||
virarEsquerda();
|
||||
moverParaFrente();
|
||||
} else if (haCaminho('frente')) {
|
||||
moverParaFrente();
|
||||
} else {
|
||||
virarDireita();
|
||||
}
|
||||
}
|
||||
`,
|
||||
fase7_fail: `
|
||||
moverParaFrente();
|
||||
moverParaFrente();
|
||||
moverParaFrente();
|
||||
`,
|
||||
fase8: `
|
||||
while (!chegouNoAlvo()) {
|
||||
if (haCaminho('esquerda')) {
|
||||
virarEsquerda();
|
||||
moverParaFrente();
|
||||
} else if (haCaminho('frente')) {
|
||||
moverParaFrente();
|
||||
} else {
|
||||
virarDireita();
|
||||
}
|
||||
}
|
||||
`,
|
||||
fase8_fail: `
|
||||
while (!chegouNoAlvo()) {
|
||||
moverParaFrente();
|
||||
if (haCaminho("frente")) {
|
||||
virarEsquerda();
|
||||
}
|
||||
if (haCaminho("direita")) {
|
||||
virarDireita();
|
||||
}
|
||||
}
|
||||
`,
|
||||
fase9: `
|
||||
while (!chegouNoAlvo()) {
|
||||
if (haCaminho('esquerda')) {
|
||||
virarEsquerda();
|
||||
moverParaFrente();
|
||||
} else if (haCaminho('frente')) {
|
||||
moverParaFrente();
|
||||
} else {
|
||||
virarDireita();
|
||||
}
|
||||
}
|
||||
`,
|
||||
fase9_fail: `
|
||||
if (haCaminho("esquerda")) {
|
||||
moverParaFrente();
|
||||
} else {
|
||||
virarEsquerda();
|
||||
}
|
||||
`,
|
||||
fase10: `
|
||||
while (!chegouNoAlvo()) {
|
||||
if (haCaminho('esquerda')) {
|
||||
virarEsquerda();
|
||||
moverParaFrente();
|
||||
} else if (haCaminho('frente')) {
|
||||
moverParaFrente();
|
||||
} else if (haCaminho('direita')) {
|
||||
virarDireita();
|
||||
moverParaFrente();
|
||||
} else {
|
||||
virarDireita();
|
||||
virarDireita();
|
||||
}
|
||||
}
|
||||
`,
|
||||
fase10_fail: `
|
||||
if (haCaminho('esquerda')) {
|
||||
virarEsquerda();
|
||||
moverParaFrente();
|
||||
} else if (haCaminho('frente')) {
|
||||
moverParaFrente();
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
// Constantes para tipos de tile
|
||||
const TILE_TYPES = {
|
||||
PAREDE: 0,
|
||||
CAMINHO: 1,
|
||||
INICIO: 2,
|
||||
FIM: 3,
|
||||
};
|
||||
|
||||
// Constantes para direções
|
||||
const Direcao = {
|
||||
NORTE: 0,
|
||||
LESTE: 1,
|
||||
SUL: 2,
|
||||
OESTE: 3,
|
||||
};
|
||||
|
||||
// Cena de teste que replica os métodos reais da AutomatoScene
|
||||
class TestAutomatoScene {
|
||||
constructor(configFase) {
|
||||
this.mapa = configFase.mapa;
|
||||
this.historico = [];
|
||||
this.resultadoJogada = "em_andamento";
|
||||
this.configFase = configFase;
|
||||
|
||||
this.posicaoInicial = this.encontrarPosicao(TILE_TYPES.INICIO);
|
||||
this.posicaoFinal = this.encontrarPosicao(TILE_TYPES.FIM);
|
||||
this.posicaoJogador = { ...this.posicaoInicial };
|
||||
this.direcaoJogador = Direcao.LESTE;
|
||||
|
||||
this.pegmanSprite = {
|
||||
setPosition: vi.fn(),
|
||||
play: vi.fn(),
|
||||
setFrame: vi.fn(),
|
||||
on: vi.fn(),
|
||||
off: vi.fn(),
|
||||
x: 0,
|
||||
y: 0,
|
||||
};
|
||||
|
||||
this.tweens = {
|
||||
add: vi.fn((config) => {
|
||||
if (config.onComplete) {
|
||||
setTimeout(config.onComplete, 0);
|
||||
}
|
||||
return { stop: vi.fn() };
|
||||
}),
|
||||
};
|
||||
|
||||
this.sound = {
|
||||
play: vi.fn(),
|
||||
context: { state: "running", resume: vi.fn() },
|
||||
};
|
||||
|
||||
this.anims = {
|
||||
exists: vi.fn(() => true),
|
||||
};
|
||||
}
|
||||
|
||||
encontrarPosicao(tipo) {
|
||||
for (let y = 0; y < this.mapa.length; y++) {
|
||||
for (let x = 0; x < this.mapa[y].length; x++) {
|
||||
if (this.mapa[y][x] === tipo) return { x, y };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
atualizarVisualJogador() {
|
||||
if (this.posicaoJogador) {
|
||||
const TAMANHO_TILE = 50;
|
||||
const posX = this.posicaoJogador.x * TAMANHO_TILE + TAMANHO_TILE / 2;
|
||||
const posY = this.posicaoJogador.y * TAMANHO_TILE + TAMANHO_TILE / 2 - 6;
|
||||
this.pegmanSprite.setPosition(posX, posY);
|
||||
|
||||
const animacoesDirecao = [
|
||||
"pegman_idle_norte",
|
||||
"pegman_idle_leste",
|
||||
"pegman_idle_sul",
|
||||
"pegman_idle_oeste",
|
||||
];
|
||||
this.pegmanSprite.play(animacoesDirecao[this.direcaoJogador]);
|
||||
}
|
||||
}
|
||||
|
||||
async moverParaFrente() {
|
||||
if (this.resultadoJogada !== "em_andamento") {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// Proteção contra loops infinitos no teste
|
||||
if (this.historico.length > 500) {
|
||||
this.resultadoJogada = "falha";
|
||||
console.warn(
|
||||
"⚠️ Limite de ações atingido (500) - possível loop infinito",
|
||||
);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
let { x, y } = this.posicaoJogador;
|
||||
|
||||
if (this.direcaoJogador === Direcao.NORTE) y--;
|
||||
else if (this.direcaoJogador === Direcao.LESTE) x++;
|
||||
else if (this.direcaoJogador === Direcao.SUL) y++;
|
||||
else if (this.direcaoJogador === Direcao.OESTE) x--;
|
||||
|
||||
const proximoTile =
|
||||
this.mapa[y] && this.mapa[y][x] !== undefined ? this.mapa[y][x] : -1;
|
||||
|
||||
if (proximoTile === TILE_TYPES.PAREDE || proximoTile === -1) {
|
||||
this.animarFalha();
|
||||
this.resultadoJogada = "falha";
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
const TAMANHO_TILE = 50;
|
||||
const novaX = x * TAMANHO_TILE + TAMANHO_TILE / 2;
|
||||
const novaY = y * TAMANHO_TILE + TAMANHO_TILE / 2 - 6;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this.tweens.add({
|
||||
targets: this.pegmanSprite,
|
||||
x: novaX,
|
||||
y: novaY,
|
||||
duration: 0,
|
||||
ease: "Power2",
|
||||
onComplete: () => {
|
||||
this.posicaoJogador = { x, y };
|
||||
this.historico.push({
|
||||
action: "moverParaFrente",
|
||||
position: { ...this.posicaoJogador },
|
||||
direction: this.direcaoJogador,
|
||||
});
|
||||
this.atualizarVisualJogador();
|
||||
resolve();
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
animarFalha() {
|
||||
this.pegmanSprite.play("pegman_fall");
|
||||
}
|
||||
|
||||
async virarEsquerda() {
|
||||
const novaDirecao = (this.direcaoJogador + 3) % 4;
|
||||
await this.animarRotacao(novaDirecao);
|
||||
this.historico.push({
|
||||
action: "virarEsquerda",
|
||||
direction: this.direcaoJogador,
|
||||
});
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async virarDireita() {
|
||||
const novaDirecao = (this.direcaoJogador + 1) % 4;
|
||||
await this.animarRotacao(novaDirecao);
|
||||
this.historico.push({
|
||||
action: "virarDireita",
|
||||
direction: this.direcaoJogador,
|
||||
});
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async animarRotacao(novaDirecao) {
|
||||
this.direcaoJogador = novaDirecao;
|
||||
const nomesDirecoes = ["norte", "leste", "sul", "oeste"];
|
||||
const novaDirecaoNome = nomesDirecoes[novaDirecao];
|
||||
|
||||
this.pegmanSprite.play(`pegman_idle_${novaDirecaoNome}`);
|
||||
this.atualizarVisualJogador();
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
chegouNoAlvo() {
|
||||
// Para uso do código do usuário: sair do loop se falhou
|
||||
if (this.resultadoJogada === "falha") {
|
||||
return true;
|
||||
}
|
||||
|
||||
return this.verificarChegadaReal();
|
||||
}
|
||||
|
||||
// Método auxiliar para verificação real (usado pelo validator)
|
||||
verificarChegadaReal() {
|
||||
return (
|
||||
this.posicaoJogador.x === this.posicaoFinal.x &&
|
||||
this.posicaoJogador.y === this.posicaoFinal.y
|
||||
);
|
||||
}
|
||||
|
||||
haCaminho(direcaoRelativa) {
|
||||
let direcaoAbsoluta = this.direcaoJogador;
|
||||
if (direcaoRelativa === "esquerda") {
|
||||
direcaoAbsoluta = (this.direcaoJogador + 3) % 4;
|
||||
} else if (direcaoRelativa === "direita") {
|
||||
direcaoAbsoluta = (this.direcaoJogador + 1) % 4;
|
||||
}
|
||||
|
||||
let { x, y } = this.posicaoJogador;
|
||||
if (direcaoAbsoluta === Direcao.NORTE) y--;
|
||||
else if (direcaoAbsoluta === Direcao.LESTE) x++;
|
||||
else if (direcaoAbsoluta === Direcao.SUL) y++;
|
||||
else if (direcaoAbsoluta === Direcao.OESTE) x--;
|
||||
|
||||
const proximoTile =
|
||||
this.mapa[y] && this.mapa[y][x] !== undefined ? this.mapa[y][x] : -1;
|
||||
return proximoTile !== TILE_TYPES.PAREDE && proximoTile !== -1;
|
||||
}
|
||||
|
||||
highlightBlock() {}
|
||||
}
|
||||
|
||||
describe("Automato - Integração de Lógica (Código -> Validação)", () => {
|
||||
let scene;
|
||||
let interpreter;
|
||||
|
||||
const runFlow = async (code, phaseId) => {
|
||||
const configFase = gameConfig.fases.find((f) => f.id === phaseId);
|
||||
scene = new TestAutomatoScene(configFase);
|
||||
interpreter = new GameInterpreter({ stepDelay: 0, pauseExec: false });
|
||||
const api = setupAutomatoAPI(scene, { animationSpeed: 0 });
|
||||
await interpreter.executeCode(code, api);
|
||||
return validateSolution(scene.historico, configFase, gameConfig, scene);
|
||||
};
|
||||
|
||||
it("Fase 1: Deve aprovar solução correta (Primeiro Passo)", async () => {
|
||||
const result = await runFlow(SOLUTIONS.fase1, 1);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("Fase 1: Deve reprovar solução incompleta", async () => {
|
||||
const result = await runFlow(SOLUTIONS.fase1_fail, 1);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it("Fase 2: Deve aprovar solução correta (Primeira Curva)", async () => {
|
||||
const result = await runFlow(SOLUTIONS.fase2, 2);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("Fase 2: Deve reprovar direção errada", async () => {
|
||||
const result = await runFlow(SOLUTIONS.fase2_fail, 2);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it("Fase 3: Deve aprovar loop correto (Linha Reta)", async () => {
|
||||
const result = await runFlow(SOLUTIONS.fase3, 3);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("Fase 3: Deve reprovar sem loop", async () => {
|
||||
const result = await runFlow(SOLUTIONS.fase3_fail, 3);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it("Fase 4: Deve aprovar escadaria correta", async () => {
|
||||
const result = await runFlow(SOLUTIONS.fase4, 4);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("Fase 4: Deve reprovar movimento incompleto", async () => {
|
||||
const result = await runFlow(SOLUTIONS.fase4_fail, 4);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it("Fase 5: Deve aprovar torre correta", async () => {
|
||||
const result = await runFlow(SOLUTIONS.fase5, 5);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("Fase 5: Deve reprovar sequência errada", async () => {
|
||||
const result = await runFlow(SOLUTIONS.fase5_fail, 5);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it("Fase 6: Deve aprovar caminho com condicionais", async () => {
|
||||
const result = await runFlow(SOLUTIONS.fase6, 6);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("Fase 6: Deve reprovar caminho sem validação", async () => {
|
||||
const result = await runFlow(SOLUTIONS.fase6_fail, 6);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it("Fase 7: Deve aprovar labirinto ramificado", async () => {
|
||||
const result = await runFlow(SOLUTIONS.fase7, 7);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("Fase 7: Deve reprovar prioridade errada", async () => {
|
||||
const result = await runFlow(SOLUTIONS.fase7_fail, 7);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it("Fase 8: Deve aprovar labirinto complexo", async () => {
|
||||
const result = await runFlow(SOLUTIONS.fase8, 8);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("Fase 8: Deve reprovar lógica incompleta", async () => {
|
||||
const result = await runFlow(SOLUTIONS.fase8_fail, 8);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it("Fase 9: Deve aprovar labirinto desafiador", async () => {
|
||||
const result = await runFlow(SOLUTIONS.fase9, 9);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("Fase 9: Deve reprovar lógica simplificada demais", async () => {
|
||||
const result = await runFlow(SOLUTIONS.fase9_fail, 9);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it("Fase 10: Deve aprovar labirinto final", async () => {
|
||||
const result = await runFlow(SOLUTIONS.fase10, 10);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("Fase 10: Deve reprovar lógica incompleta no final", async () => {
|
||||
const result = await runFlow(SOLUTIONS.fase10_fail, 10);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it("Deve detectar colisão com parede", async () => {
|
||||
const badCode = `virarEsquerda(); moverParaFrente();`;
|
||||
const result = await runFlow(badCode, 1);
|
||||
expect(result.success).toBe(false);
|
||||
expect(scene.resultadoJogada).toBe("falha");
|
||||
});
|
||||
|
||||
it("Deve verificar chegouNoAlvo() corretamente", async () => {
|
||||
const configFase = gameConfig.fases.find((f) => f.id === 1);
|
||||
scene = new TestAutomatoScene(configFase);
|
||||
expect(scene.chegouNoAlvo()).toBe(false);
|
||||
scene.posicaoJogador = { ...scene.posicaoFinal };
|
||||
expect(scene.chegouNoAlvo()).toBe(true);
|
||||
});
|
||||
|
||||
it("Deve verificar haCaminho() corretamente", async () => {
|
||||
const configFase = gameConfig.fases.find((f) => f.id === 1);
|
||||
scene = new TestAutomatoScene(configFase);
|
||||
expect(scene.haCaminho("frente")).toBe(true);
|
||||
expect(scene.haCaminho("esquerda")).toBe(false);
|
||||
});
|
||||
}, 60000);
|
||||
BIN
app/src/atividades/programacao/automato/assets/marker.png
Normal file
|
After Width: | Height: | Size: 859 B |
BIN
app/src/atividades/programacao/automato/assets/pegman.png
Normal file
|
After Width: | Height: | Size: 9.8 KiB |
BIN
app/src/atividades/programacao/automato/assets/tiles_pegman.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
406
app/src/atividades/programacao/automato/blocks/blocks.js
Normal file
@@ -0,0 +1,406 @@
|
||||
/**
|
||||
* @fileoverview Utility module for blocks.js
|
||||
*
|
||||
* @module games.automato.blocks.blocks
|
||||
*/
|
||||
|
||||
import * as Blockly from "blockly/core";
|
||||
import { javascriptGenerator } from "blockly/javascript";
|
||||
|
||||
export const registerBlocks = () => {
|
||||
defineBlocks();
|
||||
defineGenerators();
|
||||
};
|
||||
|
||||
/**
|
||||
* Registra os blocos e geradores do Autômato no Blockly.
|
||||
* Chamado durante inicialização do editor para expor os blocos customizados.
|
||||
* @returns {void}
|
||||
*/
|
||||
|
||||
export const generateDynamicToolbox = (allowedBlocks = []) => {
|
||||
const blockDefinitions = {
|
||||
moveForward: {
|
||||
kind: "block",
|
||||
type: "automato_move_forward",
|
||||
},
|
||||
turnLeft: {
|
||||
kind: "block",
|
||||
type: "automato_turn_left",
|
||||
},
|
||||
turnRight: {
|
||||
kind: "block",
|
||||
type: "automato_turn_right",
|
||||
},
|
||||
automato_if: {
|
||||
kind: "block",
|
||||
type: "automato_if",
|
||||
},
|
||||
automato_ifElse: {
|
||||
kind: "block",
|
||||
type: "automato_ifElse",
|
||||
},
|
||||
isPathAhead: {
|
||||
kind: "block",
|
||||
type: "automato_is_path_ahead",
|
||||
},
|
||||
isPathLeft: {
|
||||
kind: "block",
|
||||
type: "automato_is_path_left",
|
||||
},
|
||||
isPathRight: {
|
||||
kind: "block",
|
||||
type: "automato_is_path_right",
|
||||
},
|
||||
automato_repeat_until_goal: {
|
||||
kind: "block",
|
||||
type: "automato_repeat_until_goal",
|
||||
},
|
||||
};
|
||||
|
||||
const toolboxContents = {
|
||||
kind: "categoryToolbox",
|
||||
contents: [
|
||||
{
|
||||
kind: "category",
|
||||
name: "Movimento",
|
||||
colour: "#4CAF50",
|
||||
contents: [],
|
||||
cssConfig: {
|
||||
container: "movimento",
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "category",
|
||||
name: "Repetição",
|
||||
colour: "#FF9800",
|
||||
contents: [],
|
||||
cssConfig: {
|
||||
container: "repeticao",
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "category",
|
||||
name: "Lógica",
|
||||
colour: "#2196F3",
|
||||
contents: [],
|
||||
cssConfig: {
|
||||
container: "logica",
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "category",
|
||||
name: "Sensores",
|
||||
colour: "#9C27B0",
|
||||
contents: [],
|
||||
cssConfig: {
|
||||
container: "sensores",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
allowedBlocks.forEach((blockId) => {
|
||||
const blockDef = blockDefinitions[blockId];
|
||||
if (!blockDef) return;
|
||||
|
||||
if (["moveForward", "turnLeft", "turnRight"].includes(blockId)) {
|
||||
toolboxContents.contents[0].contents.push(blockDef);
|
||||
} else if (["automato_repeat_until_goal"].includes(blockId)) {
|
||||
toolboxContents.contents[1].contents.push(blockDef);
|
||||
} else if (["automato_if", "automato_ifElse"].includes(blockId)) {
|
||||
toolboxContents.contents[2].contents.push(blockDef);
|
||||
} else if (["isPathAhead", "isPathLeft", "isPathRight"].includes(blockId)) {
|
||||
toolboxContents.contents[3].contents.push(blockDef);
|
||||
}
|
||||
});
|
||||
|
||||
toolboxContents.contents = toolboxContents.contents.filter(
|
||||
(category) => category.contents && category.contents.length > 0,
|
||||
);
|
||||
|
||||
return toolboxContents;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gera a toolbox dinâmica contendo apenas os blocos permitidos para a fase.
|
||||
* @param {Array<string>} [allowedBlocks=[]] - Identificadores de blocos habilitados
|
||||
* @returns {Object} Estrutura de toolbox compatível com Blockly
|
||||
*/
|
||||
|
||||
const defineBlocks = () => {
|
||||
// Bloco: Mover Frente
|
||||
Blockly.Blocks["automato_move_forward"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput().appendField("mover a frente");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour("#4CAF50");
|
||||
this.setTooltip("Move o autômato uma posição para frente");
|
||||
this.setHelpUrl("");
|
||||
},
|
||||
};
|
||||
|
||||
// Bloco: Virar à Esquerda
|
||||
Blockly.Blocks["automato_turn_left"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput().appendField("↺ virar à esquerda");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour("#9C27B0");
|
||||
this.setTooltip("Vira o autômato 90° para a esquerda");
|
||||
this.setHelpUrl("");
|
||||
},
|
||||
};
|
||||
|
||||
// Bloco: Virar à Direita
|
||||
Blockly.Blocks["automato_turn_right"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput().appendField("↻ virar à direita");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour("#9C27B0");
|
||||
this.setTooltip("Vira o autômato 90° para a direita");
|
||||
this.setHelpUrl("");
|
||||
},
|
||||
};
|
||||
|
||||
// Bloco: Se (condicional simples)
|
||||
Blockly.Blocks["automato_if"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput()
|
||||
.appendField("se")
|
||||
.appendField(
|
||||
new Blockly.FieldDropdown([
|
||||
["há caminho à frente", "isPathAhead"],
|
||||
["há caminho à esquerda", "isPathLeft"],
|
||||
["há caminho à direita", "isPathRight"],
|
||||
]),
|
||||
"DIR",
|
||||
);
|
||||
this.appendStatementInput("DO").setCheck(null).appendField("faça");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour("#2196F3");
|
||||
this.setTooltip("Execute comandos se a condição for verdadeira");
|
||||
this.setHelpUrl("");
|
||||
},
|
||||
};
|
||||
|
||||
// Bloco: Se/Senão (condicional com else)
|
||||
Blockly.Blocks["automato_ifElse"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput()
|
||||
.appendField("se")
|
||||
.appendField(
|
||||
new Blockly.FieldDropdown([
|
||||
["há caminho à frente", "isPathAhead"],
|
||||
["há caminho à esquerda", "isPathLeft"],
|
||||
["há caminho à direita", "isPathRight"],
|
||||
]),
|
||||
"DIR",
|
||||
);
|
||||
this.appendStatementInput("DO").setCheck(null).appendField("faça");
|
||||
this.appendStatementInput("ELSE").setCheck(null).appendField("senão");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour("#2196F3");
|
||||
this.setTooltip("Execute comandos diferentes dependendo da condição");
|
||||
this.setHelpUrl("");
|
||||
},
|
||||
};
|
||||
|
||||
// Bloco: Verificar se há caminho à frente
|
||||
Blockly.Blocks["automato_is_path_ahead"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput().appendField("👁️ há caminho à frente?");
|
||||
this.setOutput(true, "Boolean");
|
||||
this.setColour("#2196F3");
|
||||
this.setTooltip("Verifica se há um caminho livre à frente do autômato");
|
||||
this.setHelpUrl("");
|
||||
},
|
||||
};
|
||||
|
||||
// Bloco: Verificar se há caminho à esquerda
|
||||
Blockly.Blocks["automato_is_path_left"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput().appendField("há caminho à esquerda?");
|
||||
this.setOutput(true, "Boolean");
|
||||
this.setColour("#2196F3");
|
||||
this.setTooltip("Verifica se há um caminho livre à esquerda do autômato");
|
||||
this.setHelpUrl("");
|
||||
},
|
||||
};
|
||||
|
||||
// Bloco: Verificar se há caminho à direita
|
||||
Blockly.Blocks["automato_is_path_right"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput().appendField("há caminho à direita?");
|
||||
this.setOutput(true, "Boolean");
|
||||
this.setColour("#2196F3");
|
||||
this.setTooltip("Verifica se há um caminho livre à direita do autômato");
|
||||
this.setHelpUrl("");
|
||||
},
|
||||
};
|
||||
|
||||
// Bloco: Repita até o objetivo
|
||||
Blockly.Blocks["automato_repeat_until_goal"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput().appendField("repita até o objetivo");
|
||||
this.appendStatementInput("DO").setCheck(null).appendField("fazer");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour("#FF9800");
|
||||
this.setTooltip("Repete as ações até o objetivo ser alcançado");
|
||||
this.setHelpUrl("");
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const defineGenerators = () => {
|
||||
javascriptGenerator.STATEMENT_PREFIX = "highlightBlock(%1);\n";
|
||||
javascriptGenerator.addReservedWords("highlightBlock");
|
||||
|
||||
// Gerador: Mover Frente
|
||||
javascriptGenerator.forBlock["automato_move_forward"] = function () {
|
||||
return "moverParaFrente();\n";
|
||||
};
|
||||
|
||||
// Gerador: Virar à Esquerda
|
||||
javascriptGenerator.forBlock["automato_turn_left"] = function () {
|
||||
return "virarEsquerda();\n";
|
||||
};
|
||||
|
||||
// Gerador: Virar à Direita
|
||||
javascriptGenerator.forBlock["automato_turn_right"] = function () {
|
||||
return "virarDireita();\n";
|
||||
};
|
||||
|
||||
// Gerador: Se (condicional simples)
|
||||
javascriptGenerator.forBlock["automato_if"] = function (block) {
|
||||
// Pega o valor do dropdown: 'isPathAhead', 'isPathLeft', ou 'isPathRight'
|
||||
const direcaoDropdown = block.getFieldValue("DIR");
|
||||
|
||||
// Mapeia o valor do dropdown para o argumento da nossa função 'haCaminho'
|
||||
const mapaDeDirecao = {
|
||||
isPathAhead: '"frente"',
|
||||
isPathLeft: '"esquerda"',
|
||||
isPathRight: '"direita"',
|
||||
};
|
||||
const argumentoFuncao = mapaDeDirecao[direcaoDropdown];
|
||||
|
||||
const statements = javascriptGenerator.statementToCode(block, "DO");
|
||||
// Gera o código correto: if (haCaminho("frente")) { ... }
|
||||
return `if (haCaminho(${argumentoFuncao})) {\n${statements}}\n`;
|
||||
};
|
||||
|
||||
// Gerador: Se/Senão (condicional com else)
|
||||
javascriptGenerator.forBlock["automato_ifElse"] = function (block) {
|
||||
const direcaoDropdown = block.getFieldValue("DIR");
|
||||
|
||||
const mapaDeDirecao = {
|
||||
isPathAhead: '"frente"',
|
||||
isPathLeft: '"esquerda"',
|
||||
isPathRight: '"direita"',
|
||||
};
|
||||
const argumentoFuncao = mapaDeDirecao[direcaoDropdown];
|
||||
|
||||
const statementsIf = javascriptGenerator.statementToCode(block, "DO");
|
||||
const statementsElse = javascriptGenerator.statementToCode(block, "ELSE");
|
||||
// Gera o código correto: if (haCaminho("frente")) { ... } else { ... }
|
||||
return `if (haCaminho(${argumentoFuncao})) {\n${statementsIf}} else {\n${statementsElse}}\n`;
|
||||
};
|
||||
|
||||
// Gerador: Verificar se há caminho à frente / esquerda / direita
|
||||
javascriptGenerator.forBlock["automato_is_path_ahead"] = () => [
|
||||
'haCaminho("frente")',
|
||||
javascriptGenerator.ORDER_FUNCTION_CALL,
|
||||
];
|
||||
javascriptGenerator.forBlock["automato_is_path_left"] = () => [
|
||||
'haCaminho("esquerda")',
|
||||
javascriptGenerator.ORDER_FUNCTION_CALL,
|
||||
];
|
||||
javascriptGenerator.forBlock["automato_is_path_right"] = () => [
|
||||
'haCaminho("direita")',
|
||||
javascriptGenerator.ORDER_FUNCTION_CALL,
|
||||
];
|
||||
|
||||
// Gerador: Repita até o objetivo
|
||||
javascriptGenerator.forBlock["automato_repeat_until_goal"] = function (
|
||||
block,
|
||||
) {
|
||||
const statements = javascriptGenerator.statementToCode(block, "DO");
|
||||
return `while (!chegouNoAlvo()) {\n${statements}}\n`;
|
||||
};
|
||||
};
|
||||
|
||||
// Configuração da toolbox padrão (todos os blocos disponíveis)
|
||||
export const automatoToolbox = {
|
||||
kind: "categoryToolbox",
|
||||
contents: [
|
||||
{
|
||||
kind: "category",
|
||||
name: "Movimento",
|
||||
colour: "#4CAF50",
|
||||
contents: [
|
||||
{
|
||||
kind: "block",
|
||||
type: "automato_move_forward",
|
||||
},
|
||||
{
|
||||
kind: "block",
|
||||
type: "automato_turn_left",
|
||||
},
|
||||
{
|
||||
kind: "block",
|
||||
type: "automato_turn_right",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "category",
|
||||
name: "Repetição",
|
||||
colour: "#FF9800",
|
||||
contents: [
|
||||
{
|
||||
kind: "block",
|
||||
type: "automato_repeat_until_goal",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "category",
|
||||
name: "Lógica",
|
||||
colour: "#2196F3",
|
||||
contents: [
|
||||
{
|
||||
kind: "block",
|
||||
type: "automato_if",
|
||||
},
|
||||
{
|
||||
kind: "block",
|
||||
type: "automato_ifElse",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "category",
|
||||
name: "Sensores",
|
||||
colour: "#9C27B0",
|
||||
contents: [
|
||||
{
|
||||
kind: "block",
|
||||
type: "automato_is_path_ahead",
|
||||
},
|
||||
{
|
||||
kind: "block",
|
||||
type: "automato_is_path_left",
|
||||
},
|
||||
{
|
||||
kind: "block",
|
||||
type: "automato_is_path_right",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
293
app/src/atividades/programacao/automato/config/config.js
Normal file
@@ -0,0 +1,293 @@
|
||||
/**
|
||||
* @fileoverview Utility module for config.js
|
||||
*
|
||||
* @module games.automato.config.config
|
||||
*/
|
||||
|
||||
export const gameConfig = {
|
||||
gameId: "automato",
|
||||
gameName: "Autômato",
|
||||
type: "blocks",
|
||||
icon: "🤖",
|
||||
thumbnail: "/images/atividades/programacao/automato-thumbnail.png",
|
||||
descricao: "Aprenda programação navegando por labirintos com blocos Blockly",
|
||||
categoria: "Lógica",
|
||||
tempoEstimado: "15-30 min",
|
||||
dificuldade: "Iniciante",
|
||||
conceitos: [
|
||||
"Sequências",
|
||||
"Loops/Repetição",
|
||||
"Condicionais",
|
||||
"Estruturas de controle",
|
||||
],
|
||||
route: "/atividades/programacao/automato",
|
||||
component: "AutomatoGame",
|
||||
obetivos: [
|
||||
"Entender sequências de comandos",
|
||||
"Usar loops para otimizar código",
|
||||
"Aplicar condicionais para tomada de decisão",
|
||||
"Resolver problemas de navegação",
|
||||
],
|
||||
metadata: {
|
||||
ultimaAtualizacao: "2026-08-01",
|
||||
versao: "1.1.0",
|
||||
},
|
||||
mensagens: {
|
||||
naoChegou: "Você não chegou ao objetivo! Verifique seu caminho.",
|
||||
bateuNaParede:
|
||||
"O Pegman bateu na parede! Verifique os comandos de movimento.",
|
||||
erroGeral: "Algo deu errado durante a execução. Verifique seu código.",
|
||||
sucessoGenerico: "Parabéns! Você completou o desafio!",
|
||||
timeoutExcedido:
|
||||
"O tempo de execução foi excedido. Verifique se não há loops infinitos.",
|
||||
},
|
||||
fases: [
|
||||
{
|
||||
id: 1,
|
||||
nome: "Primeiro Passo",
|
||||
descricao: "Aprenda a mover para frente",
|
||||
maxBlocks: 2,
|
||||
startPosition: { x: 2, y: 4 },
|
||||
allowedBlocks: ["moveForward"],
|
||||
mapa: [
|
||||
[0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 2, 1, 3, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0],
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
nome: "Primeira Curva",
|
||||
descricao: "Aprenda a virar à direita e a esquerda",
|
||||
maxBlocks: 5,
|
||||
startPosition: { x: 2, y: 4 },
|
||||
allowedBlocks: ["moveForward", "turnLeft", "turnRight"],
|
||||
mapa: [
|
||||
[0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 1, 3, 0, 0, 0],
|
||||
[0, 0, 2, 1, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0],
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
nome: "Linha Reta",
|
||||
descricao: "Use repetição para economizar blocos",
|
||||
maxBlocks: 2,
|
||||
startPosition: { x: 1, y: 4 },
|
||||
allowedBlocks: [
|
||||
"moveForward",
|
||||
"turnLeft",
|
||||
"turnRight",
|
||||
"automato_repeat_until_goal",
|
||||
],
|
||||
mapa: [
|
||||
[0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 2, 1, 1, 1, 1, 3, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0],
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
nome: "Escadaria",
|
||||
descricao: "Navegue pela escadaria diagonal",
|
||||
maxBlocks: 5,
|
||||
startPosition: { x: 1, y: 6 },
|
||||
allowedBlocks: [
|
||||
"moveForward",
|
||||
"turnLeft",
|
||||
"turnRight",
|
||||
"automato_repeat_until_goal",
|
||||
],
|
||||
mapa: [
|
||||
[0, 0, 0, 0, 0, 0, 0, 1],
|
||||
[0, 0, 0, 0, 0, 0, 1, 1],
|
||||
[0, 0, 0, 0, 0, 3, 1, 0],
|
||||
[0, 0, 0, 0, 1, 1, 0, 0],
|
||||
[0, 0, 0, 1, 1, 0, 0, 0],
|
||||
[0, 0, 1, 1, 0, 0, 0, 0],
|
||||
[0, 2, 1, 0, 0, 0, 0, 0],
|
||||
[1, 1, 0, 0, 0, 0, 0, 0],
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
nome: "Torre",
|
||||
descricao: "Suba a torre",
|
||||
maxBlocks: 5,
|
||||
startPosition: { x: 3, y: 6 },
|
||||
allowedBlocks: [
|
||||
"moveForward",
|
||||
"turnLeft",
|
||||
"turnRight",
|
||||
"automato_repeat_until_goal",
|
||||
],
|
||||
mapa: [
|
||||
[0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 3, 0, 0],
|
||||
[0, 0, 0, 0, 0, 1, 0, 0],
|
||||
[0, 0, 0, 0, 0, 1, 0, 0],
|
||||
[0, 0, 0, 0, 0, 1, 0, 0],
|
||||
[0, 0, 0, 0, 0, 1, 0, 0],
|
||||
[0, 0, 0, 2, 1, 1, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0],
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
nome: "Caminho em Bloco",
|
||||
descricao: "Use condicionais - verifique se há caminho à frente",
|
||||
maxBlocks: 5,
|
||||
startPosition: { x: 1, y: 6 },
|
||||
allowedBlocks: [
|
||||
"moveForward",
|
||||
"turnLeft",
|
||||
"turnRight",
|
||||
"automato_repeat_until_goal",
|
||||
"automato_if",
|
||||
],
|
||||
mapa: [
|
||||
[0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 1, 1, 1, 1, 1, 0, 0],
|
||||
[0, 1, 0, 0, 0, 1, 0, 0],
|
||||
[0, 1, 1, 3, 0, 1, 0, 0],
|
||||
[0, 0, 0, 0, 0, 1, 0, 0],
|
||||
[0, 2, 1, 1, 1, 1, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0],
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
nome: "Labirinto Ramificado",
|
||||
descricao:
|
||||
"Navegue por caminhos que se ramificam - use condicionais gerais",
|
||||
maxBlocks: 10,
|
||||
startPosition: { x: 1, y: 2 },
|
||||
allowedBlocks: [
|
||||
"moveForward",
|
||||
"turnLeft",
|
||||
"turnRight",
|
||||
"automato_repeat_until_goal",
|
||||
"automato_if",
|
||||
],
|
||||
mapa: [
|
||||
[0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 1, 1, 0],
|
||||
[0, 2, 1, 1, 1, 1, 0, 0],
|
||||
[0, 0, 0, 0, 0, 1, 1, 0],
|
||||
[0, 1, 1, 3, 0, 1, 0, 0],
|
||||
[0, 1, 0, 1, 0, 1, 0, 0],
|
||||
[0, 1, 1, 1, 1, 1, 1, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0],
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
nome: "Caminho Complexo",
|
||||
descricao: "Um labirinto mais desafiador",
|
||||
maxBlocks: 7,
|
||||
startPosition: { x: 1, y: 6 },
|
||||
allowedBlocks: [
|
||||
"moveForward",
|
||||
"turnLeft",
|
||||
"turnRight",
|
||||
"automato_repeat_until_goal",
|
||||
"automato_if",
|
||||
],
|
||||
mapa: [
|
||||
[0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 1, 1, 1, 1, 0, 0, 0],
|
||||
[0, 1, 0, 0, 1, 1, 0, 0],
|
||||
[0, 1, 1, 1, 0, 1, 0, 0],
|
||||
[0, 0, 0, 1, 0, 1, 0, 0],
|
||||
[0, 2, 1, 1, 0, 3, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0],
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
nome: "Labirinto Avançado",
|
||||
descricao: "Use todas suas habilidades - agora com condicionais if/else",
|
||||
maxBlocks: 10,
|
||||
startPosition: { x: 5, y: 6 },
|
||||
allowedBlocks: [
|
||||
"moveForward",
|
||||
"turnLeft",
|
||||
"turnRight",
|
||||
"automato_repeat_until_goal",
|
||||
"automato_if",
|
||||
"automato_ifElse",
|
||||
],
|
||||
mapa: [
|
||||
[0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 1, 1, 1, 1, 1, 0, 0],
|
||||
[0, 0, 1, 0, 0, 0, 0, 0],
|
||||
[3, 1, 1, 1, 1, 1, 1, 0],
|
||||
[0, 1, 0, 1, 0, 1, 1, 0],
|
||||
[1, 1, 1, 1, 1, 0, 1, 0],
|
||||
[0, 1, 0, 1, 0, 2, 1, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0],
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
nome: "Desafio Final",
|
||||
descricao: "O último desafio - use tudo que aprendeu!",
|
||||
maxBlocks: 10,
|
||||
startPosition: { x: 1, y: 6 },
|
||||
allowedBlocks: [
|
||||
"moveForward",
|
||||
"turnLeft",
|
||||
"turnRight",
|
||||
"automato_repeat_until_goal",
|
||||
"automato_if",
|
||||
"automato_ifElse",
|
||||
],
|
||||
mapa: [
|
||||
[0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 1, 1, 0, 3, 0, 1, 0],
|
||||
[0, 1, 1, 0, 1, 1, 1, 0],
|
||||
[0, 1, 0, 1, 0, 1, 0, 0],
|
||||
[0, 1, 1, 1, 1, 1, 1, 0],
|
||||
[0, 0, 0, 1, 0, 0, 1, 0],
|
||||
[0, 2, 1, 1, 1, 0, 1, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0],
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
SquareType: {
|
||||
WALL: 0,
|
||||
OPEN: 1,
|
||||
START: 2,
|
||||
FINISH: 3,
|
||||
},
|
||||
|
||||
DirectionType: {
|
||||
NORTH: 0,
|
||||
EAST: 1,
|
||||
SOUTH: 2,
|
||||
WEST: 3,
|
||||
},
|
||||
|
||||
BlockColors: {
|
||||
MOVEMENT: 290, // Roxo para movimento
|
||||
LOOPS: 120, // Verde para loops
|
||||
LOGIC: 210, // Azul para lógica
|
||||
},
|
||||
};
|
||||
400
app/src/atividades/programacao/automato/config/debugSolutions.js
Normal file
@@ -0,0 +1,400 @@
|
||||
/**
|
||||
* @fileoverview Utility module for debugSolutions.js
|
||||
*
|
||||
* @module games.automato.config.debugSolutions
|
||||
*/
|
||||
|
||||
export const debugSolutions = {
|
||||
1: {
|
||||
blocks: {
|
||||
languageVersion: 0,
|
||||
blocks: [
|
||||
{
|
||||
type: "automato_move_forward",
|
||||
id: "`P+9hPVc7g2DJ0q16RK,",
|
||||
x: 13,
|
||||
y: 13,
|
||||
next: {
|
||||
block: {
|
||||
type: "automato_move_forward",
|
||||
id: "zZKB=Au92}qd~WsjdKY5",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
2: {
|
||||
blocks: {
|
||||
languageVersion: 0,
|
||||
blocks: [
|
||||
{
|
||||
type: "automato_move_forward",
|
||||
id: "%tpiI~Q)QvKV*j:Utvj#",
|
||||
x: 36,
|
||||
y: 31,
|
||||
next: {
|
||||
block: {
|
||||
type: "automato_turn_left",
|
||||
id: "i9kNT4,rQx%kC*BG+,k^",
|
||||
next: {
|
||||
block: {
|
||||
type: "automato_move_forward",
|
||||
id: "Wf}J1L`vrB#0),u6I^pM",
|
||||
next: {
|
||||
block: {
|
||||
type: "automato_turn_right",
|
||||
id: "4;eo@)^654.383-EkN9-",
|
||||
next: {
|
||||
block: {
|
||||
type: "automato_move_forward",
|
||||
id: "neew[~/eKgbzlC[+oi*Q",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
3: {
|
||||
blocks: {
|
||||
languageVersion: 0,
|
||||
blocks: [
|
||||
{
|
||||
type: "automato_repeat_until_goal",
|
||||
id: "`O0p7RA@Cv99swNQ+M1p",
|
||||
x: 13,
|
||||
y: 13,
|
||||
inputs: {
|
||||
DO: {
|
||||
block: {
|
||||
type: "automato_move_forward",
|
||||
id: "HThd+E5B?r2KAGp54_?l",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
4: {
|
||||
blocks: {
|
||||
languageVersion: 0,
|
||||
blocks: [
|
||||
{
|
||||
type: "automato_repeat_until_goal",
|
||||
id: "_GOa9b!@f(I=^l_[!pg3",
|
||||
x: 38,
|
||||
y: 88,
|
||||
inputs: {
|
||||
DO: {
|
||||
block: {
|
||||
type: "automato_move_forward",
|
||||
id: "QUX4{ospcnon~=%Xqpx#",
|
||||
next: {
|
||||
block: {
|
||||
type: "automato_turn_left",
|
||||
id: "nY-ZmPn~1(z6F~iG$b}o",
|
||||
next: {
|
||||
block: {
|
||||
type: "automato_move_forward",
|
||||
id: "D}G*#p933.aY//p?%P[*",
|
||||
next: {
|
||||
block: {
|
||||
type: "automato_turn_right",
|
||||
id: "!J{Ri5A0hu^R4IBhv~J7",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
5: {
|
||||
blocks: {
|
||||
languageVersion: 0,
|
||||
blocks: [
|
||||
{
|
||||
type: "automato_move_forward",
|
||||
id: "={d8;tDvVSR;Qsk3)Rj3",
|
||||
x: 38,
|
||||
y: 38,
|
||||
next: {
|
||||
block: {
|
||||
type: "automato_move_forward",
|
||||
id: "Qud3z!0448y{D@I=%Xk:",
|
||||
next: {
|
||||
block: {
|
||||
type: "automato_turn_left",
|
||||
id: "Z8XmhaM{H#*8.ZXP0*jl",
|
||||
next: {
|
||||
block: {
|
||||
type: "automato_repeat_until_goal",
|
||||
id: "1f6%~iCco}DyQ:cgH86D",
|
||||
inputs: {
|
||||
DO: {
|
||||
block: {
|
||||
type: "automato_move_forward",
|
||||
id: "iqtmU-HoFmKi#54Y8C;$",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
6: {
|
||||
blocks: {
|
||||
languageVersion: 0,
|
||||
blocks: [
|
||||
{
|
||||
type: "automato_repeat_until_goal",
|
||||
id: "N]+p?1(9_O^MX}b*hf[m",
|
||||
x: 34,
|
||||
y: 134,
|
||||
inputs: {
|
||||
DO: {
|
||||
block: {
|
||||
type: "automato_move_forward",
|
||||
id: "F(RCqq2^5`s$pF`=j6dT",
|
||||
next: {
|
||||
block: {
|
||||
type: "automato_if",
|
||||
id: "5cJ_Yx7;cFI,n_jlW02P",
|
||||
fields: { DIR: "isPathLeft" },
|
||||
inputs: {
|
||||
DO: {
|
||||
block: {
|
||||
type: "automato_turn_left",
|
||||
id: "WMI/e:8FL;UnhP[01~14",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
7: {
|
||||
blocks: {
|
||||
languageVersion: 0,
|
||||
blocks: [
|
||||
{
|
||||
type: "automato_repeat_until_goal",
|
||||
id: "lk.$gb7DEMwgOW(D]o?C",
|
||||
x: 38,
|
||||
y: 88,
|
||||
inputs: {
|
||||
DO: {
|
||||
block: {
|
||||
type: "automato_move_forward",
|
||||
id: "FMhkj:m]}N0SfeHc.L@y",
|
||||
next: {
|
||||
block: {
|
||||
type: "automato_if",
|
||||
id: "DNgAG1XXR/8Vv%t}~qHR",
|
||||
fields: { DIR: "isPathRight" },
|
||||
inputs: {
|
||||
DO: {
|
||||
block: {
|
||||
type: "automato_turn_right",
|
||||
id: "fY[5U1;Twpd083i|5rO4",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
8: {
|
||||
blocks: {
|
||||
languageVersion: 0,
|
||||
blocks: [
|
||||
{
|
||||
type: "automato_repeat_until_goal",
|
||||
id: "hcd5qweto~5BP@T@Fcwe",
|
||||
x: 38,
|
||||
y: 13,
|
||||
inputs: {
|
||||
DO: {
|
||||
block: {
|
||||
type: "automato_move_forward",
|
||||
id: "stEZ$sLmPJuhn@ZxY::5",
|
||||
next: {
|
||||
block: {
|
||||
type: "automato_if",
|
||||
id: "#6mJ52gC8FHKA_4k3|=`",
|
||||
fields: { DIR: "isPathLeft" },
|
||||
inputs: {
|
||||
DO: {
|
||||
block: {
|
||||
type: "automato_turn_left",
|
||||
id: "p@R/O;YFznVYn13eIEtr",
|
||||
},
|
||||
},
|
||||
},
|
||||
next: {
|
||||
block: {
|
||||
type: "automato_if",
|
||||
id: "MjcW?7[@Onp;daUW[`yn",
|
||||
fields: { DIR: "isPathRight" },
|
||||
inputs: {
|
||||
DO: {
|
||||
block: {
|
||||
type: "automato_turn_right",
|
||||
id: ":}$)EO]z4?l@,yNQ4oVk",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
9: {
|
||||
blocks: {
|
||||
languageVersion: 0,
|
||||
blocks: [
|
||||
{
|
||||
type: "automato_repeat_until_goal",
|
||||
id: "y,XAdDT6I89o$A3bkpR;",
|
||||
x: 38,
|
||||
y: 13,
|
||||
inputs: {
|
||||
DO: {
|
||||
block: {
|
||||
type: "automato_ifElse",
|
||||
id: "Q0:H}+Vw{.*o8s9HvgTH",
|
||||
fields: { DIR: "isPathAhead" },
|
||||
inputs: {
|
||||
DO: {
|
||||
block: {
|
||||
type: "automato_move_forward",
|
||||
id: "Zaf:rnw2j?vjQk-#.Ug;",
|
||||
},
|
||||
},
|
||||
ELSE: {
|
||||
block: {
|
||||
type: "automato_turn_left",
|
||||
id: "AJ35Foz:[iTEW!r(8L;-",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
10: {
|
||||
blocks: {
|
||||
languageVersion: 0,
|
||||
blocks: [
|
||||
{
|
||||
type: "automato_repeat_until_goal",
|
||||
id: ".3)q2,d1Z/Z5C_cC`^_x",
|
||||
x: 38,
|
||||
y: 38,
|
||||
inputs: {
|
||||
DO: {
|
||||
block: {
|
||||
type: "automato_if",
|
||||
id: "D/2h}S=UJC],YD8/?F|a",
|
||||
fields: { DIR: "isPathLeft" },
|
||||
inputs: {
|
||||
DO: {
|
||||
block: {
|
||||
type: "automato_turn_left",
|
||||
id: "3n-#$Jz_^UC7K5PGmJX}",
|
||||
},
|
||||
},
|
||||
},
|
||||
next: {
|
||||
block: {
|
||||
type: "automato_move_forward",
|
||||
id: "(Zd%Q5ar+q9hWy74pqot",
|
||||
next: {
|
||||
block: {
|
||||
type: "automato_if",
|
||||
id: "oB(:A7l(9}labEfl67$G",
|
||||
fields: { DIR: "isPathLeft" },
|
||||
inputs: {
|
||||
DO: {
|
||||
block: {
|
||||
type: "automato_turn_left",
|
||||
id: "FKsY*NZS]v0_BEP3O?T}",
|
||||
},
|
||||
},
|
||||
},
|
||||
next: {
|
||||
block: {
|
||||
type: "automato_move_forward",
|
||||
id: "U:Ya=3QLz^VRddRJwCH4",
|
||||
next: {
|
||||
block: {
|
||||
type: "automato_if",
|
||||
id: "?;`/thN5QX84f-3Urf%%",
|
||||
fields: { DIR: "isPathRight" },
|
||||
inputs: {
|
||||
DO: {
|
||||
block: {
|
||||
type: "automato_turn_right",
|
||||
id: "PFs,A8RxNI+F^i-vv!bg",
|
||||
next: {
|
||||
block: {
|
||||
type: "automato_move_forward",
|
||||
id: "D/v2nvg%+kbVG`|YFo[.",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
71
app/src/atividades/programacao/automato/config/tourSteps.js
Normal file
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* @fileoverview Utility module for tourSteps.js
|
||||
*
|
||||
* @module games.automato.config.tourSteps
|
||||
*/
|
||||
|
||||
import {
|
||||
createWelcomeStep,
|
||||
createGameAreaStep,
|
||||
createToolboxStep,
|
||||
createWorkspaceStep,
|
||||
createRunButtonStep,
|
||||
createResetInfoStep,
|
||||
createPhaseSelectorStep,
|
||||
createPhaseInfoStep,
|
||||
createHelpButtonStep,
|
||||
gameIcons,
|
||||
defaultGameTourOptions,
|
||||
} from "../../../../utils/tourHelpers";
|
||||
|
||||
export const automatoTourSteps = [
|
||||
createWelcomeStep({
|
||||
gameName: "Jogo do Autômato",
|
||||
description:
|
||||
"Neste jogo, você vai programar um robô para navegar em um labirinto e alcançar o objetivo marcado.",
|
||||
challenge: "Use blocos de programação para guiar o Pegman até a bandeira!",
|
||||
iconSvg: gameIcons.robot,
|
||||
}),
|
||||
|
||||
createGameAreaStep({
|
||||
title: "Área do Labirinto",
|
||||
description:
|
||||
"Aqui você vê o Pegman e o labirinto. Seu objetivo é programar o Pegman para chegar até a bandeira.",
|
||||
}),
|
||||
|
||||
createToolboxStep({
|
||||
description:
|
||||
"Arraste os blocos de movimentação disponíveis para a área de programação.",
|
||||
}),
|
||||
|
||||
createWorkspaceStep({
|
||||
description:
|
||||
"Monte sua sequência de comandos. Conecte os blocos na ordem que o Pegman deve executar.",
|
||||
}),
|
||||
|
||||
createRunButtonStep({
|
||||
description: "Clique aqui para ver o Pegman executar seus comandos.",
|
||||
}),
|
||||
|
||||
createResetInfoStep({
|
||||
description:
|
||||
"Se o Pegman não chegar ao objetivo, use o botão de reset para tentar outra solução.",
|
||||
}),
|
||||
|
||||
createPhaseSelectorStep({
|
||||
description:
|
||||
"O jogo tem várias fases com diferentes labirintos e níveis de complexidade.",
|
||||
}),
|
||||
|
||||
createPhaseInfoStep({
|
||||
description:
|
||||
"Aqui você vê o número da fase atual e pode acompanhar seu progresso.",
|
||||
}),
|
||||
|
||||
createHelpButtonStep({
|
||||
description:
|
||||
"Clique no botão de ajuda para rever este tour a qualquer momento.",
|
||||
}),
|
||||
];
|
||||
|
||||
export const automatoTourOptions = defaultGameTourOptions;
|
||||
654
app/src/atividades/programacao/automato/game.js
Normal file
@@ -0,0 +1,654 @@
|
||||
/**
|
||||
* @fileoverview Utility module for game.js
|
||||
*
|
||||
* @module games.automato.game
|
||||
*/
|
||||
|
||||
import Phaser from "phaser";
|
||||
import { BaseGameScene } from "../../../shared/BaseGameScene.js";
|
||||
import { gameEventBus } from "../../../utils/gameEvents";
|
||||
import { GameInterpreter } from "../../../interpreters/GameInterpreter.js";
|
||||
import { setupAutomatoAPI } from "./hooks/interpreterSetup.js";
|
||||
import { validateSolution } from "./validation/validators.js";
|
||||
|
||||
import tiles from "./assets/tiles_pegman.png";
|
||||
import pegman from "./assets/pegman.png";
|
||||
import marker from "./assets/marker.png";
|
||||
|
||||
const ASSETS = {
|
||||
IMG: {
|
||||
TILES: "tiles",
|
||||
PEGMAN: "pegman",
|
||||
MARKER: "marker",
|
||||
},
|
||||
};
|
||||
|
||||
const CONSTANTES = {
|
||||
TAMANHO_TILE: 50,
|
||||
PEGMAN_HEIGHT: 51,
|
||||
PEGMAN_WIDTH: 49,
|
||||
VELOCIDADE_ANIMACAO: 200,
|
||||
};
|
||||
|
||||
const Direcao = {
|
||||
NORTE: 0,
|
||||
LESTE: 1,
|
||||
SUL: 2,
|
||||
OESTE: 3,
|
||||
};
|
||||
|
||||
const TILE_TYPES = {
|
||||
PAREDE: 0,
|
||||
CAMINHO: 1,
|
||||
INICIO: 2,
|
||||
FIM: 3,
|
||||
};
|
||||
|
||||
class AutomatoScene extends BaseGameScene {
|
||||
constructor() {
|
||||
super("AutomatoScene");
|
||||
|
||||
// Estado específico do Automato
|
||||
this.mapa = null;
|
||||
this.posicaoInicial = null;
|
||||
this.posicaoFinal = null;
|
||||
this.posicaoJogador = null;
|
||||
this.direcaoJogador = Direcao.LESTE;
|
||||
this.pegmanSprite = null;
|
||||
this.gradeVisual = null;
|
||||
this.resultadoJogada = "em_andamento";
|
||||
}
|
||||
|
||||
preload() {
|
||||
this.preloadGlobalAssets();
|
||||
this.load.image(ASSETS.IMG.MARKER, marker);
|
||||
this.load.spritesheet(ASSETS.IMG.TILES, tiles, {
|
||||
frameWidth: CONSTANTES.TAMANHO_TILE,
|
||||
frameHeight: CONSTANTES.TAMANHO_TILE,
|
||||
});
|
||||
this.load.spritesheet(ASSETS.IMG.PEGMAN, pegman, {
|
||||
frameHeight: CONSTANTES.PEGMAN_HEIGHT,
|
||||
frameWidth: CONSTANTES.PEGMAN_WIDTH,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Preload dos assets do autômato (tiles, spritesheets e sons se houver).
|
||||
* @returns {void}
|
||||
*/
|
||||
|
||||
init(data) {
|
||||
super.init(data);
|
||||
|
||||
this.mapa = this.configFase?.mapa || [];
|
||||
this.direcaoJogador = Direcao.LESTE;
|
||||
this.resultadoJogada = "em_andamento";
|
||||
}
|
||||
|
||||
/**
|
||||
* Inicialização específica do AutomatoScene.
|
||||
* Recebe `data` e configura estado derivado da `configFase`.
|
||||
* @param {Object} data - Dados opcionais da cena
|
||||
* @returns {void}
|
||||
*/
|
||||
|
||||
onBeforeRun() {
|
||||
this.posicaoJogador = { ...this.posicaoInicial };
|
||||
this.direcaoJogador = Direcao.LESTE;
|
||||
this.resultadoJogada = "em_andamento";
|
||||
this.atualizarVisualJogador();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook chamado imediatamente antes da execução do código do aluno.
|
||||
* Prepara posição inicial e estado de animação.
|
||||
* @returns {void}
|
||||
*/
|
||||
|
||||
onReset() {
|
||||
this.posicaoJogador = { ...this.posicaoInicial };
|
||||
this.direcaoJogador = Direcao.LESTE;
|
||||
this.resultadoJogada = "em_andamento";
|
||||
this.atualizarVisualJogador();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook chamado quando o usuário reseta a cena manualmente.
|
||||
* Restaura estado mas não altera configurações persistentes.
|
||||
* @returns {void}
|
||||
*/
|
||||
|
||||
onSuccess() {
|
||||
this.animarVitoria();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler chamado quando a validação indica sucesso.
|
||||
* Deve executar animações de vitória e finalizar a execução.
|
||||
* @returns {void}
|
||||
*/
|
||||
|
||||
onFailure() {
|
||||
// animarFalha() já foi disparada em moverParaFrente() ao bater na parede;
|
||||
// não repetir aqui para evitar dupla animação.
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler chamado quando a validação indica falha.
|
||||
* Deve executar animações de falha e limpar execução.
|
||||
* @returns {void}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Atualiza a posição e animação visual do Pegman
|
||||
* Sincroniza sprite com estado lógico (posição e direção)
|
||||
*/
|
||||
atualizarVisualJogador() {
|
||||
if (this.posicaoJogador) {
|
||||
const posX =
|
||||
this.posicaoJogador.x * CONSTANTES.TAMANHO_TILE +
|
||||
CONSTANTES.TAMANHO_TILE / 2;
|
||||
const posY =
|
||||
this.posicaoJogador.y * CONSTANTES.TAMANHO_TILE +
|
||||
CONSTANTES.TAMANHO_TILE / 2 -
|
||||
6;
|
||||
this.pegmanSprite.setPosition(posX, posY);
|
||||
|
||||
const animacoesDirecao = [
|
||||
"pegman_idle_norte",
|
||||
"pegman_idle_leste",
|
||||
"pegman_idle_sul",
|
||||
"pegman_idle_oeste",
|
||||
];
|
||||
this.pegmanSprite.play(animacoesDirecao[this.direcaoJogador]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move o Pegman para frente na direção atual
|
||||
* Verifica colisão com paredes e atualiza animação
|
||||
* Se colidir, marca como falha e para a execução
|
||||
*
|
||||
* @returns {Promise<void>} Promise que resolve quando movimento completa
|
||||
*/
|
||||
moverParaFrente() {
|
||||
this.historico.push({
|
||||
tipo: "move",
|
||||
direcao: this.direcaoJogador,
|
||||
posicao: { ...this.posicaoJogador },
|
||||
});
|
||||
|
||||
if (this.resultadoJogada !== "em_andamento") return Promise.resolve();
|
||||
|
||||
let { x, y } = this.posicaoJogador;
|
||||
|
||||
if (this.direcaoJogador === Direcao.NORTE) y--;
|
||||
else if (this.direcaoJogador === Direcao.LESTE) x++;
|
||||
else if (this.direcaoJogador === Direcao.SUL) y++;
|
||||
else if (this.direcaoJogador === Direcao.OESTE) x--;
|
||||
|
||||
const proximoTile =
|
||||
this.mapa[y] && this.mapa[y][x] !== undefined ? this.mapa[y][x] : -1;
|
||||
|
||||
if (proximoTile === TILE_TYPES.PAREDE || proximoTile === -1) {
|
||||
this.resultadoJogada = "falha";
|
||||
this.animarFalha();
|
||||
|
||||
if (this.gameInterpreter) {
|
||||
this.gameInterpreter.stopInternal();
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
return new Promise((resolve) => {
|
||||
const novaX = x * CONSTANTES.TAMANHO_TILE + CONSTANTES.TAMANHO_TILE / 2;
|
||||
const novaY =
|
||||
y * CONSTANTES.TAMANHO_TILE + CONSTANTES.TAMANHO_TILE / 2 - 6;
|
||||
|
||||
this.tweens.add({
|
||||
targets: this.pegmanSprite,
|
||||
x: novaX,
|
||||
y: novaY,
|
||||
duration: CONSTANTES.VELOCIDADE_ANIMACAO / 2,
|
||||
ease: "Power2",
|
||||
onComplete: () => {
|
||||
this.posicaoJogador = { x, y };
|
||||
this.atualizarVisualJogador();
|
||||
resolve();
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Anima a falha do Pegman (batida na parede)
|
||||
* Faz bounce back e depois animação de queda
|
||||
*/
|
||||
animarFalha() {
|
||||
const deltaX =
|
||||
this.direcaoJogador === Direcao.LESTE
|
||||
? 1
|
||||
: this.direcaoJogador === Direcao.OESTE
|
||||
? -1
|
||||
: 0;
|
||||
const deltaY =
|
||||
this.direcaoJogador === Direcao.NORTE
|
||||
? -1
|
||||
: this.direcaoJogador === Direcao.SUL
|
||||
? 1
|
||||
: 0;
|
||||
|
||||
const bounceX =
|
||||
this.pegmanSprite.x + (deltaX * CONSTANTES.TAMANHO_TILE) / 4;
|
||||
const bounceY =
|
||||
this.pegmanSprite.y + (deltaY * CONSTANTES.TAMANHO_TILE) / 4;
|
||||
|
||||
this.tweens.add({
|
||||
targets: this.pegmanSprite,
|
||||
x: bounceX,
|
||||
y: bounceY,
|
||||
duration: CONSTANTES.VELOCIDADE_ANIMACAO / 3,
|
||||
yoyo: true,
|
||||
repeat: 1,
|
||||
ease: "Power2",
|
||||
onComplete: () => {
|
||||
this.pegmanSprite.play("pegman_fall");
|
||||
this.resultadoJogada = "falha";
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Vira o Pegman 90° para a esquerda
|
||||
* Anima a rotação do sprite
|
||||
*
|
||||
* @returns {Promise<void>} Promise que resolve quando rotação completa
|
||||
*/
|
||||
virarEsquerda() {
|
||||
this.historico.push({
|
||||
tipo: "turnLeft",
|
||||
de: this.direcaoJogador,
|
||||
para: (this.direcaoJogador + 3) % 4,
|
||||
});
|
||||
|
||||
const novaDirecao = (this.direcaoJogador + 3) % 4;
|
||||
return this.animarRotacao(novaDirecao);
|
||||
}
|
||||
|
||||
/**
|
||||
* Vira o Pegman 90° para a direita
|
||||
* Anima a rotação do sprite
|
||||
*
|
||||
* @returns {Promise<void>} Promise que resolve quando rotação completa
|
||||
*/
|
||||
virarDireita() {
|
||||
this.historico.push({
|
||||
tipo: "turnRight",
|
||||
de: this.direcaoJogador,
|
||||
para: (this.direcaoJogador + 1) % 4,
|
||||
});
|
||||
|
||||
const novaDirecao = (this.direcaoJogador + 1) % 4;
|
||||
return this.animarRotacao(novaDirecao);
|
||||
}
|
||||
|
||||
/**
|
||||
* Anima a rotação do Pegman de uma direção para outra
|
||||
* Usa animações pré-definidas ou atualiza visual diretamente
|
||||
*
|
||||
* @param {number} novaDirecao - Nova direção (0=Norte, 1=Leste, 2=Sul, 3=Oeste)
|
||||
* @returns {Promise<void>} Promise que resolve quando animação completa
|
||||
*/
|
||||
animarRotacao(novaDirecao) {
|
||||
return new Promise((resolve) => {
|
||||
const direcaoAtual = this.direcaoJogador;
|
||||
const nomesDirecoes = ["norte", "leste", "sul", "oeste"];
|
||||
const direcaoAtualNome = nomesDirecoes[direcaoAtual];
|
||||
const novaDirecaoNome = nomesDirecoes[novaDirecao];
|
||||
|
||||
this.direcaoJogador = novaDirecao;
|
||||
|
||||
const chaveAnimacao = `${direcaoAtualNome}_para_${novaDirecaoNome}`;
|
||||
|
||||
if (this.anims.exists(chaveAnimacao)) {
|
||||
this.pegmanSprite.play(chaveAnimacao);
|
||||
|
||||
const onRotationComplete = () => {
|
||||
this.atualizarVisualJogador();
|
||||
this.pegmanSprite.off("animationcomplete", onRotationComplete);
|
||||
resolve();
|
||||
};
|
||||
|
||||
this.pegmanSprite.on("animationcomplete", onRotationComplete);
|
||||
} else {
|
||||
this.atualizarVisualJogador();
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica se o Pegman chegou ao objetivo (tile tipo FIM)
|
||||
*
|
||||
* @returns {boolean} true se chegou no alvo, false caso contrário
|
||||
*/
|
||||
chegouNoAlvo() {
|
||||
return (
|
||||
this.posicaoJogador.x === this.posicaoFinal.x &&
|
||||
this.posicaoJogador.y === this.posicaoFinal.y
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica se há caminho livre em uma direção relativa
|
||||
*
|
||||
* @param {string} direcaoRelativa - 'frente', 'esquerda' ou 'direita'
|
||||
* @returns {boolean} true se há caminho, false se é parede ou fora do mapa
|
||||
*/
|
||||
haCaminho(direcaoRelativa) {
|
||||
let direcaoAbsoluta = this.direcaoJogador;
|
||||
if (direcaoRelativa === "esquerda") {
|
||||
direcaoAbsoluta = (this.direcaoJogador + 3) % 4;
|
||||
} else if (direcaoRelativa === "direita") {
|
||||
direcaoAbsoluta = (this.direcaoJogador + 1) % 4;
|
||||
}
|
||||
|
||||
let { x, y } = this.posicaoJogador;
|
||||
if (direcaoAbsoluta === Direcao.NORTE) y--;
|
||||
else if (direcaoAbsoluta === Direcao.LESTE) x++;
|
||||
else if (direcaoAbsoluta === Direcao.SUL) y++;
|
||||
else if (direcaoAbsoluta === Direcao.OESTE) x--;
|
||||
|
||||
const proximoTile =
|
||||
this.mapa[y] && this.mapa[y][x] !== undefined ? this.mapa[y][x] : -1;
|
||||
return proximoTile !== TILE_TYPES.PAREDE && proximoTile !== -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Anima a vitória do Pegman
|
||||
* Sequência de frames de comemoração
|
||||
*/
|
||||
animarVitoria() {
|
||||
const stepSpeed = 150;
|
||||
this.pegmanSprite.setFrame(16);
|
||||
setTimeout(() => this.pegmanSprite.setFrame(18), stepSpeed);
|
||||
setTimeout(() => this.pegmanSprite.setFrame(16), stepSpeed * 2);
|
||||
setTimeout(() => {
|
||||
const framesBase = [0, 4, 8, 12];
|
||||
this.pegmanSprite.setFrame(framesBase[this.direcaoJogador]);
|
||||
}, stepSpeed * 3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Destaca um bloco no workspace do Blockly e sinaliza pausa visual.
|
||||
* Usado pelo interpreter para indicar o bloco atualmente executado.
|
||||
* @param {string} id - Id do bloco a ser destacado
|
||||
* @returns {void}
|
||||
*/
|
||||
highlightBlock(id) {
|
||||
if (this.workspace) this.workspace.highlightBlock(id);
|
||||
this.highlightPause = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trata o resultado final da execução do código do aluno.
|
||||
* Em caso de sucesso dispara evento de sucesso; caso contrário, falha.
|
||||
* @param {string} result - Resultado retornado pelo interpretador
|
||||
* @returns {void}
|
||||
*/
|
||||
handleExecutionResult(result) {
|
||||
if (result === "failure" || this.resultadoJogada === "falha") {
|
||||
gameEventBus.gameFailure();
|
||||
return;
|
||||
}
|
||||
if (this.chegouNoAlvo()) {
|
||||
this.animarVitoria();
|
||||
setTimeout(() => gameEventBus.gameSuccess(), 800);
|
||||
} else {
|
||||
gameEventBus.gameFailure();
|
||||
}
|
||||
}
|
||||
|
||||
createAnimations() {
|
||||
this.anims.create({
|
||||
key: "pegman_idle_norte",
|
||||
frames: [{ key: "pegman", frame: 0 }],
|
||||
frameRate: 1,
|
||||
});
|
||||
this.anims.create({
|
||||
key: "pegman_idle_leste",
|
||||
frames: [{ key: "pegman", frame: 4 }],
|
||||
frameRate: 1,
|
||||
});
|
||||
this.anims.create({
|
||||
key: "pegman_idle_sul",
|
||||
frames: [{ key: "pegman", frame: 8 }],
|
||||
frameRate: 1,
|
||||
});
|
||||
this.anims.create({
|
||||
key: "pegman_idle_oeste",
|
||||
frames: [{ key: "pegman", frame: 12 }],
|
||||
frameRate: 1,
|
||||
});
|
||||
this.anims.create({
|
||||
key: "pegman_fall",
|
||||
frames: this.anims.generateFrameNumbers("pegman", { start: 18, end: 20 }),
|
||||
frameRate: 10,
|
||||
repeat: 0,
|
||||
});
|
||||
this.createRotationAnimations();
|
||||
}
|
||||
|
||||
createRotationAnimations() {
|
||||
this.anims.create({
|
||||
key: "norte_para_leste",
|
||||
frames: [0, 1, 2, 3, 4].map((frame) => ({ key: "pegman", frame })),
|
||||
frameRate: 30,
|
||||
});
|
||||
this.anims.create({
|
||||
key: "norte_para_oeste",
|
||||
frames: [0, 15, 14, 13, 12].map((frame) => ({ key: "pegman", frame })),
|
||||
frameRate: 30,
|
||||
});
|
||||
this.anims.create({
|
||||
key: "oeste_para_sul",
|
||||
frames: [12, 11, 10, 9, 8].map((frame) => ({ key: "pegman", frame })),
|
||||
frameRate: 30,
|
||||
});
|
||||
this.anims.create({
|
||||
key: "oeste_para_norte",
|
||||
frames: [12, 13, 14, 15, 0].map((frame) => ({ key: "pegman", frame })),
|
||||
frameRate: 30,
|
||||
});
|
||||
this.anims.create({
|
||||
key: "leste_para_sul",
|
||||
frames: [4, 5, 6, 7, 8].map((frame) => ({ key: "pegman", frame })),
|
||||
frameRate: 30,
|
||||
});
|
||||
this.anims.create({
|
||||
key: "leste_para_norte",
|
||||
frames: [4, 3, 2, 1, 0].map((frame) => ({ key: "pegman", frame })),
|
||||
frameRate: 30,
|
||||
});
|
||||
this.anims.create({
|
||||
key: "sul_para_oeste",
|
||||
frames: [8, 9, 10, 11, 12].map((frame) => ({ key: "pegman", frame })),
|
||||
frameRate: 30,
|
||||
});
|
||||
this.anims.create({
|
||||
key: "sul_para_leste",
|
||||
frames: [8, 7, 6, 5, 4].map((frame) => ({ key: "pegman", frame })),
|
||||
frameRate: 30,
|
||||
});
|
||||
}
|
||||
|
||||
createVisualGrid(TILE_SHAPES, normalize) {
|
||||
this.gradeVisual = this.add.group();
|
||||
for (let y = 0; y < this.mapa.length; y++) {
|
||||
for (let x = 0; x < this.mapa[y].length; x++) {
|
||||
let tileShape =
|
||||
normalize(x, y) +
|
||||
normalize(x, y - 1) +
|
||||
normalize(x + 1, y) +
|
||||
normalize(x, y + 1) +
|
||||
normalize(x - 1, y);
|
||||
if (!TILE_SHAPES[tileShape]) {
|
||||
tileShape =
|
||||
tileShape === "00000" && Math.random() > 0.3
|
||||
? "null0"
|
||||
: "null" + Math.floor(1 + Math.random() * 4);
|
||||
}
|
||||
const [tileX, tileY] = TILE_SHAPES[tileShape];
|
||||
const frameIndex = tileY * 5 + tileX;
|
||||
const tileSprite = this.add.sprite(
|
||||
x * CONSTANTES.TAMANHO_TILE,
|
||||
y * CONSTANTES.TAMANHO_TILE,
|
||||
"tiles",
|
||||
frameIndex,
|
||||
);
|
||||
tileSprite.setDisplaySize(
|
||||
CONSTANTES.TAMANHO_TILE,
|
||||
CONSTANTES.TAMANHO_TILE,
|
||||
);
|
||||
tileSprite.setOrigin(0);
|
||||
this.gradeVisual.add(tileSprite);
|
||||
}
|
||||
}
|
||||
for (let y = 0; y < this.mapa.length; y++) {
|
||||
for (let x = 0; x < this.mapa[y].length; x++) {
|
||||
if (this.mapa[y][x] === TILE_TYPES.FIM) {
|
||||
const markerImg = this.add.image(
|
||||
x * CONSTANTES.TAMANHO_TILE + CONSTANTES.TAMANHO_TILE / 2,
|
||||
y * CONSTANTES.TAMANHO_TILE + CONSTANTES.TAMANHO_TILE / 2 - 10,
|
||||
"marker",
|
||||
);
|
||||
markerImg.setDisplaySize(12, 20.04);
|
||||
markerImg.setOrigin(0.5);
|
||||
this.gradeVisual.add(markerImg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
createPegmanSprite() {
|
||||
this.pegmanSprite = this.add.sprite(0, 0, "pegman", 4);
|
||||
this.pegmanSprite.setDisplaySize(
|
||||
CONSTANTES.TAMANHO_TILE * 0.8,
|
||||
CONSTANTES.TAMANHO_TILE * 0.8,
|
||||
);
|
||||
this.pegmanSprite.setOrigin(0.5);
|
||||
this.atualizarVisualJogador();
|
||||
}
|
||||
|
||||
create() {
|
||||
this.mapa = this.configFase?.mapa || [];
|
||||
this.resultadoJogada = "em_andamento";
|
||||
|
||||
const TILE_SHAPES = {
|
||||
10010: [4, 0],
|
||||
10001: [3, 3],
|
||||
11000: [0, 1],
|
||||
10100: [0, 2],
|
||||
11010: [4, 1],
|
||||
10101: [3, 2],
|
||||
10110: [0, 0],
|
||||
10011: [2, 0],
|
||||
11001: [4, 2],
|
||||
11100: [2, 3],
|
||||
11110: [1, 1],
|
||||
10111: [1, 0],
|
||||
11011: [2, 1],
|
||||
11101: [1, 2],
|
||||
11111: [2, 2],
|
||||
null0: [4, 3],
|
||||
null1: [3, 0],
|
||||
null2: [3, 1],
|
||||
null3: [0, 3],
|
||||
null4: [1, 3],
|
||||
};
|
||||
|
||||
const normalize = (x, y) => {
|
||||
if (x < 0 || x >= this.mapa[0].length || y < 0 || y >= this.mapa.length)
|
||||
return "0";
|
||||
return this.mapa[y][x] === TILE_TYPES.PAREDE ? "0" : "1";
|
||||
};
|
||||
|
||||
this.createAnimations();
|
||||
|
||||
const encontrarPosicao = (tipo) => {
|
||||
for (let y = 0; y < this.mapa.length; y++) {
|
||||
for (let x = 0; x < this.mapa[y].length; x++) {
|
||||
if (this.mapa[y][x] === tipo) return { x, y };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
this.posicaoInicial = encontrarPosicao(TILE_TYPES.INICIO);
|
||||
this.posicaoFinal = encontrarPosicao(TILE_TYPES.FIM);
|
||||
this.posicaoJogador = { ...this.posicaoInicial };
|
||||
this.direcaoJogador = Direcao.LESTE;
|
||||
|
||||
this.createVisualGrid(TILE_SHAPES, normalize);
|
||||
this.createPegmanSprite();
|
||||
|
||||
this.gameInterpreter = new GameInterpreter({
|
||||
stepDelay: 20,
|
||||
pauseExec: true,
|
||||
});
|
||||
|
||||
this.setupStandardController(
|
||||
setupAutomatoAPI,
|
||||
(history, config, gameConfig) =>
|
||||
validateSolution(history, config, gameConfig, this),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const createGame = (
|
||||
parentElement,
|
||||
configFaseAtual,
|
||||
customFailureHandler = null,
|
||||
idFaseAtual = null,
|
||||
gameConfig = null,
|
||||
) => {
|
||||
const config =
|
||||
idFaseAtual && gameConfig
|
||||
? gameConfig.fases[idFaseAtual - 1]
|
||||
: configFaseAtual;
|
||||
|
||||
return {
|
||||
type: Phaser.AUTO,
|
||||
width: config.mapa[0].length * CONSTANTES.TAMANHO_TILE,
|
||||
height: config.mapa.length * CONSTANTES.TAMANHO_TILE,
|
||||
scale: {
|
||||
mode: Phaser.Scale.EXPAND,
|
||||
autoCenter: Phaser.Scale.CENTER_BOTH,
|
||||
},
|
||||
backgroundColor: "#F1EEE7",
|
||||
parent: parentElement,
|
||||
scene: [AutomatoScene],
|
||||
callbacks: {
|
||||
preBoot: (game) => {
|
||||
game.registry.merge({
|
||||
configFase: config,
|
||||
gameConfig: gameConfig,
|
||||
customFailureHandler: customFailureHandler,
|
||||
stepDelay: 20,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Factory que monta a configuração Phaser para o jogo Autômato.
|
||||
* Calcula largura/altura a partir do mapa da fase e injeta callbacks.
|
||||
* @param {HTMLElement} parentElement - Elemento DOM pai para o canvas
|
||||
* @param {Object} configFaseAtual - Configuração da fase (fallback)
|
||||
* @param {Function|null} customFailureHandler - Handler opcional de falha
|
||||
* @param {number|null} idFaseAtual - Índice da fase atual (1-based)
|
||||
* @param {Object|null} gameConfig - Configuração completa do jogo
|
||||
* @returns {Object} Configuração para inicializar `Phaser.Game`
|
||||
*/
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* @fileoverview Utility module for interpreterSetup.js
|
||||
*
|
||||
* @module games.automato.hooks.interpreterSetup
|
||||
*/
|
||||
|
||||
import { ApiHelpers } from "../../../../interpreters/ApiHelpers.js";
|
||||
|
||||
/**
|
||||
* Configura a API do Automato para o js-interpreter
|
||||
* @param {object} scene - Cena do jogo Phaser
|
||||
* @param {object} config - Configurações de velocidade e animação
|
||||
* @returns {function} - Função de setup para o interpreter
|
||||
*/
|
||||
export const setupAutomatoAPI = (scene, config = {}) => {
|
||||
const animationDelay = config.animationSpeed;
|
||||
|
||||
return (interpreter, globalScope) => {
|
||||
ApiHelpers.registerFunction(
|
||||
interpreter,
|
||||
globalScope,
|
||||
"moverParaFrente",
|
||||
ApiHelpers.createActionWrapper(scene, "moverParaFrente", animationDelay),
|
||||
true,
|
||||
);
|
||||
|
||||
ApiHelpers.registerFunction(
|
||||
interpreter,
|
||||
globalScope,
|
||||
"virarEsquerda",
|
||||
ApiHelpers.createActionWrapper(scene, "virarEsquerda", animationDelay),
|
||||
true,
|
||||
);
|
||||
|
||||
ApiHelpers.registerFunction(
|
||||
interpreter,
|
||||
globalScope,
|
||||
"virarDireita",
|
||||
ApiHelpers.createActionWrapper(scene, "virarDireita", animationDelay),
|
||||
true,
|
||||
);
|
||||
|
||||
ApiHelpers.registerFunction(
|
||||
interpreter,
|
||||
globalScope,
|
||||
"chegouNoAlvo",
|
||||
ApiHelpers.createConditionWrapper(scene, "chegouNoAlvo"),
|
||||
false,
|
||||
);
|
||||
|
||||
ApiHelpers.registerFunction(
|
||||
interpreter,
|
||||
globalScope,
|
||||
"haCaminho",
|
||||
ApiHelpers.createConditionWrapper(scene, "haCaminho"),
|
||||
false,
|
||||
);
|
||||
|
||||
ApiHelpers.registerFunction(
|
||||
interpreter,
|
||||
globalScope,
|
||||
"highlightBlock",
|
||||
ApiHelpers.createHighlightWrapper(scene),
|
||||
false,
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export const AUTOMATO_COMMANDS = {
|
||||
ACTIONS: ["moverParaFrente", "virarEsquerda", "virarDireita"],
|
||||
CONDITIONS: ["chegouNoAlvo", "haCaminho"],
|
||||
SPECIAL: ["highlightBlock"],
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* @fileoverview Utility module for useAutomatoTour.js
|
||||
*
|
||||
* @module games.automato.hooks.useAutomatoTour
|
||||
*/
|
||||
|
||||
import { useGameTour } from "../../../../hooks/useGameTour";
|
||||
import { automatoTourSteps, automatoTourOptions } from "../config/tourSteps";
|
||||
|
||||
export const useAutomatoTour = () => {
|
||||
/**
|
||||
* Hook que inicializa e retorna o tour do Autômato.
|
||||
* Fornece handlers para iniciar/pausar o tour da interface do jogo.
|
||||
* @returns {Object} API do tour (start, cancel, next, etc.)
|
||||
*/
|
||||
return useGameTour("automato", automatoTourSteps, automatoTourOptions);
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* @fileoverview Utility module for validators.js
|
||||
*
|
||||
* @module games.automato.validation.validators
|
||||
*/
|
||||
|
||||
import { BaseGameValidator } from "../../../../shared/BaseGameValidator";
|
||||
|
||||
/**
|
||||
* Validador do Automato Game.
|
||||
* Valida se o Pegman chegou ao objetivo (tile marcado como 3) e aplica
|
||||
* mensagens de feedback configuradas em `gameConfig`.
|
||||
*
|
||||
* @class AutomatoValidator
|
||||
* @extends BaseGameValidator
|
||||
*/
|
||||
class AutomatoValidator extends BaseGameValidator {
|
||||
/**
|
||||
* Valida a solução do aluno
|
||||
*
|
||||
* @param {Array} history - Histórico de ações (para debug/estatísticas)
|
||||
* @param {Object} config - Configuração da fase atual
|
||||
* @param {Object} gameConfig - Configuração global do jogo
|
||||
* @param {Object} sceneRef - Referência à cena Phaser
|
||||
* @returns {Object} { success: boolean, reason?: string }
|
||||
*/
|
||||
validatePhase(history, config, gameConfig, sceneRef) {
|
||||
// Verificar se o jogo falhou (bateu na parede, etc)
|
||||
if (sceneRef && sceneRef.resultadoJogada === "falha") {
|
||||
return this.failure(
|
||||
gameConfig?.mensagens?.falhouExecucao ||
|
||||
"Você bateu em uma parede ou saiu do caminho!",
|
||||
);
|
||||
}
|
||||
|
||||
// Validar se chegou no objetivo
|
||||
// A cena implementa chegouNoAlvo() que verifica se está no tile 3
|
||||
if (sceneRef && sceneRef.chegouNoAlvo && sceneRef.chegouNoAlvo()) {
|
||||
return this.success();
|
||||
}
|
||||
|
||||
// Não chegou ao objetivo
|
||||
return this.failure(
|
||||
gameConfig?.mensagens?.naoChegou ||
|
||||
"Você não chegou ao objetivo! Verifique seu caminho.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton para reutilização
|
||||
const validatorInstance = new AutomatoValidator();
|
||||
|
||||
/**
|
||||
* Função exportada para validação de soluções do Automato Game
|
||||
*
|
||||
* @param {Array} history - Histórico de ações
|
||||
* @param {Object} config - Configuração da fase
|
||||
* @param {Object} gameConfig - Configuração global
|
||||
* @param {Object} sceneRef - Referência à cena (necessário para validação)
|
||||
* @returns {Object} { success: boolean, reason?: string }
|
||||
*/
|
||||
export const validateSolution = (history, config, gameConfig, sceneRef) => {
|
||||
return validatorInstance.validatePhase(history, config, gameConfig, sceneRef);
|
||||
};
|
||||
BIN
app/src/atividades/programacao/bkp.7z
Normal file
80
app/src/atividades/programacao/cripto/CriptoGame.jsx
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* @fileoverview React component for CriptoGame.jsx
|
||||
*
|
||||
* @module games.cripto.CriptoGame
|
||||
*/
|
||||
|
||||
|
||||
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";
|
||||
import { starterBlocks } from "./config/starterBlocks";
|
||||
import { useCriptoTour } from "./hooks/useCriptoTour";
|
||||
import { debugSolutions } from "./config/debugSolutions";
|
||||
import "shepherd.js/dist/css/shepherd.css";
|
||||
import "../../../styles/shepherd-theme.css";
|
||||
|
||||
function CriptoContent() {
|
||||
const { setFailureMessage, isDebugMode } = useGameState();
|
||||
useCriptoTour();
|
||||
|
||||
useEffect(() => {
|
||||
registerBlocks();
|
||||
}, []);
|
||||
|
||||
const toolboxGenerator = useMemo(() => {
|
||||
|
||||
|
||||
return (allowedBlocks) => generateDynamicToolbox(allowedBlocks);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<GameBase
|
||||
gameFactory={createGame}
|
||||
gameConfig={gameConfig}
|
||||
customFailureHandler={setFailureMessage}
|
||||
failureHandler={setFailureMessage}
|
||||
>
|
||||
<GameEditor>
|
||||
<BlocklyEditor
|
||||
toolboxGenerator={toolboxGenerator}
|
||||
debugSolutions={isDebugMode ? debugSolutions : null}
|
||||
starterBlocks={starterBlocks}
|
||||
starter={starterBlocks}
|
||||
/>
|
||||
</GameEditor>
|
||||
</GameBase>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Componente interno que monta a cena e o editor do jogo Cripto.
|
||||
* Registra blocos, configura toolbox dinâmico e injeta o `gameFactory`.
|
||||
* @returns {JSX.Element} Conteúdo do jogo (editor + canvas)
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Componente de página que fornece o contexto de estado do jogo Cripto.
|
||||
* Envolve `CriptoContent` com o `GameStateProvider` configurado.
|
||||
* @returns {JSX.Element} Página completa do jogo Cripto
|
||||
*/
|
||||
export default function CriptoGame() {
|
||||
return (
|
||||
<GameStateProvider gameConfig={gameConfig}>
|
||||
<CriptoContent />
|
||||
</GameStateProvider>
|
||||
);
|
||||
}
|
||||
|
||||
CriptoContent.propTypes = {};
|
||||
CriptoGame.propTypes = {};
|
||||
BIN
app/src/atividades/programacao/cripto/assets/background_loop.mp3
Normal file
821
app/src/atividades/programacao/cripto/blocks/blocks.js
Normal file
@@ -0,0 +1,821 @@
|
||||
/**
|
||||
* @fileoverview Utility module for blocks.js
|
||||
*
|
||||
* @module games.cripto.blocks.blocks
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
import * as Blockly from "blockly/core";
|
||||
import "blockly/blocks";
|
||||
import { javascriptGenerator } from "blockly/javascript";
|
||||
|
||||
const HUE_LOGICA = 210;
|
||||
const HUE_MATEMATICA = 230;
|
||||
const HUE_TEXTO = 160;
|
||||
const HUE_REPETICAO = 120;
|
||||
const HUE_VARIAVEIS = 330;
|
||||
|
||||
export const registerBlocks = () => {
|
||||
defineBlocks();
|
||||
defineGenerators();
|
||||
};
|
||||
|
||||
/**
|
||||
* Registra todos os blocos e geradores do jogo Cripto no Blockly.
|
||||
* Deve ser chamado uma vez durante a inicialização do editor.
|
||||
* @returns {void}
|
||||
*/
|
||||
|
||||
|
||||
export const generateDynamicToolbox = (allowedBlocks = []) => {
|
||||
const blockDefinitions = {
|
||||
// Matemática
|
||||
math_number: {
|
||||
kind: "block",
|
||||
type: "math_number",
|
||||
},
|
||||
math_arithmetic: {
|
||||
kind: "block",
|
||||
type: "math_arithmetic",
|
||||
},
|
||||
math_modulo: {
|
||||
kind: "block",
|
||||
type: "math_modulo",
|
||||
},
|
||||
|
||||
// Texto
|
||||
text: {
|
||||
kind: "block",
|
||||
type: "text",
|
||||
},
|
||||
text_indexOf: {
|
||||
kind: "block",
|
||||
type: "text_indexOf",
|
||||
},
|
||||
text_charAt: {
|
||||
kind: "block",
|
||||
type: "text_charAt",
|
||||
},
|
||||
text_join: {
|
||||
kind: "block",
|
||||
type: "text_join",
|
||||
},
|
||||
text_length: {
|
||||
kind: "block",
|
||||
type: "text_length",
|
||||
},
|
||||
alfabeto: {
|
||||
kind: "block",
|
||||
type: "alfabeto",
|
||||
},
|
||||
alfabeto_secreto: {
|
||||
kind: "block",
|
||||
type: "alfabeto_secreto",
|
||||
},
|
||||
|
||||
// Lógica
|
||||
controls_if: {
|
||||
kind: "block",
|
||||
type: "controls_if",
|
||||
},
|
||||
logic_compare: {
|
||||
kind: "block",
|
||||
type: "logic_compare",
|
||||
},
|
||||
|
||||
// Repetição
|
||||
controls_whileUntil: {
|
||||
kind: "block",
|
||||
type: "controls_whileUntil",
|
||||
},
|
||||
definir_contador: {
|
||||
kind: "block",
|
||||
type: "definir_contador",
|
||||
},
|
||||
obter_contador: {
|
||||
kind: "block",
|
||||
type: "obter_contador",
|
||||
},
|
||||
// Blocos Customizados de Entrada/Saída
|
||||
definir_entrada: {
|
||||
kind: "block",
|
||||
type: "definir_entrada",
|
||||
},
|
||||
definir_saida: {
|
||||
kind: "block",
|
||||
type: "definir_saida",
|
||||
},
|
||||
concatenar_saida: {
|
||||
kind: "block",
|
||||
type: "concatenar_saida",
|
||||
},
|
||||
obter_entrada: {
|
||||
kind: "block",
|
||||
type: "obter_entrada",
|
||||
},
|
||||
obter_saida: {
|
||||
kind: "block",
|
||||
type: "obter_saida",
|
||||
},
|
||||
|
||||
// Variáveis Customizadas
|
||||
definir_letra: {
|
||||
kind: "block",
|
||||
type: "definir_letra",
|
||||
},
|
||||
obter_letra: {
|
||||
kind: "block",
|
||||
type: "obter_letra",
|
||||
},
|
||||
definir_posicao: {
|
||||
kind: "block",
|
||||
type: "definir_posicao",
|
||||
},
|
||||
obter_posicao: {
|
||||
kind: "block",
|
||||
type: "obter_posicao",
|
||||
},
|
||||
definir_nova_posicao: {
|
||||
kind: "block",
|
||||
type: "definir_nova_posicao",
|
||||
},
|
||||
obter_nova_posicao: {
|
||||
kind: "block",
|
||||
type: "obter_nova_posicao",
|
||||
},
|
||||
definir_nova_letra: {
|
||||
kind: "block",
|
||||
type: "definir_nova_letra",
|
||||
},
|
||||
obter_nova_letra: {
|
||||
kind: "block",
|
||||
type: "obter_nova_letra",
|
||||
},
|
||||
definir_chave: {
|
||||
kind: "block",
|
||||
type: "definir_chave",
|
||||
},
|
||||
obter_chave: {
|
||||
kind: "block",
|
||||
type: "obter_chave",
|
||||
},
|
||||
definir_letra_secreta: {
|
||||
kind: "block",
|
||||
type: "definir_letra_secreta",
|
||||
},
|
||||
obter_letra_secreta: {
|
||||
kind: "block",
|
||||
type: "obter_letra_secreta",
|
||||
},
|
||||
definir_soma: {
|
||||
kind: "block",
|
||||
type: "definir_soma",
|
||||
},
|
||||
obter_soma: {
|
||||
kind: "block",
|
||||
type: "obter_soma",
|
||||
},
|
||||
};
|
||||
|
||||
const toolboxContents = {
|
||||
kind: "categoryToolbox",
|
||||
contents: [
|
||||
{
|
||||
kind: "category",
|
||||
name: "Entrada/Saída",
|
||||
colour: HUE_VARIAVEIS,
|
||||
contents: [],
|
||||
cssConfig: { container: "variaveis" },
|
||||
},
|
||||
{
|
||||
kind: "category",
|
||||
name: "Lógica",
|
||||
colour: HUE_LOGICA,
|
||||
contents: [],
|
||||
cssConfig: { container: "logica" },
|
||||
},
|
||||
{
|
||||
kind: "category",
|
||||
name: "Repetição",
|
||||
colour: HUE_REPETICAO,
|
||||
contents: [],
|
||||
cssConfig: { container: "repeticao" },
|
||||
},
|
||||
{
|
||||
kind: "category",
|
||||
name: "Texto",
|
||||
colour: HUE_TEXTO,
|
||||
contents: [],
|
||||
cssConfig: { container: "texto" },
|
||||
},
|
||||
{
|
||||
kind: "category",
|
||||
name: "Matemática",
|
||||
colour: HUE_MATEMATICA,
|
||||
contents: [],
|
||||
cssConfig: { container: "matematica" },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
allowedBlocks.forEach((blockId) => {
|
||||
const blockDef = blockDefinitions[blockId];
|
||||
|
||||
if (!blockDef) {
|
||||
console.warn(`Bloco não encontrado: ${blockId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const categoryMap = {
|
||||
obter_entrada: 0,
|
||||
obter_saida: 0,
|
||||
definir_entrada: 0,
|
||||
definir_saida: 0,
|
||||
concatenar_saida: 0,
|
||||
definir_letra: 0,
|
||||
obter_letra: 0,
|
||||
definir_posicao: 0,
|
||||
obter_posicao: 0,
|
||||
definir_nova_posicao: 0,
|
||||
obter_nova_posicao: 0,
|
||||
definir_nova_letra: 0,
|
||||
obter_nova_letra: 0,
|
||||
definir_chave: 0,
|
||||
obter_chave: 0,
|
||||
definir_letra_secreta: 0,
|
||||
obter_letra_secreta: 0,
|
||||
definir_soma: 0,
|
||||
obter_soma: 0,
|
||||
controls_if: 1,
|
||||
logic_compare: 1,
|
||||
controls_whileUntil: 2,
|
||||
definir_contador: 2,
|
||||
obter_contador: 2,
|
||||
text: 3,
|
||||
text_charAt: 3,
|
||||
text_join: 3,
|
||||
text_length: 3,
|
||||
text_indexOf: 3,
|
||||
alfabeto: 3,
|
||||
alfabeto_secreto: 3,
|
||||
math_number: 4,
|
||||
math_arithmetic: 4,
|
||||
math_modulo: 4,
|
||||
};
|
||||
|
||||
const categoryIndex = categoryMap[blockId];
|
||||
|
||||
if (
|
||||
categoryIndex !== undefined &&
|
||||
categoryIndex >= 0 &&
|
||||
toolboxContents.contents[categoryIndex]
|
||||
) {
|
||||
if (!toolboxContents.contents[categoryIndex].contents) {
|
||||
toolboxContents.contents[categoryIndex].contents = [];
|
||||
}
|
||||
toolboxContents.contents[categoryIndex].contents.push(blockDef);
|
||||
}
|
||||
});
|
||||
|
||||
return toolboxContents;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gera a estrutura de toolbox do Blockly contendo apenas blocos permitidos.
|
||||
* Recebe `allowedBlocks` (lista de ids) e retorna o JSON do toolbox.
|
||||
* @param {Array<string>} [allowedBlocks=[]] - Lista de blocos habilitados
|
||||
* @returns {Object} Estrutura `categoryToolbox` para o Blockly
|
||||
*/
|
||||
|
||||
const defineBlocks = () => {
|
||||
Blockly.Blocks["text_charAt"] = {
|
||||
init: function () {
|
||||
this.setHelpUrl(Blockly.Msg["TEXT_CHARAT_HELPURL"]);
|
||||
this.setColour(HUE_TEXTO);
|
||||
this.setOutput(true, "String");
|
||||
this.appendValueInput("VALUE").setCheck("String").appendField("no texto");
|
||||
this.appendValueInput("AT").setCheck("Number").appendField("obter letra");
|
||||
this.setInputsInline(true);
|
||||
this.setTooltip(Blockly.Msg["TEXT_CHARAT_TOOLTIP"]);
|
||||
},
|
||||
};
|
||||
|
||||
// Bloco: Definir Contador
|
||||
Blockly.Blocks["definir_contador"] = {
|
||||
init: function () {
|
||||
this.appendValueInput("VALUE")
|
||||
.setCheck(null)
|
||||
.appendField("definir CONTADOR como");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(HUE_REPETICAO);
|
||||
this.setTooltip("Define o valor do CONTADOR");
|
||||
this.setHelpUrl("");
|
||||
},
|
||||
};
|
||||
|
||||
// Bloco: Definir Entrada
|
||||
Blockly.Blocks["definir_entrada"] = {
|
||||
init: function () {
|
||||
this.appendValueInput("VALUE")
|
||||
.setCheck(null)
|
||||
.appendField("definir ENTRADA como");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(HUE_VARIAVEIS);
|
||||
this.setTooltip("Define o valor da ENTRADA");
|
||||
this.setHelpUrl("");
|
||||
},
|
||||
};
|
||||
|
||||
// Bloco: Definir Saída
|
||||
Blockly.Blocks["definir_saida"] = {
|
||||
init: function () {
|
||||
this.appendValueInput("VALUE")
|
||||
.setCheck(null)
|
||||
.appendField("definir SAÍDA como");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(HUE_VARIAVEIS);
|
||||
this.setTooltip("Define o valor da SAÍDA");
|
||||
this.setHelpUrl("");
|
||||
},
|
||||
};
|
||||
|
||||
// Bloco: Concatenar Saída
|
||||
Blockly.Blocks["concatenar_saida"] = {
|
||||
init: function () {
|
||||
this.appendValueInput("VALUE")
|
||||
.setCheck(null)
|
||||
.appendField("adicionar à SAÍDA");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(HUE_VARIAVEIS);
|
||||
this.setTooltip("Adiciona um valor ao final da saída");
|
||||
this.setHelpUrl("");
|
||||
},
|
||||
};
|
||||
|
||||
// Bloco: Obter Entrada
|
||||
Blockly.Blocks["obter_entrada"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput().appendField("ENTRADA");
|
||||
this.setOutput(true, null);
|
||||
this.setColour(HUE_VARIAVEIS);
|
||||
this.setTooltip("Obtém o valor atual da entrada");
|
||||
this.setHelpUrl("");
|
||||
},
|
||||
};
|
||||
|
||||
// Bloco: Obter Saída
|
||||
Blockly.Blocks["obter_saida"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput().appendField("SAÍDA");
|
||||
this.setOutput(true, null);
|
||||
this.setColour(HUE_VARIAVEIS);
|
||||
this.setTooltip("Obtém o valor atual da saída");
|
||||
this.setHelpUrl("");
|
||||
},
|
||||
};
|
||||
|
||||
// Bloco: Obter Contador
|
||||
Blockly.Blocks["obter_contador"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput().appendField("CONTADOR");
|
||||
this.setOutput(true, null);
|
||||
this.setColour(HUE_REPETICAO);
|
||||
this.setTooltip("Obtém o valor atual do CONTADOR");
|
||||
this.setHelpUrl("");
|
||||
},
|
||||
};
|
||||
|
||||
// Bloco: Alfabeto (constante)
|
||||
Blockly.Blocks["alfabeto"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput().appendField("ALFABETO");
|
||||
this.setOutput(true, "String");
|
||||
this.setColour(HUE_TEXTO);
|
||||
this.setTooltip(
|
||||
"Retorna o alfabeto completo: ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||
);
|
||||
this.setHelpUrl("");
|
||||
},
|
||||
};
|
||||
|
||||
// Bloco: Alfabeto Secreto (constante)
|
||||
Blockly.Blocks["alfabeto_secreto"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput().appendField("ALFABETO SECRETO");
|
||||
this.setOutput(true, "String");
|
||||
this.setColour(HUE_TEXTO);
|
||||
this.setTooltip(
|
||||
"Retorna o alfabeto embaralhado: QWERTYUIOPASDFGHJKLZXCVBNM",
|
||||
);
|
||||
this.setHelpUrl("");
|
||||
},
|
||||
};
|
||||
|
||||
// ============ BLOCOS DE VARIÁVEIS CUSTOMIZADAS ============
|
||||
|
||||
// Bloco: Definir letra
|
||||
Blockly.Blocks["definir_letra"] = {
|
||||
init: function () {
|
||||
this.appendValueInput("VALUE")
|
||||
.setCheck(null)
|
||||
.appendField("definir LETRA como");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(HUE_VARIAVEIS);
|
||||
this.setTooltip("Define o valor da variável LETRA");
|
||||
this.setHelpUrl("");
|
||||
},
|
||||
};
|
||||
|
||||
// Bloco: Obter letra
|
||||
Blockly.Blocks["obter_letra"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput().appendField("LETRA");
|
||||
this.setOutput(true, null);
|
||||
this.setColour(HUE_VARIAVEIS);
|
||||
this.setTooltip("Obtém o valor da variável LETRA");
|
||||
this.setHelpUrl("");
|
||||
},
|
||||
};
|
||||
|
||||
// Bloco: Definir posicao
|
||||
Blockly.Blocks["definir_posicao"] = {
|
||||
init: function () {
|
||||
this.appendValueInput("VALUE")
|
||||
.setCheck(null)
|
||||
.appendField("definir POSIÇÃO como");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(HUE_VARIAVEIS);
|
||||
this.setTooltip("Define o valor da variável POSIÇÃO");
|
||||
this.setHelpUrl("");
|
||||
},
|
||||
};
|
||||
|
||||
// Bloco: Obter posicao
|
||||
Blockly.Blocks["obter_posicao"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput().appendField("POSIÇÃO");
|
||||
this.setOutput(true, null);
|
||||
this.setColour(HUE_VARIAVEIS);
|
||||
this.setTooltip("Obtém o valor da variável POSIÇÃO");
|
||||
this.setHelpUrl("");
|
||||
},
|
||||
};
|
||||
|
||||
// Bloco: Definir nova_posicao
|
||||
Blockly.Blocks["definir_nova_posicao"] = {
|
||||
init: function () {
|
||||
this.appendValueInput("VALUE")
|
||||
.setCheck(null)
|
||||
.appendField("definir nova_posicao como");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(HUE_VARIAVEIS);
|
||||
this.setTooltip("Define o valor da variável nova_posicao");
|
||||
this.setHelpUrl("");
|
||||
},
|
||||
};
|
||||
|
||||
// Bloco: Obter nova_posicao
|
||||
Blockly.Blocks["obter_nova_posicao"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput().appendField("nova_posicao");
|
||||
this.setOutput(true, null);
|
||||
this.setColour(HUE_VARIAVEIS);
|
||||
this.setTooltip("Obtém o valor da variável nova_posicao");
|
||||
this.setHelpUrl("");
|
||||
},
|
||||
};
|
||||
|
||||
// Bloco: Definir nova_letra
|
||||
Blockly.Blocks["definir_nova_letra"] = {
|
||||
init: function () {
|
||||
this.appendValueInput("VALUE")
|
||||
.setCheck(null)
|
||||
.appendField("definir nova_letra como");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(HUE_VARIAVEIS);
|
||||
this.setTooltip("Define o valor da variável nova_letra");
|
||||
this.setHelpUrl("");
|
||||
},
|
||||
};
|
||||
|
||||
// Bloco: Obter nova_letra
|
||||
Blockly.Blocks["obter_nova_letra"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput().appendField("nova_letra");
|
||||
this.setOutput(true, null);
|
||||
this.setColour(HUE_VARIAVEIS);
|
||||
this.setTooltip("Obtém o valor da variável nova_letra");
|
||||
this.setHelpUrl("");
|
||||
},
|
||||
};
|
||||
|
||||
// Bloco: Definir chave
|
||||
Blockly.Blocks["definir_chave"] = {
|
||||
init: function () {
|
||||
this.appendValueInput("VALUE")
|
||||
.setCheck(null)
|
||||
.appendField("definir chave como");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(HUE_VARIAVEIS);
|
||||
this.setTooltip(
|
||||
"Define o valor da variável chave (deslocamento da cifra)",
|
||||
);
|
||||
this.setHelpUrl("");
|
||||
},
|
||||
};
|
||||
|
||||
// Bloco: Obter chave
|
||||
Blockly.Blocks["obter_chave"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput().appendField("chave");
|
||||
this.setOutput(true, null);
|
||||
this.setColour(HUE_VARIAVEIS);
|
||||
this.setTooltip("Obtém o valor da variável chave");
|
||||
this.setHelpUrl("");
|
||||
},
|
||||
};
|
||||
|
||||
// Bloco: Definir letra_secreta
|
||||
Blockly.Blocks["definir_letra_secreta"] = {
|
||||
init: function () {
|
||||
this.appendValueInput("VALUE")
|
||||
.setCheck(null)
|
||||
.appendField("definir LETRA_SECRETA como");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(HUE_VARIAVEIS);
|
||||
this.setTooltip("Define o valor da variável LETRA_SECRETA");
|
||||
this.setHelpUrl("");
|
||||
},
|
||||
};
|
||||
|
||||
// Bloco: Obter letra_secreta
|
||||
Blockly.Blocks["obter_letra_secreta"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput().appendField("LETRA_SECRETA");
|
||||
this.setOutput(true, null);
|
||||
this.setColour(HUE_VARIAVEIS);
|
||||
this.setTooltip("Obtém o valor da variável LETRA_SECRETA");
|
||||
this.setHelpUrl("");
|
||||
},
|
||||
};
|
||||
|
||||
// Bloco: Definir soma
|
||||
Blockly.Blocks["definir_soma"] = {
|
||||
init: function () {
|
||||
this.appendValueInput("VALUE")
|
||||
.setCheck(null)
|
||||
.appendField("definir SOMA como");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(HUE_VARIAVEIS);
|
||||
this.setTooltip("Define o valor da variável SOMA (acumulador)");
|
||||
this.setHelpUrl("");
|
||||
},
|
||||
};
|
||||
|
||||
// Bloco: Obter soma
|
||||
Blockly.Blocks["obter_soma"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput().appendField("SOMA");
|
||||
this.setOutput(true, null);
|
||||
this.setColour(HUE_VARIAVEIS);
|
||||
this.setTooltip("Obtém o valor da variável SOMA");
|
||||
this.setHelpUrl("");
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const defineGenerators = () => {
|
||||
javascriptGenerator.STATEMENT_PREFIX = "highlightBlock(%1);\n";
|
||||
javascriptGenerator.addReservedWords("highlightBlock");
|
||||
|
||||
// Gerador: Definir Entrada
|
||||
javascriptGenerator.forBlock["definir_entrada"] = function (block) {
|
||||
const value =
|
||||
javascriptGenerator.valueToCode(
|
||||
block,
|
||||
"VALUE",
|
||||
javascriptGenerator.ORDER_ATOMIC,
|
||||
) || "''";
|
||||
return "definirEntrada(" + value + ");\n";
|
||||
};
|
||||
|
||||
// Gerador: Definir Saída
|
||||
javascriptGenerator.forBlock["definir_saida"] = function (block) {
|
||||
const value =
|
||||
javascriptGenerator.valueToCode(
|
||||
block,
|
||||
"VALUE",
|
||||
javascriptGenerator.ORDER_ATOMIC,
|
||||
) || "''";
|
||||
return "definirSaida(" + value + ");\n";
|
||||
};
|
||||
|
||||
// Gerador: Definir Contador
|
||||
javascriptGenerator.forBlock["definir_contador"] = function (block) {
|
||||
const value =
|
||||
javascriptGenerator.valueToCode(
|
||||
block,
|
||||
"VALUE",
|
||||
javascriptGenerator.ORDER_ATOMIC,
|
||||
) || "''";
|
||||
return "definirContador(" + value + ");\n";
|
||||
};
|
||||
|
||||
// Gerador: Concatenar Saída
|
||||
javascriptGenerator.forBlock["concatenar_saida"] = function (block) {
|
||||
const value =
|
||||
javascriptGenerator.valueToCode(
|
||||
block,
|
||||
"VALUE",
|
||||
javascriptGenerator.ORDER_ATOMIC,
|
||||
) || "''";
|
||||
return "concatenarSaida(" + value + ");\n";
|
||||
};
|
||||
|
||||
// Gerador: Obter Entrada
|
||||
javascriptGenerator.forBlock["obter_entrada"] = function (block) {
|
||||
const code = "obterEntrada()";
|
||||
return [code, javascriptGenerator.ORDER_FUNCTION_CALL];
|
||||
};
|
||||
|
||||
// Gerador: Obter Saída
|
||||
javascriptGenerator.forBlock["obter_saida"] = function (block) {
|
||||
const code = "obterSaida()";
|
||||
return [code, javascriptGenerator.ORDER_FUNCTION_CALL];
|
||||
};
|
||||
|
||||
// Gerador: Obter Contador
|
||||
javascriptGenerator.forBlock["obter_contador"] = function (block) {
|
||||
const code = "obterContador()";
|
||||
return [code, javascriptGenerator.ORDER_FUNCTION_CALL];
|
||||
};
|
||||
|
||||
// Gerador: Alfabeto
|
||||
javascriptGenerator.forBlock["alfabeto"] = function (block) {
|
||||
const code = '"ABCDEFGHIJKLMNOPQRSTUVWXYZ"';
|
||||
return [code, javascriptGenerator.ORDER_ATOMIC];
|
||||
};
|
||||
|
||||
// Gerador: Alfabeto Secreto
|
||||
javascriptGenerator.forBlock["alfabeto_secreto"] = function (block) {
|
||||
const code = '"QWERTYUIOPASDFGHJKLZXCVBNM"';
|
||||
return [code, javascriptGenerator.ORDER_ATOMIC];
|
||||
};
|
||||
|
||||
// Gerador customizado: text_charAt (0-based, não subtrai 1)
|
||||
// Assume que todos os índices fornecidos já são 0-based (compatível com CONTADOR = 0)
|
||||
javascriptGenerator.forBlock["text_charAt"] = function (block) {
|
||||
const text =
|
||||
javascriptGenerator.valueToCode(
|
||||
block,
|
||||
"VALUE",
|
||||
javascriptGenerator.ORDER_MEMBER,
|
||||
) || "''";
|
||||
const at =
|
||||
javascriptGenerator.valueToCode(
|
||||
block,
|
||||
"AT",
|
||||
javascriptGenerator.ORDER_NONE,
|
||||
) || "0";
|
||||
const code = text + ".charAt(" + at + ")";
|
||||
return [code, javascriptGenerator.ORDER_MEMBER];
|
||||
};
|
||||
|
||||
// Gerador customizado: text_indexOf (0-based, não adiciona 1)
|
||||
// Retorna o índice 0-based direto, compatível com charAt e arrays JavaScript
|
||||
javascriptGenerator.forBlock["text_indexOf"] = function (block) {
|
||||
const text =
|
||||
javascriptGenerator.valueToCode(
|
||||
block,
|
||||
"VALUE",
|
||||
javascriptGenerator.ORDER_MEMBER,
|
||||
) || "''";
|
||||
const search =
|
||||
javascriptGenerator.valueToCode(
|
||||
block,
|
||||
"FIND",
|
||||
javascriptGenerator.ORDER_NONE,
|
||||
) || "''";
|
||||
const code = text + ".indexOf(" + search + ")";
|
||||
return [code, javascriptGenerator.ORDER_MEMBER];
|
||||
};
|
||||
|
||||
// ============ GERADORES PARA VARIÁVEIS CUSTOMIZADAS ============
|
||||
|
||||
// Geradores: letra
|
||||
javascriptGenerator.forBlock["definir_letra"] = function (block) {
|
||||
const value =
|
||||
javascriptGenerator.valueToCode(
|
||||
block,
|
||||
"VALUE",
|
||||
javascriptGenerator.ORDER_ATOMIC,
|
||||
) || "''";
|
||||
return "var letra = " + value + ";\n";
|
||||
};
|
||||
|
||||
javascriptGenerator.forBlock["obter_letra"] = function (block) {
|
||||
return ["letra", javascriptGenerator.ORDER_ATOMIC];
|
||||
};
|
||||
|
||||
// Geradores: posicao
|
||||
javascriptGenerator.forBlock["definir_posicao"] = function (block) {
|
||||
const value =
|
||||
javascriptGenerator.valueToCode(
|
||||
block,
|
||||
"VALUE",
|
||||
javascriptGenerator.ORDER_ATOMIC,
|
||||
) || "0";
|
||||
return "var posicao = " + value + ";\n";
|
||||
};
|
||||
|
||||
javascriptGenerator.forBlock["obter_posicao"] = function (block) {
|
||||
return ["posicao", javascriptGenerator.ORDER_ATOMIC];
|
||||
};
|
||||
|
||||
// Geradores: nova_posicao
|
||||
javascriptGenerator.forBlock["definir_nova_posicao"] = function (block) {
|
||||
const value =
|
||||
javascriptGenerator.valueToCode(
|
||||
block,
|
||||
"VALUE",
|
||||
javascriptGenerator.ORDER_ATOMIC,
|
||||
) || "0";
|
||||
return "var nova_posicao = " + value + ";\n";
|
||||
};
|
||||
|
||||
javascriptGenerator.forBlock["obter_nova_posicao"] = function (block) {
|
||||
return ["nova_posicao", javascriptGenerator.ORDER_ATOMIC];
|
||||
};
|
||||
|
||||
// Geradores: nova_letra
|
||||
javascriptGenerator.forBlock["definir_nova_letra"] = function (block) {
|
||||
const value =
|
||||
javascriptGenerator.valueToCode(
|
||||
block,
|
||||
"VALUE",
|
||||
javascriptGenerator.ORDER_ATOMIC,
|
||||
) || "''";
|
||||
return "var nova_letra = " + value + ";\n";
|
||||
};
|
||||
|
||||
javascriptGenerator.forBlock["obter_nova_letra"] = function (block) {
|
||||
return ["nova_letra", javascriptGenerator.ORDER_ATOMIC];
|
||||
};
|
||||
|
||||
// Geradores: chave
|
||||
javascriptGenerator.forBlock["definir_chave"] = function (block) {
|
||||
const value =
|
||||
javascriptGenerator.valueToCode(
|
||||
block,
|
||||
"VALUE",
|
||||
javascriptGenerator.ORDER_ATOMIC,
|
||||
) || "0";
|
||||
return "var chave = " + value + ";\n";
|
||||
};
|
||||
|
||||
javascriptGenerator.forBlock["obter_chave"] = function (block) {
|
||||
return ["chave", javascriptGenerator.ORDER_ATOMIC];
|
||||
};
|
||||
|
||||
// Geradores: letra_secreta
|
||||
javascriptGenerator.forBlock["definir_letra_secreta"] = function (block) {
|
||||
const value =
|
||||
javascriptGenerator.valueToCode(
|
||||
block,
|
||||
"VALUE",
|
||||
javascriptGenerator.ORDER_ATOMIC,
|
||||
) || "''";
|
||||
return "var letra_secreta = " + value + ";\n";
|
||||
};
|
||||
|
||||
javascriptGenerator.forBlock["obter_letra_secreta"] = function (block) {
|
||||
return ["letra_secreta", javascriptGenerator.ORDER_ATOMIC];
|
||||
};
|
||||
|
||||
// Geradores: soma
|
||||
javascriptGenerator.forBlock["definir_soma"] = function (block) {
|
||||
const value =
|
||||
javascriptGenerator.valueToCode(
|
||||
block,
|
||||
"VALUE",
|
||||
javascriptGenerator.ORDER_ATOMIC,
|
||||
) || "0";
|
||||
return "var soma = " + value + ";\n";
|
||||
};
|
||||
|
||||
javascriptGenerator.forBlock["obter_soma"] = function (block) {
|
||||
return ["soma", javascriptGenerator.ORDER_ATOMIC];
|
||||
};
|
||||
};
|
||||
175
app/src/atividades/programacao/cripto/config/codeValidations.js
Normal file
@@ -0,0 +1,175 @@
|
||||
/**
|
||||
* @fileoverview Utility module for codeValidations.js
|
||||
*
|
||||
* @module games.cripto.config.codeValidations
|
||||
*/
|
||||
|
||||
// Código de validações para prevenir loops infinitos e erros comuns nas fases do jogo.
|
||||
// As validações são usadas pelo BaseGameScene/config.js para bloquear execuções inseguras.
|
||||
|
||||
/**
|
||||
* Valida se um loop while contém incremento ou decremento do contador
|
||||
* Previne loops infinitos onde o contador nunca muda
|
||||
*/
|
||||
export function validateWhileWithCounter(code) {
|
||||
// Verifica se há um loop while no código
|
||||
const hasWhile = /while\s*\(/i.test(code);
|
||||
if (!hasWhile) return { valid: true };
|
||||
|
||||
// Verifica se o contador é usado na condição do while
|
||||
const whileWithCounter = /while\s*\([^)]*[cC]ontador[^)]*\)/i.test(code);
|
||||
if (!whileWithCounter) {
|
||||
// Se não usa contador na condição, não validamos (pode ser outro tipo de loop)
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
// Verifica se há incremento/decremento do contador dentro do loop
|
||||
const hasCounterIncrement =
|
||||
/(definirContador|contador\s*[+-]=|\+\+contador|contador\+\+|--contador|contador--)/i.test(
|
||||
code,
|
||||
);
|
||||
|
||||
if (!hasCounterIncrement) {
|
||||
return {
|
||||
valid: false,
|
||||
message:
|
||||
'Loop Infinito Detectado!\n\nSeu loop WHILE usa o CONTADOR na condição, mas não há incremento/decremento do CONTADOR dentro do loop.\n\nIsso causará um loop infinito!\n\nSolução: Adicione um bloco "definir CONTADOR" para incrementar o contador dentro do loop.',
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Valida se o loop while tem uma condição que pode eventualmente se tornar falsa
|
||||
*/
|
||||
export function validateWhileCondition(code) {
|
||||
// Detecta condições sempre verdadeiras óbvias
|
||||
const alwaysTruePatterns = [
|
||||
/while\s*\(\s*true\s*\)/i,
|
||||
/while\s*\(\s*1\s*\)/i,
|
||||
/while\s*\(\s*"[^"]+"\s*\)/i, // string não vazia
|
||||
];
|
||||
|
||||
for (const pattern of alwaysTruePatterns) {
|
||||
if (pattern.test(code)) {
|
||||
return {
|
||||
valid: false,
|
||||
message:
|
||||
'Loop Infinito Detectado!\n\nSua condição do WHILE é sempre verdadeira, o que causará um loop infinito.\n\nSolução: Use uma condição que possa se tornar falsa, como "CONTADOR < comprimento de ENTRADA".',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Valida se o loop while que usa length() modifica o que está sendo iterado
|
||||
*/
|
||||
export function validateWhileWithLength(code) {
|
||||
const hasWhileWithLength = /while\s*\([^)]*\.length[^)]*\)/i.test(code);
|
||||
if (!hasWhileWithLength) return { valid: true };
|
||||
|
||||
// Verifica se há chamada para obterEntrada() dentro da condição
|
||||
const hasGetInputInCondition = /while\s*\([^)]*obterEntrada\(\)[^)]*\)/i.test(
|
||||
code,
|
||||
);
|
||||
if (!hasGetInputInCondition) return { valid: true };
|
||||
|
||||
// Verifica se há incremento do contador
|
||||
const hasCounterIncrement =
|
||||
/(definirContador|contador\s*[+-]=|\+\+contador|contador\+\+)/i.test(
|
||||
code,
|
||||
);
|
||||
|
||||
if (!hasCounterIncrement) {
|
||||
return {
|
||||
valid: false,
|
||||
message:
|
||||
"Loop Infinito Detectado!\n\nSeu loop usa o comprimento da ENTRADA, mas não incrementa o CONTADOR.\n\nIsso causará um loop infinito!\n\nSolução: Adicione incremento do CONTADOR dentro do loop.",
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Valida se há pelo menos um loop no código (para fases que exigem iteração)
|
||||
*/
|
||||
export function validateHasLoop(code) {
|
||||
const hasLoop = /while\s*\(|for\s*\(/i.test(code);
|
||||
|
||||
if (!hasLoop) {
|
||||
return {
|
||||
valid: false,
|
||||
message:
|
||||
"Loop Necessário!\n\nEsta fase requer que você use um loop (WHILE) para processar cada caractere da entrada.\n\nSolução: Use um bloco WHILE para percorrer a entrada caractere por caractere.",
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Valida se o código define a entrada antes de usá-la
|
||||
*/
|
||||
export function validateInputBeforeUse(code) {
|
||||
const hasGetInput = /obterEntrada\(\)/i.test(code);
|
||||
if (!hasGetInput) return { valid: true }; // Não usa entrada, ok
|
||||
|
||||
const hasSetInput = /definirEntrada\(/i.test(code);
|
||||
|
||||
if (!hasSetInput) {
|
||||
return {
|
||||
valid: false,
|
||||
message:
|
||||
'Entrada não definida!\n\nVocê está tentando obter a ENTRADA sem defini-la primeiro.\n\nSolução: Use o bloco "definir ENTRADA" antes de usar "obter ENTRADA".',
|
||||
};
|
||||
}
|
||||
|
||||
// Verifica ordem (definir antes de obter) - simplificado
|
||||
const setInputIndex = code.search(/definirEntrada\(/i);
|
||||
const getInputIndex = code.search(/obterEntrada\(\)/i);
|
||||
|
||||
if (getInputIndex < setInputIndex) {
|
||||
return {
|
||||
valid: false,
|
||||
message:
|
||||
'Ordem incorreta!\n\nVocê está tentando obter a ENTRADA antes de defini-la.\n\nSolução: Mova o bloco "definir ENTRADA" para antes de "obter ENTRADA".',
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Combina múltiplas validações
|
||||
*/
|
||||
export function validateCode(code, validations = []) {
|
||||
for (const validation of validations) {
|
||||
const result = validation(code);
|
||||
if (!result.valid) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validações padrão para fases com loops
|
||||
*/
|
||||
export const defaultLoopValidations = [
|
||||
validateWhileCondition,
|
||||
validateWhileWithCounter,
|
||||
validateWhileWithLength,
|
||||
validateInputBeforeUse,
|
||||
];
|
||||
|
||||
/**
|
||||
* Validações para fases que exigem loops
|
||||
*/
|
||||
export const requiredLoopValidations = [
|
||||
validateHasLoop,
|
||||
...defaultLoopValidations,
|
||||
];
|
||||
419
app/src/atividades/programacao/cripto/config/config.js
Normal file
@@ -0,0 +1,419 @@
|
||||
/**
|
||||
* @fileoverview Utility module for config.js
|
||||
*
|
||||
* @module games.cripto.config.config
|
||||
*/
|
||||
|
||||
export const gameConfig = {
|
||||
gameId: "cripto",
|
||||
gameName: "Cripto",
|
||||
type: "blocks",
|
||||
icon: "🔐",
|
||||
thumbnail: "/images/atividades/programacao/cripto-thumbnail.png",
|
||||
descricao:
|
||||
"Aprenda fundamentos de criptografia e segurança cibernética programando blocos para proteger dados e comunicações.",
|
||||
dificuldade: "Avançado",
|
||||
categoria: "Lógica",
|
||||
tempoEstimado: "45-60 min",
|
||||
conceitos: [
|
||||
"Criptografia",
|
||||
"Repetição",
|
||||
"Variaveis",
|
||||
"Funções",
|
||||
"Condicionais",
|
||||
],
|
||||
route: "/atividades/programacao/cripto",
|
||||
component: "CriptoGame",
|
||||
objectives: [
|
||||
"Entender os conceitos básicos de criptografia",
|
||||
"Implementar algoritmos de criptografia simples",
|
||||
"Aplicar lógica de programação para resolver desafios de segurança",
|
||||
],
|
||||
metadata: {
|
||||
lastUpdated: "2026-02-12",
|
||||
version: "1.0.0",
|
||||
},
|
||||
|
||||
fases: [
|
||||
{
|
||||
id: 1,
|
||||
nome: "De Letra para Número",
|
||||
descricao:
|
||||
'Converta cada letra do alfabeto em sua posição numérica (A=0, B=1, C=2...). Primeiro, defina a ENTRADA como o alfabeto completo "ABCDEFGHIJKLMNOPQRSTUVWXYZ". Depois, use um loop "enquanto" para percorrer cada posição e adicionar o número correspondente à SAÍDA. Dica: o CONTADOR já representa a posição da letra!',
|
||||
timeout: 30,
|
||||
allowedBlocks: [
|
||||
"obter_contador",
|
||||
"definir_contador",
|
||||
"definir_entrada",
|
||||
"obter_entrada",
|
||||
"definir_saida",
|
||||
"concatenar_saida",
|
||||
"obter_saida",
|
||||
"text_length",
|
||||
"controls_whileUntil",
|
||||
"logic_compare",
|
||||
"math_number",
|
||||
"math_arithmetic",
|
||||
"text",
|
||||
],
|
||||
/**
|
||||
* Garante que há um loop com incremento do contador para evitar loop infinito
|
||||
*/
|
||||
validationRegex: /(while|for)[\s\S]*definirContador[\s\S]*\+/,
|
||||
expectedInput: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||
expectedOutput: "012345678910111213141516171819202122232425",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
nome: "De Número para Letra",
|
||||
descricao:
|
||||
'Reverta o processo! A ENTRADA contém dígitos "0123456789" (cada dígito é a posição de uma letra). Crie a variável "letra" para guardar cada caractere. Use o bloco "no texto obter letra #" para pegar cada dígito, depois busque no ALFABETO a letra correspondente. Por exemplo: dígito "0" → letra "A", dígito "1" → letra "B".',
|
||||
timeout: 30,
|
||||
allowedBlocks: [
|
||||
"obter_contador",
|
||||
"definir_contador",
|
||||
"definir_entrada",
|
||||
"obter_entrada",
|
||||
"definir_saida",
|
||||
"concatenar_saida",
|
||||
"text_length",
|
||||
"text_charAt",
|
||||
"controls_whileUntil",
|
||||
"logic_compare",
|
||||
"math_number",
|
||||
"math_arithmetic",
|
||||
"text",
|
||||
"alfabeto",
|
||||
"definir_letra",
|
||||
"obter_letra",
|
||||
],
|
||||
/**
|
||||
* Garante que há um loop com incremento do contador
|
||||
*/
|
||||
validationRegex: /(while|for)[\s\S]*definirContador[\s\S]*\+/,
|
||||
expectedInput: "0123456789",
|
||||
expectedOutput: "ABCDEFGHIJ",
|
||||
alfabeto: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
nome: "Cifra de César (+3)",
|
||||
descricao:
|
||||
'Implemente a Cifra de César com deslocamento fixo de +3. Para a ENTRADA "TECNOLOGIA", cada letra deve avançar 3 posições (exemplo: A→D, B→E, C→F). Crie variáveis: "letra" (use "no texto obter letra #"), "posicao" (use "no texto encontrar primeira ocorrência de" para achar a letra no ALFABETO), "nova_posicao" (calcule: (posicao + 3) RESTO DA DIVISÃO POR 26 - use o bloco "resto da divisão de... por..."), e "nova_letra" (pegue do ALFABETO na nova_posicao).',
|
||||
timeout: 30,
|
||||
allowedBlocks: [
|
||||
"obter_contador",
|
||||
"definir_contador",
|
||||
"definir_entrada",
|
||||
"obter_entrada",
|
||||
"definir_saida",
|
||||
"concatenar_saida",
|
||||
"text_length",
|
||||
"text_charAt",
|
||||
"text_indexOf",
|
||||
"controls_whileUntil",
|
||||
"logic_compare",
|
||||
"math_number",
|
||||
"math_arithmetic",
|
||||
"math_modulo",
|
||||
"text",
|
||||
"alfabeto",
|
||||
"definir_letra",
|
||||
"obter_letra",
|
||||
"definir_posicao",
|
||||
"obter_posicao",
|
||||
"definir_nova_posicao",
|
||||
"obter_nova_posicao",
|
||||
"definir_nova_letra",
|
||||
"obter_nova_letra",
|
||||
],
|
||||
/**
|
||||
* Garante loop com incremento de contador
|
||||
*/
|
||||
validationRegex: /(while|for)[\s\S]*definirContador[\s\S]*\+/,
|
||||
expectedInput: "TECNOLOGIA",
|
||||
expectedOutput: "WHFQRORJLD",
|
||||
alfabeto: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||
chave: 3,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
nome: "Cifra de César (-3)",
|
||||
descricao:
|
||||
'Descriptografe uma mensagem cifrada com deslocamento -3. Para a ENTRADA "GHFRGD", volte 3 posições: G→D, H→E, etc. Crie as mesmas variáveis da fase anterior: "letra", "posicao", "nova_posicao" e "nova_letra". Importante: calcule nova_posicao como (posicao - 3 + 26) RESTO DA DIVISÃO POR 26 (use o bloco "resto da divisão de... por..."). O +26 evita números negativos!',
|
||||
timeout: 30,
|
||||
allowedBlocks: [
|
||||
"obter_contador",
|
||||
"definir_contador",
|
||||
"definir_entrada",
|
||||
"obter_entrada",
|
||||
"definir_saida",
|
||||
"concatenar_saida",
|
||||
"text_length",
|
||||
"text_charAt",
|
||||
"text_indexOf",
|
||||
"controls_whileUntil",
|
||||
"logic_compare",
|
||||
"math_number",
|
||||
"math_arithmetic",
|
||||
"math_modulo",
|
||||
"text",
|
||||
"alfabeto",
|
||||
"definir_letra",
|
||||
"obter_letra",
|
||||
"definir_posicao",
|
||||
"obter_posicao",
|
||||
"definir_nova_posicao",
|
||||
"obter_nova_posicao",
|
||||
"definir_nova_letra",
|
||||
"obter_nova_letra",
|
||||
],
|
||||
/**
|
||||
* Garante loop com incremento de contador
|
||||
*/
|
||||
validationRegex: /(while|for)[\s\S]*definirContador[\s\S]*\+/,
|
||||
expectedInput: "GHFRGD",
|
||||
expectedOutput: "DECODA",
|
||||
alfabeto: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||
chave: -3,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
nome: "Criptografia com Chave Variável",
|
||||
descricao:
|
||||
'Use uma chave customizada para criptografar! Para a ENTRADA "NUCLEO" com chave 5, desloque cada letra 5 posições. Crie a variável "chave" com valor 5. Depois use as mesmas variáveis das fases anteriores: "letra", "posicao", "nova_posicao" (use o bloco "resto da divisão" para calcular (posicao + chave) % 26) e "nova_letra". Na fórmula, use a variável "chave" em vez do número fixo 3.',
|
||||
timeout: 30,
|
||||
allowedBlocks: [
|
||||
"obter_contador",
|
||||
"definir_contador",
|
||||
"definir_entrada",
|
||||
"obter_entrada",
|
||||
"definir_saida",
|
||||
"concatenar_saida",
|
||||
"text_length",
|
||||
"text_charAt",
|
||||
"text_indexOf",
|
||||
"controls_whileUntil",
|
||||
"logic_compare",
|
||||
"math_number",
|
||||
"math_arithmetic",
|
||||
"math_modulo",
|
||||
"text",
|
||||
"alfabeto",
|
||||
"definir_letra",
|
||||
"obter_letra",
|
||||
"definir_posicao",
|
||||
"obter_posicao",
|
||||
"definir_nova_posicao",
|
||||
"obter_nova_posicao",
|
||||
"definir_nova_letra",
|
||||
"obter_nova_letra",
|
||||
"definir_chave",
|
||||
"obter_chave",
|
||||
],
|
||||
/**
|
||||
* Garante loop com incremento de contador
|
||||
*/
|
||||
validationRegex: /(while|for)[\s\S]*definirContador[\s\S]*\+/,
|
||||
expectedInput: "NUCLEO",
|
||||
expectedOutput: "SZHQJT",
|
||||
alfabeto: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||
chave: 5,
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
nome: "Descriptografia com Chave Variável",
|
||||
descricao:
|
||||
'Desfaça a criptografia anterior! Para a ENTRADA "SZHQJT" com chave 5, volte 5 posições. Use as mesmas variáveis da Fase 5, mas agora calcule nova_posicao como: (posicao - chave + 26) RESTO DA DIVISÃO POR 26 (use o bloco "resto da divisão de... por..."). Lembre-se: sempre some 26 antes do módulo quando subtrair!',
|
||||
timeout: 30,
|
||||
allowedBlocks: [
|
||||
"obter_contador",
|
||||
"definir_contador",
|
||||
"definir_entrada",
|
||||
"obter_entrada",
|
||||
"definir_saida",
|
||||
"concatenar_saida",
|
||||
"text_length",
|
||||
"text_charAt",
|
||||
"text_indexOf",
|
||||
"controls_whileUntil",
|
||||
"logic_compare",
|
||||
"math_number",
|
||||
"math_arithmetic",
|
||||
"math_modulo",
|
||||
"text",
|
||||
"alfabeto",
|
||||
"definir_letra",
|
||||
"obter_letra",
|
||||
"definir_posicao",
|
||||
"obter_posicao",
|
||||
"definir_nova_posicao",
|
||||
"obter_nova_posicao",
|
||||
"definir_nova_letra",
|
||||
"obter_nova_letra",
|
||||
"definir_chave",
|
||||
"obter_chave",
|
||||
],
|
||||
/**
|
||||
* Garante loop com incremento de contador
|
||||
*/
|
||||
validationRegex: /(while|for)[\s\S]*definirContador[\s\S]*\+/,
|
||||
expectedInput: "SZHQJT",
|
||||
expectedOutput: "NUCLEO",
|
||||
alfabeto: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||
chave: 5,
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
nome: "Código Leetspeak",
|
||||
descricao:
|
||||
'Use condicionais para substituir letras específicas por números! Para a ENTRADA "SOBERANIA", substitua A→4, E→3, S→5 e I→1, mantendo as outras iguais. Crie a variável "letra" para pegar cada caractere (use "no texto obter letra #"). Depois use blocos SE-SENÃO: se letra = "A" então concatene "4", senão se letra = "E" então concatene "3", senão se letra = "S" então concatene "5", senão se letra = "I" então concatene "1", senão concatene a própria letra.',
|
||||
timeout: 30,
|
||||
allowedBlocks: [
|
||||
"obter_contador",
|
||||
"definir_contador",
|
||||
"definir_entrada",
|
||||
"obter_entrada",
|
||||
"definir_saida",
|
||||
"concatenar_saida",
|
||||
"text_length",
|
||||
"text_charAt",
|
||||
"controls_whileUntil",
|
||||
"controls_if",
|
||||
"logic_compare",
|
||||
"math_number",
|
||||
"text",
|
||||
"definir_letra",
|
||||
"definir_nova_letra",
|
||||
"obter_nova_letra",
|
||||
"obter_letra",
|
||||
],
|
||||
/**
|
||||
* Garante loop com incremento de contador
|
||||
*/
|
||||
validationRegex: /(while|for)[\s\S]*definirContador[\s\S]*\+/,
|
||||
expectedInput: "SOBERANIA",
|
||||
expectedOutput: "5OB3R4N14",
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
nome: "Mensagem Invertida",
|
||||
descricao:
|
||||
'Inverta a ordem das letras! Para a ENTRADA "DECODA", o resultado deve ser "ADOCED". Duas abordagens: (1) Inicie CONTADOR com (comprimento - 1) e decremente até 0, OU (2) Crie variável "letra", pegue cada caractere e use o bloco "criar texto com" para juntar: primeira entrada = letra, segunda entrada = SAÍDA atual. Isso coloca cada letra ANTES das anteriores.',
|
||||
timeout: 30,
|
||||
allowedBlocks: [
|
||||
"obter_contador",
|
||||
"definir_contador",
|
||||
"definir_entrada",
|
||||
"obter_entrada",
|
||||
"definir_saida",
|
||||
"concatenar_saida",
|
||||
"obter_saida",
|
||||
"text_length",
|
||||
"text_charAt",
|
||||
"text_join",
|
||||
"controls_whileUntil",
|
||||
"logic_compare",
|
||||
"math_number",
|
||||
"math_arithmetic",
|
||||
"text",
|
||||
"definir_letra",
|
||||
"obter_letra",
|
||||
],
|
||||
/**
|
||||
* Garante loop com incremento de contador (ou decremento para inversão)
|
||||
*/
|
||||
validationRegex: /(while|for)[\s\S]*definirContador[\s\S]*(\+|-)/,
|
||||
expectedInput: "DECODA",
|
||||
expectedOutput: "ADOCED",
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
nome: "Alfabeto Secreto",
|
||||
descricao:
|
||||
'Use um alfabeto embaralhado para cifrar! Para a ENTRADA "FENALUTA", use os blocos ALFABETO (normal) e ALFABETO_SECRETO (embaralhado). Crie variáveis: "letra" (pegue com "no texto obter letra #"), "posicao" (use "no texto encontrar" para buscar a letra no ALFABETO normal), e "letra_secreta" (pegue com "no texto obter letra #" na mesma posição do ALFABETO_SECRETO). Exemplo: R está na posição 17 do normal → posição 17 do secreto é A.',
|
||||
timeout: 30,
|
||||
allowedBlocks: [
|
||||
"obter_contador",
|
||||
"definir_contador",
|
||||
"definir_entrada",
|
||||
"obter_entrada",
|
||||
"definir_saida",
|
||||
"concatenar_saida",
|
||||
"text_length",
|
||||
"text_charAt",
|
||||
"text_indexOf",
|
||||
"controls_whileUntil",
|
||||
"logic_compare",
|
||||
"math_number",
|
||||
"text",
|
||||
"alfabeto",
|
||||
"alfabeto_secreto",
|
||||
"definir_letra",
|
||||
"obter_letra",
|
||||
"definir_posicao",
|
||||
"obter_posicao",
|
||||
"definir_letra_secreta",
|
||||
"obter_letra_secreta",
|
||||
],
|
||||
/**
|
||||
* Garante loop com incremento de contador
|
||||
*/
|
||||
validationRegex: /(while|for)[\s\S]*definirContador[\s\S]*\+/,
|
||||
expectedInput: "FENALUTA",
|
||||
expectedOutput: "YTFQSXZQ",
|
||||
alfabeto: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||
alfabetoSecreto: "QWERTYUIOPASDFGHJKLZXCVBNM",
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
nome: "Somador de Integridade (Hash)",
|
||||
descricao:
|
||||
'Para garantir que uma mensagem não foi alterada, usamos um "Hash" — uma assinatura numérica única. Para cada letra da ENTRADA "CRIPTOGRAFIA", descubra sua posição no ALFABETO e atualize a variável "SOMA" usando a fórmula: (SOMA * 31 + posicao). Para manter o número dentro de um limite, use o RESTO DA DIVISÃO por 1000000007. Dentro do loop, chame "definir SAÍDA como SOMA" para ver o hash atualizando letra a letra.',
|
||||
timeout: 30,
|
||||
allowedBlocks: [
|
||||
"obter_contador",
|
||||
"definir_contador",
|
||||
"definir_entrada",
|
||||
"obter_entrada",
|
||||
"definir_saida",
|
||||
"concatenar_saida",
|
||||
"text_length",
|
||||
"text_charAt",
|
||||
"text_indexOf",
|
||||
"controls_whileUntil",
|
||||
"logic_compare",
|
||||
"math_number",
|
||||
"math_arithmetic",
|
||||
"math_modulo",
|
||||
"text",
|
||||
"alfabeto",
|
||||
"definir_letra",
|
||||
"obter_letra",
|
||||
"definir_posicao",
|
||||
"obter_posicao",
|
||||
"definir_soma",
|
||||
"obter_soma",
|
||||
],
|
||||
/**
|
||||
* Garante loop com incremento de contador
|
||||
*/
|
||||
validationRegex: /(while|for)[\s\S]*definirContador[\s\S]*\+/,
|
||||
expectedInput: "CRIPTOGRAFIA",
|
||||
expectedOutput: "911701368",
|
||||
alfabeto: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||
},
|
||||
],
|
||||
mensagens: {
|
||||
entradaIncorreta:
|
||||
"Entrada incorreta. Você deve definir a ENTRADA como o alfabeto completo.",
|
||||
saidaIncorreta:
|
||||
"Saída incorreta. Cada letra deve ser convertida para sua posição numérica (A=0, B=1, C=2...). O CONTADOR já representa essa posição!",
|
||||
erroGeral: "Algo deu errado durante a execução. Verifique seu código.",
|
||||
erroEstrutura:
|
||||
'Loop Infinito Detectado!\n\nSeu código não incrementa o CONTADOR dentro do loop WHILE. Isso causará um loop infinito!\n\n💡 Solução:\nAdicione um bloco "definir CONTADOR como (obter CONTADOR + 1)" dentro do loop para que o contador avance a cada iteração.',
|
||||
sucessoGenerico: "Parabéns! Você completou o desafio!",
|
||||
timeoutExcedido:
|
||||
"O tempo de execução foi excedido. Verifique se não há loops infinitos ou se o contador está sendo incrementado corretamente.",
|
||||
},
|
||||
};
|
||||
2430
app/src/atividades/programacao/cripto/config/debugSolutions.js
Normal file
1910
app/src/atividades/programacao/cripto/config/starterBlocks.js
Normal file
73
app/src/atividades/programacao/cripto/config/tourSteps.js
Normal file
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* @fileoverview Utility module for tourSteps.js
|
||||
*
|
||||
* @module games.cripto.config.tourSteps
|
||||
*/
|
||||
|
||||
import {
|
||||
createWelcomeStep,
|
||||
createGameAreaStep,
|
||||
createToolboxStep,
|
||||
createWorkspaceStep,
|
||||
createRunButtonStep,
|
||||
createResetInfoStep,
|
||||
createPhaseSelectorStep,
|
||||
createPhaseInfoStep,
|
||||
createHelpButtonStep,
|
||||
gameIcons,
|
||||
defaultGameTourOptions,
|
||||
} from "../../../../utils/tourHelpers";
|
||||
|
||||
export const criptoTourSteps = [
|
||||
createWelcomeStep({
|
||||
gameName: "Jogo Cripto",
|
||||
description:
|
||||
"Bem-vindo ao mundo da criptografia! Aqui você vai aprender os fundamentos de segurança digital e como transformar informações.",
|
||||
challenge:
|
||||
"Use programação em blocos para converter letras em números e implementar algoritmos de criptografia!",
|
||||
iconSvg: gameIcons.lock,
|
||||
}),
|
||||
|
||||
createGameAreaStep({
|
||||
title: "Monitor Criptográfico",
|
||||
description:
|
||||
"Nesta tela você verá três áreas: valores de ENTRADA e SAÍDA. Seus blocos transformarão a entrada em saída criptografada.",
|
||||
}),
|
||||
|
||||
createToolboxStep({
|
||||
description:
|
||||
"Use os blocos disponíveis: definir entrada/saída, obter valores, concatenar texto, contador, loops e condicionais. Arraste-os para criar seu algoritmo.",
|
||||
}),
|
||||
|
||||
createWorkspaceStep({
|
||||
description:
|
||||
"Monte sua sequência lógica aqui. Encaixe os blocos na ordem correta para processar a entrada e gerar a saída esperada.",
|
||||
}),
|
||||
|
||||
createRunButtonStep({
|
||||
description:
|
||||
"Execute seu código! Você verá as animações acontecendo passo a passo: cursor piscando na entrada e caracteres embaralhando na saída.",
|
||||
}),
|
||||
|
||||
createResetInfoStep({
|
||||
description:
|
||||
"Se algo não funcionar como esperado, use o reset para limpar e tentar uma nova solução.",
|
||||
}),
|
||||
|
||||
createPhaseSelectorStep({
|
||||
description:
|
||||
"O jogo tem várias fases com diferentes desafios de criptografia, desde conversão básica até algoritmos mais complexos.",
|
||||
}),
|
||||
|
||||
createPhaseInfoStep({
|
||||
description:
|
||||
"Acompanhe seu progresso e veja informações sobre a fase atual aqui.",
|
||||
}),
|
||||
|
||||
createHelpButtonStep({
|
||||
description:
|
||||
"Acesse este tour novamente clicando no botão de ajuda sempre que precisar de orientação.",
|
||||
}),
|
||||
];
|
||||
|
||||
export const criptoTourOptions = defaultGameTourOptions;
|
||||
293
app/src/atividades/programacao/cripto/game.js
Normal file
@@ -0,0 +1,293 @@
|
||||
/**
|
||||
* @fileoverview Utility module for game.js
|
||||
*
|
||||
* @module games.cripto.game
|
||||
*/
|
||||
|
||||
import Phaser from "phaser";
|
||||
import { BaseGameScene } from "../../../shared/BaseGameScene.js";
|
||||
import { setupCriptoAPI } from "./hooks/interpreterSetup.js";
|
||||
import { validationSolution } from "./validation/validators.js";
|
||||
import { gameConfig } from "./config/config.js";
|
||||
import { ConstantesJogo } from "./ui/constants.js";
|
||||
import { inicializarLayout } from "./ui/layout.js";
|
||||
import {
|
||||
animarEntradaCaractere,
|
||||
animarNovoCaractereSaida,
|
||||
} from "./ui/animations.js";
|
||||
import backgroundLoopSound from "./assets/background_loop.mp3";
|
||||
|
||||
const CRIPTO_AUDIO = {
|
||||
BACKGROUND_LOOP: "cripto_background_loop",
|
||||
};
|
||||
|
||||
class CriptoScene extends BaseGameScene {
|
||||
constructor() {
|
||||
super("CriptoScene");
|
||||
|
||||
// Variáveis globais do jogo
|
||||
this.entrada = "";
|
||||
this.saida = "";
|
||||
this.contador = 0;
|
||||
|
||||
// Textos visuais
|
||||
this.textoEntrada = null;
|
||||
this.textoSaida = null;
|
||||
|
||||
// Animação Matrix
|
||||
this.colunasMatrix = null;
|
||||
this.matrixEffect = null;
|
||||
|
||||
// Grid background animado
|
||||
this.gridBackground = null;
|
||||
|
||||
// Monitor CRT
|
||||
this.crtMonitor = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inicializa a cena `CriptoScene` com dados opcionais.
|
||||
* @param {Object} data - Dados passados pela inicialização da cena
|
||||
* @returns {void}
|
||||
*/
|
||||
init(data) {
|
||||
super.init(data);
|
||||
this.limparVariaveis();
|
||||
}
|
||||
|
||||
/**
|
||||
* Faz o preload dos recursos necessários para o jogo (áudio, imagens).
|
||||
* Usa `preloadGlobalAssets` para recursos compartilhados.
|
||||
* @returns {void}
|
||||
*/
|
||||
preload() {
|
||||
this.preloadGlobalAssets();
|
||||
this.load.audio(CRIPTO_AUDIO.BACKGROUND_LOOP, backgroundLoopSound);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cria elementos visuais da cena e configura o controlador padrão.
|
||||
* É responsável por inicializar o layout, textos e efeitos visuais.
|
||||
* @returns {void}
|
||||
*/
|
||||
create() {
|
||||
this.setupStandardController(
|
||||
() => setupCriptoAPI(this, { animationSpeed: 100 }),
|
||||
(historico) =>
|
||||
validationSolution(historico, this.configFase, gameConfig, this),
|
||||
);
|
||||
|
||||
// Inicializar layout visual
|
||||
const layout = inicializarLayout(this);
|
||||
this.textoEntrada = layout.textoEntrada;
|
||||
this.textoSaida = layout.textoSaida;
|
||||
this.colunasMatrix = layout.colunasMatrix;
|
||||
this.matrixEffect = layout.matrixEffect;
|
||||
this.gridBackground = layout.gridBackground;
|
||||
this.crtMonitor = layout.crtMonitor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loop de atualização da cena, chamado pelo Phaser a cada frame.
|
||||
* Atualiza efeitos visuais dependentes de `time`/`delta`.
|
||||
* @param {number} time - Tempo atual do jogo
|
||||
* @param {number} delta - Intervalo em ms desde o último frame
|
||||
* @returns {void}
|
||||
*/
|
||||
update(time, delta) {
|
||||
if (this.gridBackground) {
|
||||
this.gridBackground.update();
|
||||
}
|
||||
|
||||
if (this.matrixEffect) {
|
||||
this.matrixEffect.update(delta);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Preparações feitas imediatamente antes da execução do código do aluno.
|
||||
* Reinicia histórico e variáveis, e inicia áudio de fundo.
|
||||
* @returns {void}
|
||||
*/
|
||||
onBeforeRun() {
|
||||
this.historico = [];
|
||||
this.limparVariaveis();
|
||||
this.playAudio(CRIPTO_AUDIO.BACKGROUND_LOOP, { loop: true, volume: 0.5 });
|
||||
}
|
||||
|
||||
/**
|
||||
* Ação executada quando a cena é resetada manualmente pelo usuário.
|
||||
* Deve restaurar estado visual e interromper áudios em execução.
|
||||
* @returns {void}
|
||||
*/
|
||||
onReset() {
|
||||
this.limparVariaveis();
|
||||
this.stopAudio(CRIPTO_AUDIO.BACKGROUND_LOOP);
|
||||
}
|
||||
|
||||
/**
|
||||
* Limpa variáveis de estado internas do jogo e reseta textos visuais.
|
||||
* @returns {void}
|
||||
*/
|
||||
limparVariaveis() {
|
||||
this.entrada = "";
|
||||
this.saida = "";
|
||||
this.contador = 0;
|
||||
|
||||
if (this.textoEntrada) this.textoEntrada.setText("");
|
||||
if (this.textoSaida) this.textoSaida.setText("");
|
||||
}
|
||||
|
||||
/**
|
||||
* Define o valor de entrada do jogo e registra no histórico.
|
||||
* Retorna uma Promise que resolve quando animação (se houver) termina.
|
||||
* @param {string} valor - Novo valor de entrada
|
||||
* @returns {Promise<string>|Promise<void>}
|
||||
*/
|
||||
definirEntrada(valor) {
|
||||
this.entrada = String(valor || "");
|
||||
this.historico.push({ tipo: "definir_entrada", valor: this.entrada });
|
||||
|
||||
if (this.textoEntrada) {
|
||||
return animarEntradaCaractere(this, this.textoEntrada, this.entrada);
|
||||
}
|
||||
|
||||
return Promise.resolve(this.entrada);
|
||||
}
|
||||
|
||||
/**
|
||||
* Atualiza a string de saída e registra ação no histórico.
|
||||
* @param {string} valor - Novo conteúdo da saída
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
definirSaida(valor) {
|
||||
this.saida = String(valor || "");
|
||||
this.historico.push({ tipo: "definir_saida", valor: this.saida });
|
||||
|
||||
if (this.textoSaida) {
|
||||
this.textoSaida.setText(this.saida);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ajusta o contador interno do jogo.
|
||||
* @param {number} valor - Valor numérico para o contador
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
definirContador(valor) {
|
||||
this.contador = Number(valor) || 0;
|
||||
this.historico.push({ tipo: "definir_contador", valor: this.contador });
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retorna o valor atual de entrada e registra a leitura no histórico.
|
||||
* @returns {string}
|
||||
*/
|
||||
obterEntrada() {
|
||||
this.historico.push({ tipo: "obter_entrada", valor: this.entrada });
|
||||
return this.entrada;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retorna a string de saída atual e registra a leitura no histórico.
|
||||
* @returns {string}
|
||||
*/
|
||||
obterSaida() {
|
||||
this.historico.push({ tipo: "obter_saida", valor: this.saida });
|
||||
return this.saida;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retorna o valor numérico do contador e registra a leitura.
|
||||
* @returns {number}
|
||||
*/
|
||||
obterContador() {
|
||||
const valor = Number(this.contador);
|
||||
this.historico.push({ tipo: "obter_contador", valor: valor });
|
||||
return valor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Concatena texto à saída atual, animando a transição quando aplicável.
|
||||
* @param {string} valor - Valor a ser concatenado à saída
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
concatenarSaida(valor) {
|
||||
const valorString =
|
||||
valor !== null && valor !== undefined ? String(valor) : "";
|
||||
const saidaAnterior = this.saida;
|
||||
this.saida += valorString;
|
||||
this.historico.push({ tipo: "concatenar_saida", valor: this.saida });
|
||||
|
||||
if (this.textoSaida && valorString) {
|
||||
return animarNovoCaractereSaida(
|
||||
this,
|
||||
this.textoSaida,
|
||||
saidaAnterior,
|
||||
this.saida,
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Chamado quando a validação sinaliza sucesso. Limpa áudios/efeitos.
|
||||
* @returns {void}
|
||||
*/
|
||||
onSuccess() {
|
||||
this.stopAudio(CRIPTO_AUDIO.BACKGROUND_LOOP);
|
||||
}
|
||||
|
||||
/**
|
||||
* Chamado quando a validação sinaliza falha. Limpa áudios/efeitos.
|
||||
* @returns {void}
|
||||
*/
|
||||
onFailure() {
|
||||
this.stopAudio(CRIPTO_AUDIO.BACKGROUND_LOOP);
|
||||
}
|
||||
|
||||
/**
|
||||
* Destaca um bloco no workspace do Blockly e pausa a execução visual.
|
||||
* @param {string} id - Id do bloco a ser destacado
|
||||
* @returns {void}
|
||||
*/
|
||||
highlightBlock(id) {
|
||||
if (this.workspace) this.workspace.highlightBlock(id);
|
||||
this.highlightPause = true;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Cria a configuração Phaser para o jogo Cripto.
|
||||
* Retorna o objeto de configuração usado por `new Phaser.Game(config)`
|
||||
* @param {HTMLElement} elementoPai - Elemento DOM que conterá o canvas Phaser
|
||||
* @param {Object} configFaseAtual - Configuração da fase atual
|
||||
* @returns {Object} Configuração Phaser para inicializar a cena Cripto
|
||||
*/
|
||||
export const createGame = (elementoPai, configFaseAtual) => {
|
||||
const scene = new CriptoScene();
|
||||
|
||||
return {
|
||||
type: Phaser.AUTO,
|
||||
width: ConstantesJogo.LARGURA_TELA,
|
||||
height: ConstantesJogo.ALTURA_TELA,
|
||||
backgroundColor: ConstantesJogo.COR_FUNDO,
|
||||
parent: elementoPai,
|
||||
scale: { mode: Phaser.Scale.FIT, autoCenter: Phaser.Scale.CENTER_BOTH },
|
||||
scene: scene,
|
||||
callbacks: {
|
||||
preBoot: function (game) {
|
||||
game.registry.set("configFase", configFaseAtual);
|
||||
game.registry.set("gameConfig", gameConfig);
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* @fileoverview Utility module for interpreterSetup.js
|
||||
*
|
||||
* @module games.cripto.hooks.interpreterSetup
|
||||
*/
|
||||
|
||||
import { ApiHelpers } from "../../../../interpreters/ApiHelpers.js";
|
||||
|
||||
/**
|
||||
* Configura a API disponível ao interpretador para o jogo Cripto.
|
||||
* Registra funções que chamam métodos da `scene` com wrappers do `ApiHelpers`.
|
||||
* @param {Object} scene - Instância da cena Phaser (ex.: `CriptoScene`)
|
||||
* @param {Object} [config] - Opções (ex.: `animationSpeed`)
|
||||
* @returns {Function} Função que realiza o registro no `interpreter` e `globalScope`
|
||||
*/
|
||||
export const setupCriptoAPI = (scene, config = {}) => {
|
||||
const animationDelay = config.animationSpeed || 500;
|
||||
|
||||
return (interpreter, globalScope) => {
|
||||
ApiHelpers.registerFunction(
|
||||
interpreter,
|
||||
globalScope,
|
||||
"definirEntrada",
|
||||
ApiHelpers.createActionWrapper(scene, "definirEntrada", animationDelay),
|
||||
true,
|
||||
);
|
||||
|
||||
ApiHelpers.registerFunction(
|
||||
interpreter,
|
||||
globalScope,
|
||||
"definirSaida",
|
||||
ApiHelpers.createActionWrapper(scene, "definirSaida", animationDelay),
|
||||
true,
|
||||
);
|
||||
|
||||
ApiHelpers.registerFunction(
|
||||
interpreter,
|
||||
globalScope,
|
||||
"definirContador",
|
||||
ApiHelpers.createActionWrapper(scene, "definirContador", animationDelay),
|
||||
true,
|
||||
);
|
||||
|
||||
ApiHelpers.registerFunction(
|
||||
interpreter,
|
||||
globalScope,
|
||||
"concatenarSaida",
|
||||
ApiHelpers.createActionWrapper(scene, "concatenarSaida", animationDelay),
|
||||
true,
|
||||
);
|
||||
|
||||
ApiHelpers.registerFunction(
|
||||
interpreter,
|
||||
globalScope,
|
||||
"obterContador",
|
||||
ApiHelpers.createConditionWrapper(scene, "obterContador"),
|
||||
false,
|
||||
);
|
||||
|
||||
ApiHelpers.registerFunction(
|
||||
interpreter,
|
||||
globalScope,
|
||||
"obterEntrada",
|
||||
ApiHelpers.createConditionWrapper(scene, "obterEntrada"),
|
||||
false,
|
||||
);
|
||||
|
||||
ApiHelpers.registerFunction(
|
||||
interpreter,
|
||||
globalScope,
|
||||
"obterSaida",
|
||||
ApiHelpers.createConditionWrapper(scene, "obterSaida"),
|
||||
false,
|
||||
);
|
||||
|
||||
ApiHelpers.registerFunction(
|
||||
interpreter,
|
||||
globalScope,
|
||||
"highlightBlock",
|
||||
ApiHelpers.createHighlightWrapper(scene),
|
||||
false,
|
||||
);
|
||||
};
|
||||
};
|
||||
17
app/src/atividades/programacao/cripto/hooks/useCriptoTour.js
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* @fileoverview Utility module for useCriptoTour.js
|
||||
*
|
||||
* @module games.cripto.hooks.useCriptoTour
|
||||
*/
|
||||
|
||||
import { useGameTour } from "../../../../hooks/useGameTour";
|
||||
import { criptoTourSteps, criptoTourOptions } from "../config/tourSteps";
|
||||
|
||||
export const useCriptoTour = () => {
|
||||
/**
|
||||
* Hook que retorna o controlador de tour para o jogo Cripto.
|
||||
* Encapsula `useGameTour` com os passos e opções específicos.
|
||||
* @returns {Object} API do tour (start, stop, etc.)
|
||||
*/
|
||||
return useGameTour("cripto", criptoTourSteps, criptoTourOptions);
|
||||
};
|
||||
103
app/src/atividades/programacao/cripto/ui/CRTMonitor.js
Normal file
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* @fileoverview Utility module for CRTMonitor.js
|
||||
*
|
||||
* @module games.cripto.ui.CRTMonitor
|
||||
*/
|
||||
|
||||
export default class CRTMonitor {
|
||||
constructor(scene, width, height) {
|
||||
this.scene = scene;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
|
||||
this.container = scene.add.container(width / 2, height / 2);
|
||||
this.contentLayer = scene.add.container(-width / 2, -height / 2);
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// 1. Fundo do Monitor (Verde Escuro)
|
||||
const bg = this.scene.add.rectangle(
|
||||
0,
|
||||
0,
|
||||
this.width,
|
||||
this.height,
|
||||
0x001100,
|
||||
);
|
||||
|
||||
// 2. Scanlines (Linhas horizontais)
|
||||
this.createScanlinesTexture();
|
||||
const scanlines = this.scene.add.image(0, 0, "scanlines-texture");
|
||||
scanlines.setAlpha(0.3); // Opacidade das linhas
|
||||
|
||||
// 3. Vignette (Sombra nos cantos)
|
||||
this.createVignetteTexture();
|
||||
const vignette = this.scene.add.image(0, 0, "vignette-texture");
|
||||
vignette.setAlpha(0.8);
|
||||
|
||||
// --- ORDEM DE MONTAGEM ---
|
||||
this.container.add(bg);
|
||||
this.container.add(this.contentLayer);
|
||||
this.container.add(scanlines);
|
||||
this.container.add(vignette);
|
||||
}
|
||||
|
||||
addContent(gameObject) {
|
||||
this.contentLayer.add(gameObject);
|
||||
}
|
||||
|
||||
clearContent() {
|
||||
this.contentLayer.removeAll(true);
|
||||
}
|
||||
|
||||
createScanlinesTexture() {
|
||||
if (this.scene.textures.exists("scanlines-texture")) return;
|
||||
|
||||
const graphics = this.scene.make.graphics();
|
||||
graphics.fillStyle(0x000000);
|
||||
|
||||
for (let y = 0; y < this.height; y += 3) {
|
||||
graphics.fillRect(0, y, this.width, 1);
|
||||
}
|
||||
|
||||
graphics.generateTexture("scanlines-texture", this.width, this.height);
|
||||
graphics.destroy();
|
||||
}
|
||||
|
||||
createVignetteTexture() {
|
||||
if (this.scene.textures.exists("vignette-texture")) return;
|
||||
|
||||
const canvas = this.scene.textures.createCanvas(
|
||||
"vignette-texture",
|
||||
this.width,
|
||||
this.height,
|
||||
);
|
||||
const ctx = canvas.context;
|
||||
|
||||
const gradient = ctx.createRadialGradient(
|
||||
this.width / 2,
|
||||
this.height / 2,
|
||||
this.width * 0.3, // Centro
|
||||
this.width / 2,
|
||||
this.height / 2,
|
||||
this.width * 0.7, // Bordas
|
||||
);
|
||||
|
||||
gradient.addColorStop(0, "rgba(0, 0, 0, 0)");
|
||||
gradient.addColorStop(1, "rgba(0, 0, 0, 0.8)"); // Borda escura
|
||||
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fillRect(0, 0, this.width, this.height);
|
||||
|
||||
canvas.refresh();
|
||||
}
|
||||
|
||||
getContainer() {
|
||||
return this.container;
|
||||
}
|
||||
|
||||
getContentLayer() {
|
||||
return this.contentLayer;
|
||||
}
|
||||
}
|
||||
120
app/src/atividades/programacao/cripto/ui/GridBackground.js
Normal file
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* @fileoverview Utility module for GridBackground.js
|
||||
*
|
||||
* @module games.cripto.ui.GridBackground
|
||||
*/
|
||||
|
||||
export default class GridBackground {
|
||||
constructor(scene, container = null) {
|
||||
this.scene = scene;
|
||||
this.container = container;
|
||||
this.graphics = scene.add.graphics();
|
||||
|
||||
if (container) container.add(this.graphics);
|
||||
|
||||
this.width = scene.scale.width;
|
||||
this.height = scene.scale.height;
|
||||
this.horizonY = this.height / 2;
|
||||
this.centerX = this.width / 2;
|
||||
|
||||
this.gridColor = 0x33ff33;
|
||||
this.gapSize = 50;
|
||||
this.numVerticalLines = 20;
|
||||
this.spreadBase = this.width * 4;
|
||||
this.numHorizontalLines = 10;
|
||||
this.lineAlpha = 0.1;
|
||||
|
||||
// Controle de animação
|
||||
this.offset = 0;
|
||||
this.speed = 1;
|
||||
}
|
||||
|
||||
update() {
|
||||
this.offset += this.speed;
|
||||
|
||||
// Resetar offset quando completar um ciclo
|
||||
if (this.offset >= 100) {
|
||||
this.offset = 0;
|
||||
}
|
||||
|
||||
this.draw();
|
||||
}
|
||||
|
||||
draw() {
|
||||
this.graphics.clear();
|
||||
|
||||
const horizonY = this.horizonY;
|
||||
const centerX = this.centerX;
|
||||
const width = this.width;
|
||||
const height = this.height;
|
||||
const gapSize = this.gapSize;
|
||||
|
||||
this.graphics.lineStyle(2, this.gridColor, this.lineAlpha);
|
||||
|
||||
// 1. Linhas Verticais (estáticas)
|
||||
for (
|
||||
let i = -this.numVerticalLines / 2;
|
||||
i <= this.numVerticalLines / 2;
|
||||
i++
|
||||
) {
|
||||
const xBase = centerX + (i / this.numVerticalLines) * this.spreadBase;
|
||||
const totalDistanceY = height / 2;
|
||||
const skipFactor = gapSize / totalDistanceY;
|
||||
const xStart = centerX + (xBase - centerX) * skipFactor;
|
||||
|
||||
// Chão
|
||||
this.graphics.beginPath();
|
||||
this.graphics.moveTo(xStart, horizonY + gapSize);
|
||||
this.graphics.lineTo(xBase, height);
|
||||
this.graphics.strokePath();
|
||||
|
||||
// Teto
|
||||
this.graphics.beginPath();
|
||||
this.graphics.moveTo(xStart, horizonY - gapSize);
|
||||
this.graphics.lineTo(xBase, 0);
|
||||
this.graphics.strokePath();
|
||||
}
|
||||
|
||||
// 2. Linhas Horizontais (animadas)
|
||||
const numLines = this.numHorizontalLines + 1;
|
||||
for (let i = 0; i <= numLines; i++) {
|
||||
// Adicionar offset para criar movimento
|
||||
const adjustedI = i - this.offset / 100;
|
||||
if (adjustedI < 0) continue;
|
||||
|
||||
const t = adjustedI / this.numHorizontalLines;
|
||||
if (t > 1) continue;
|
||||
|
||||
const perspectiveFactor = Math.pow(t, 2);
|
||||
const drawingSpace = height / 2 - gapSize;
|
||||
const relativeY = perspectiveFactor * drawingSpace;
|
||||
|
||||
// Chão
|
||||
const yPosFloor = horizonY + gapSize + relativeY;
|
||||
this.graphics.beginPath();
|
||||
this.graphics.moveTo(0, yPosFloor);
|
||||
this.graphics.lineTo(width, yPosFloor);
|
||||
this.graphics.strokePath();
|
||||
|
||||
// Teto
|
||||
const yPosCeiling = horizonY - gapSize - relativeY;
|
||||
this.graphics.beginPath();
|
||||
this.graphics.moveTo(0, yPosCeiling);
|
||||
this.graphics.lineTo(width, yPosCeiling);
|
||||
this.graphics.strokePath();
|
||||
}
|
||||
|
||||
// 3. Gap (Horizonte)
|
||||
this.graphics.lineStyle(2, this.gridColor, 0.3);
|
||||
|
||||
this.graphics.beginPath();
|
||||
this.graphics.moveTo(0, horizonY - gapSize);
|
||||
this.graphics.lineTo(width, horizonY - gapSize);
|
||||
this.graphics.strokePath();
|
||||
|
||||
this.graphics.beginPath();
|
||||
this.graphics.moveTo(0, horizonY + gapSize);
|
||||
this.graphics.lineTo(width, horizonY + gapSize);
|
||||
this.graphics.strokePath();
|
||||
}
|
||||
}
|
||||
54
app/src/atividades/programacao/cripto/ui/MatrixEffect.js
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* @fileoverview Utility module for MatrixEffect.js
|
||||
*
|
||||
* @module games.cripto.ui.MatrixEffect
|
||||
*/
|
||||
|
||||
import { ConstantesAnimacao } from "./constants.js";
|
||||
|
||||
export default class MatrixEffect {
|
||||
constructor(scene, colunas) {
|
||||
this.scene = scene;
|
||||
this.colunas = colunas;
|
||||
|
||||
this.caracteres = ConstantesAnimacao.MATRIX.CARACTERES_HEX;
|
||||
this.numCaracteres = ConstantesAnimacao.MATRIX.CARACTERES_POR_COLUNA;
|
||||
this.intervalo = ConstantesAnimacao.MATRIX.INTERVALO_ATUALIZACAO;
|
||||
|
||||
// Controle de animação via update()
|
||||
this.acumulador = 0;
|
||||
|
||||
// Gerar conteúdo inicial
|
||||
this.atualizarColunas();
|
||||
}
|
||||
|
||||
gerarCaractereAleatorio() {
|
||||
return this.caracteres[Math.floor(Math.random() * this.caracteres.length)];
|
||||
}
|
||||
|
||||
atualizarColuna(coluna) {
|
||||
const textoColuna = [];
|
||||
for (let i = 0; i < this.numCaracteres; i++) {
|
||||
textoColuna.push(this.gerarCaractereAleatorio());
|
||||
}
|
||||
coluna.setText(textoColuna.join("\n"));
|
||||
}
|
||||
|
||||
atualizarColunas() {
|
||||
this.colunas.forEach((coluna) => this.atualizarColuna(coluna));
|
||||
}
|
||||
|
||||
/**
|
||||
* Método chamado a cada frame do Phaser
|
||||
* @param {number} delta - Tempo decorrido desde o último frame (ms)
|
||||
*/
|
||||
update(delta = 16) {
|
||||
this.acumulador += delta;
|
||||
|
||||
// Atualizar apenas quando o intervalo for atingido
|
||||
if (this.acumulador >= this.intervalo) {
|
||||
this.atualizarColunas();
|
||||
this.acumulador = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
215
app/src/atividades/programacao/cripto/ui/animations.js
Normal file
@@ -0,0 +1,215 @@
|
||||
/**
|
||||
* @fileoverview Utility module for animations.js
|
||||
*
|
||||
* @module games.cripto.ui.animations
|
||||
*/
|
||||
|
||||
import { ConstantesAnimacao } from "./constants.js";
|
||||
|
||||
export function iniciarAnimacaoMatrix(scene, colunas) {
|
||||
const caracteres = ConstantesAnimacao.MATRIX.CARACTERES_HEX;
|
||||
const numCaracteres = ConstantesAnimacao.MATRIX.CARACTERES_POR_COLUNA;
|
||||
const intervalo = ConstantesAnimacao.MATRIX.INTERVALO_ATUALIZACAO;
|
||||
|
||||
const gerarCaractereAleatorio = () => {
|
||||
return caracteres[Math.floor(Math.random() * caracteres.length)];
|
||||
};
|
||||
|
||||
const atualizarColuna = (coluna) => {
|
||||
if (!scene.isRunning) return;
|
||||
|
||||
const textoColuna = [];
|
||||
for (let i = 0; i < numCaracteres; i++) {
|
||||
textoColuna.push(gerarCaractereAleatorio());
|
||||
}
|
||||
coluna.setText(textoColuna.join("\n"));
|
||||
};
|
||||
|
||||
colunas.forEach((coluna) => atualizarColuna(coluna));
|
||||
|
||||
const timer = scene.time.addEvent({
|
||||
delay: intervalo,
|
||||
callback: () => {
|
||||
if (scene.isRunning) {
|
||||
colunas.forEach((coluna) => atualizarColuna(coluna));
|
||||
}
|
||||
},
|
||||
loop: true,
|
||||
});
|
||||
|
||||
return timer;
|
||||
}
|
||||
|
||||
export function pararAnimacaoMatrix(colunas, timer) {
|
||||
if (timer) {
|
||||
timer.remove();
|
||||
}
|
||||
colunas.forEach((coluna) => coluna.setText(""));
|
||||
}
|
||||
|
||||
/**
|
||||
* Anima a entrada de texto caractere por caractere com cursor piscando
|
||||
* @param {Phaser.Scene} scene - A cena do Phaser
|
||||
* @param {Phaser.GameObjects.Text} textoEntrada - O objeto de texto da entrada
|
||||
* @param {string} textoFinal - O texto final a ser exibido
|
||||
* @returns {Promise} Promise que resolve quando a animação termina
|
||||
*/
|
||||
export function animarEntradaCaractere(scene, textoEntrada, textoFinal) {
|
||||
return new Promise((resolve) => {
|
||||
const cursor = ConstantesAnimacao.CURSOR;
|
||||
const velocidade = ConstantesAnimacao.ENTRADA.VELOCIDADE_DIGITACAO;
|
||||
const intervaloPiscar = ConstantesAnimacao.ENTRADA.INTERVALO_PISCAR_CURSOR;
|
||||
|
||||
let posicaoAtual = 0;
|
||||
let cursorVisivel = true;
|
||||
let timerPiscar = null;
|
||||
|
||||
const piscarCursor = () => {
|
||||
if (!scene.isRunning) return;
|
||||
|
||||
cursorVisivel = !cursorVisivel;
|
||||
const textoAtual = textoFinal.substring(0, posicaoAtual);
|
||||
textoEntrada.setText(textoAtual + (cursorVisivel ? cursor : ""));
|
||||
|
||||
timerPiscar = scene.time.delayedCall(intervaloPiscar, piscarCursor);
|
||||
};
|
||||
|
||||
const digitarProximoCaractere = () => {
|
||||
if (!scene.isRunning) {
|
||||
if (timerPiscar) timerPiscar.remove();
|
||||
textoEntrada.setText(textoFinal);
|
||||
resolve(textoFinal);
|
||||
return;
|
||||
}
|
||||
|
||||
if (posicaoAtual >= textoFinal.length) {
|
||||
if (timerPiscar) timerPiscar.remove();
|
||||
textoEntrada.setText(textoFinal);
|
||||
resolve(textoFinal);
|
||||
return;
|
||||
}
|
||||
|
||||
posicaoAtual++;
|
||||
const textoAtual = textoFinal.substring(0, posicaoAtual);
|
||||
textoEntrada.setText(textoAtual + cursor);
|
||||
cursorVisivel = true;
|
||||
|
||||
scene.time.delayedCall(velocidade, digitarProximoCaractere);
|
||||
};
|
||||
|
||||
textoEntrada.setText(cursor);
|
||||
cursorVisivel = true;
|
||||
|
||||
timerPiscar = scene.time.delayedCall(intervaloPiscar, piscarCursor);
|
||||
|
||||
scene.time.delayedCall(velocidade, digitarProximoCaractere);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Anima a saída de texto completa com embaralhamento
|
||||
* @param {Phaser.Scene} scene - A cena do Phaser
|
||||
* @param {Phaser.GameObjects.Text} textoSaida - O objeto de texto da saída
|
||||
* @param {string} textoFinal - O texto final a ser exibido
|
||||
* @returns {Promise} Promise que resolve quando a animação termina
|
||||
*/
|
||||
export function animarSaidaCaractere(scene, textoSaida, textoFinal) {
|
||||
return new Promise((resolve) => {
|
||||
const caracteres = ConstantesAnimacao.CARACTERES_EMBARALHAMENTO;
|
||||
const ultimaPosicao = textoFinal.length;
|
||||
let posicaoAtual = 0;
|
||||
const duracaoEmbaralhamento =
|
||||
ConstantesAnimacao.SAIDA.DURACAO_EMBARALHAMENTO;
|
||||
const repeticoesEmbaralhamento =
|
||||
ConstantesAnimacao.SAIDA.REPETICOES_EMBARALHAMENTO;
|
||||
|
||||
const proximoCaractere = () => {
|
||||
if (!scene.isRunning) {
|
||||
textoSaida.setText(textoFinal);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
if (posicaoAtual >= ultimaPosicao) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
let repeticao = 0;
|
||||
const embaralhar = () => {
|
||||
if (!scene.isRunning) {
|
||||
textoSaida.setText(textoFinal);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
if (repeticao < repeticoesEmbaralhamento) {
|
||||
const charAleatorio =
|
||||
caracteres[Math.floor(Math.random() * caracteres.length)];
|
||||
const textoAtual =
|
||||
textoFinal.substring(0, posicaoAtual) + charAleatorio;
|
||||
textoSaida.setText(textoAtual);
|
||||
|
||||
repeticao++;
|
||||
scene.time.delayedCall(duracaoEmbaralhamento, embaralhar);
|
||||
} else {
|
||||
posicaoAtual++;
|
||||
textoSaida.setText(textoFinal.substring(0, posicaoAtual));
|
||||
scene.time.delayedCall(
|
||||
ConstantesAnimacao.SAIDA.PAUSA_ENTRE_CARACTERES,
|
||||
proximoCaractere,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
embaralhar();
|
||||
};
|
||||
|
||||
proximoCaractere();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Anima apenas um novo caractere adicionado à saída (para concatenação)
|
||||
* @param {Phaser.Scene} scene - A cena do Phaser
|
||||
* @param {Phaser.GameObjects.Text} textoSaida - O objeto de texto da saída
|
||||
* @param {string} textoAnterior - O texto antes da concatenação
|
||||
* @param {string} textoFinal - O texto após a concatenação
|
||||
* @returns {Promise} Promise que resolve quando a animação termina
|
||||
*/
|
||||
export function animarNovoCaractereSaida(
|
||||
scene,
|
||||
textoSaida,
|
||||
textoAnterior,
|
||||
textoFinal,
|
||||
) {
|
||||
return new Promise((resolve) => {
|
||||
const caracteres = ConstantesAnimacao.CARACTERES_EMBARALHAMENTO;
|
||||
const duracaoEmbaralhamento =
|
||||
ConstantesAnimacao.CONCATENACAO.DURACAO_EMBARALHAMENTO;
|
||||
const repeticoesEmbaralhamento =
|
||||
ConstantesAnimacao.CONCATENACAO.REPETICOES_EMBARALHAMENTO;
|
||||
let repeticao = 0;
|
||||
|
||||
const embaralhar = () => {
|
||||
if (!scene.isRunning) {
|
||||
textoSaida.setText(textoFinal);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
if (repeticao < repeticoesEmbaralhamento) {
|
||||
const charAleatorio =
|
||||
caracteres[Math.floor(Math.random() * caracteres.length)];
|
||||
textoSaida.setText(textoAnterior + charAleatorio);
|
||||
repeticao++;
|
||||
scene.time.delayedCall(duracaoEmbaralhamento, embaralhar);
|
||||
} else {
|
||||
textoSaida.setText(textoFinal);
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
embaralhar();
|
||||
});
|
||||
}
|
||||
67
app/src/atividades/programacao/cripto/ui/constants.js
Normal file
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* @fileoverview Utility module for constants.js
|
||||
*
|
||||
* @module games.cripto.ui.constants
|
||||
*/
|
||||
|
||||
export const ConstantesJogo = {
|
||||
LARGURA_TELA: 800,
|
||||
ALTURA_TELA: 600,
|
||||
COR_FUNDO: "#242527",
|
||||
};
|
||||
|
||||
export const ConstantesLayout = {
|
||||
MARGEM_PERCENTUAL: 0.03,
|
||||
LARGURA_QUADRO_ESQ_PERCENTUAL: 0.2,
|
||||
COR_BORDA: 0x00ff00,
|
||||
ESPESSURA_BORDA: 5,
|
||||
RAIO_ARREDONDAMENTO: 12,
|
||||
PADDING_TEXTO: 20,
|
||||
};
|
||||
|
||||
export const ConstantesTexto = {
|
||||
ENTRADA: {
|
||||
TAMANHO_FONTE: "64px",
|
||||
COR: "#00ff00",
|
||||
ALINHAMENTO: "left",
|
||||
PESO_FONTE: "bold",
|
||||
},
|
||||
SAIDA: {
|
||||
TAMANHO_FONTE: "64px",
|
||||
COR: "#ffff00",
|
||||
ALINHAMENTO: "left",
|
||||
PESO_FONTE: "bold",
|
||||
},
|
||||
};
|
||||
|
||||
export const ConstantesAnimacao = {
|
||||
CARACTERES_EMBARALHAMENTO: String.fromCharCode(
|
||||
...Array.from({ length: 51 }, (_, i) => 128 + i),
|
||||
),
|
||||
CURSOR: "▓",
|
||||
|
||||
ENTRADA: {
|
||||
VELOCIDADE_DIGITACAO: 50,
|
||||
INTERVALO_PISCAR_CURSOR: 400,
|
||||
},
|
||||
|
||||
SAIDA: {
|
||||
DURACAO_EMBARALHAMENTO: 20,
|
||||
REPETICOES_EMBARALHAMENTO: 2,
|
||||
PAUSA_ENTRE_CARACTERES: 15,
|
||||
},
|
||||
|
||||
CONCATENACAO: {
|
||||
DURACAO_EMBARALHAMENTO: 20,
|
||||
REPETICOES_EMBARALHAMENTO: 2,
|
||||
},
|
||||
|
||||
MATRIX: {
|
||||
CARACTERES_HEX: "0123456789ABCDEF",
|
||||
NUMERO_COLUNAS: 7,
|
||||
TAMANHO_FONTE: "20px",
|
||||
COR_TEXTO: "#00ff00",
|
||||
INTERVALO_ATUALIZACAO: 250,
|
||||
CARACTERES_POR_COLUNA: 21,
|
||||
},
|
||||
};
|
||||