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>
This commit is contained in:
2025-10-29 21:30:14 -03:00
committed by Graciano
parent e24ee22b5a
commit 3da7d323e8
577 changed files with 121994 additions and 158 deletions

View 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;
}

View File

@@ -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 });
}
}
});

View File

@@ -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>

View 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);

View 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>

View File

@@ -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');
});

View File

@@ -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>

View File

@@ -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();

View File

@@ -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>

View 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();

View File

@@ -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>

View 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 });
}

View File

@@ -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>

View 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 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 });
}

View File

@@ -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>

View File

@@ -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();

View File

@@ -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>

View File

@@ -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();

View File

@@ -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>

View File

@@ -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 });
}

View File

@@ -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>

View 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' },
],
},
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -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();
});

View File

@@ -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>

View 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);

View 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>

View 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');
}
});

View 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>

View 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);
}
});

View 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>

View File

@@ -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 });
});

View File

@@ -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>

View File

@@ -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);
}
});

View 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>

View File

@@ -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();
});

View File

@@ -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>

View File

@@ -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();

View File

@@ -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>

View File

@@ -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);
}
}
});

View 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 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>

View File

@@ -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();
}

View File

@@ -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>

View File

@@ -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 (AZ)',
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 (09)',
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);
});

View File

@@ -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>

View File

@@ -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');
}
});

View File

@@ -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>

View 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' },
],
},
};

View 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 = {};

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View 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");
};

View 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.",
},
};

View File

@@ -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{(" } } } } } } }] } },
};

View File

@@ -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"}}}}}}}}]}}
};

View 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;

View 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);
},
},
};
};

View File

@@ -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);
};
};

View File

@@ -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);
};

View 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
}
};

View 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 };
}

View File

@@ -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);
}

View 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 = {};

View File

@@ -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);

Binary file not shown.

After

Width:  |  Height:  |  Size: 859 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View 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",
},
],
},
],
};

View 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
},
};

View 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[.",
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
],
},
},
};

View 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;

View 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`
*/

View File

@@ -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"],
};

View File

@@ -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);
};

View File

@@ -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);
};

Binary file not shown.

View 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 = {};

View 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];
};
};

View 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,
];

View 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.",
},
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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;

View 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);
},
},
};
};

View File

@@ -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,
);
};
};

View 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);
};

View 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;
}
}

View 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();
}
}

View 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;
}
}
}

View 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();
});
}

View 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,
},
};

Some files were not shown because too many files have changed in this diff Show More