Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,18 @@ Quando ativo, após ações de cadastro/edição, exclusão, importação e expe
- totais expedidos por SKU.

> Importante: por segurança do navegador, não é possível editar automaticamente o mesmo arquivo Excel já aberto no seu computador. O que o sistema faz é gerar uma nova versão atualizada da planilha.

## Demo de jogo mobile de resgate

Adicionado um protótipo jogável em:

- `resgate-game.html`

Tema da demo: salvar personagens em várias fases (princesa, rei, camponesa + criança etc.), com controle por botões touch e setas do teclado.

Para rodar:

```bash
python3 -m http.server 4173
# abrir http://localhost:4173/resgate-game.html
```
101 changes: 101 additions & 0 deletions resgate-game.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
:root {
--bg: #101728;
--card: #172239;
--text: #f4f7ff;
--accent: #ffca3a;
--danger: #ff595e;
--ok: #34d399;
}

* { box-sizing: border-box; }

body {
margin: 0;
font-family: Inter, system-ui, sans-serif;
background: radial-gradient(circle at top, #1f2f52, var(--bg));
color: var(--text);
}

.game-shell {
min-height: 100vh;
max-width: 540px;
margin: 0 auto;
padding: 1rem;
display: grid;
gap: 0.9rem;
}

.hud {
background: var(--card);
border-radius: 14px;
padding: 0.8rem;
display: grid;
grid-template-columns: repeat(3, 1fr);
text-align: center;
}

.label {
margin: 0;
font-size: 0.75rem;
opacity: 0.8;
}

h1, h2 { margin: 0.2rem 0 0; font-size: 1.2rem; }

.arena-wrap {
background: rgba(255, 255, 255, 0.06);
border-radius: 14px;
padding: 0.7rem;
}

.arena {
position: relative;
width: 100%;
aspect-ratio: 1/1;
background: #0b1220;
border-radius: 10px;
border: 2px solid rgba(255,255,255,0.15);
overflow: hidden;
}

.hero, .npc, .enemy {
position: absolute;
width: 24px;
height: 24px;
border-radius: 6px;
}

.hero { background: var(--accent); box-shadow: 0 0 14px rgba(255,202,58,.6); }
.npc { background: var(--ok); }
.enemy { background: var(--danger); }

.controls {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.5rem;
}

button {
border: 0;
border-radius: 10px;
padding: 0.75rem;
font-size: 1rem;
font-weight: 700;
background: #2a3f67;
color: white;
}

.actions {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.5rem;
}

#nextPhaseBtn { background: #3c7d3f; }
.secondary { background: #5f6678; }

.message {
min-height: 1.8rem;
text-align: center;
margin: 0;
}
49 changes: 49 additions & 0 deletions resgate-game.html
Original file line number Diff line number Diff line change
@@ -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, maximum-scale=1.0, user-scalable=no" />
<title>Resgate Real - Demo Mobile</title>
<link rel="stylesheet" href="resgate-game.css" />
</head>
<body>
<main class="game-shell">
<header class="hud">
<div>
<p class="label">Fase</p>
<h1 id="phaseTitle">1</h1>
</div>
<div>
<p class="label">Vidas</p>
<h2 id="lives">3</h2>
</div>
<div>
<p class="label">Objetivo</p>
<h2 id="goal">Princesa</h2>
</div>
</header>

<section class="arena-wrap">
<div id="arena" class="arena">
<div id="hero" class="hero" aria-label="Herói"></div>
</div>
</section>

<section class="controls">
<button data-dir="left">◀</button>
<button data-dir="up">▲</button>
<button data-dir="down">▼</button>
<button data-dir="right">▶</button>
</section>

<section class="actions">
<button id="nextPhaseBtn">Próxima fase</button>
<button id="restartBtn" class="secondary">Reiniciar</button>
</section>

<p id="message" class="message">Chegue no refém evitando os inimigos.</p>
</main>

<script src="resgate-game.js"></script>
</body>
</html>
166 changes: 166 additions & 0 deletions resgate-game.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
const arena = document.getElementById('arena');
const heroEl = document.getElementById('hero');
const phaseTitle = document.getElementById('phaseTitle');
const livesEl = document.getElementById('lives');
const goalEl = document.getElementById('goal');
const messageEl = document.getElementById('message');

const PHASES = [
{ target: 'Princesa', enemies: 2 },
{ target: 'Rei', enemies: 3 },
{ target: 'Camponesa + Criança', enemies: 4 },
{ target: 'Rainha', enemies: 5 },
{ target: 'Vilarejo', enemies: 6 }
];

const state = {
phase: 0,
lives: 3,
hero: { x: 16, y: 16 },
npc: null,
enemies: [],
enemyEls: []
};

function clamp(value, min, max) {
return Math.min(max, Math.max(min, value));
}

function place(el, x, y) {
el.style.left = `${x}px`;
el.style.top = `${y}px`;
}

function makeEnemy() {
const el = document.createElement('div');
el.className = 'enemy';
arena.appendChild(el);
return {
el,
x: Math.random() * 280 + 20,
y: Math.random() * 280 + 20,
vx: Math.random() > 0.5 ? 2 : -2,
vy: Math.random() > 0.5 ? 2 : -2
};
}

function resetPhase() {
state.enemyEls.forEach((el) => el.remove());
state.enemyEls = [];
state.enemies = [];

const phase = PHASES[state.phase];
phaseTitle.textContent = String(state.phase + 1);
goalEl.textContent = phase.target;
livesEl.textContent = String(state.lives);
state.hero = { x: 16, y: 16 };

document.querySelector('.npc')?.remove();
const npcEl = document.createElement('div');
npcEl.className = 'npc';
arena.appendChild(npcEl);
state.npc = {
el: npcEl,
x: Math.random() * 260 + 40,
y: Math.random() * 260 + 40
};

for (let i = 0; i < phase.enemies; i += 1) {
const enemy = makeEnemy();
state.enemies.push(enemy);
state.enemyEls.push(enemy.el);
}

messageEl.textContent = `Resgate: ${phase.target}`;
render();
}

function distance(a, b) {
return Math.hypot(a.x - b.x, a.y - b.y);
}

function moveHero(dx, dy) {
const max = arena.clientWidth - 24;
state.hero.x = clamp(state.hero.x + dx, 0, max);
state.hero.y = clamp(state.hero.y + dy, 0, max);
render();
checkCollisions();
}

function checkCollisions() {
if (distance(state.hero, state.npc) < 20) {
messageEl.textContent = `Você salvou ${PHASES[state.phase].target}!`;
return;
}

const hit = state.enemies.some((enemy) => distance(state.hero, enemy) < 18);
if (hit) {
state.lives -= 1;
livesEl.textContent = String(state.lives);
if (state.lives <= 0) {
messageEl.textContent = 'Game over! Reinicie para tentar novamente.';
state.lives = 0;
return;
}
state.hero = { x: 16, y: 16 };
messageEl.textContent = 'Você foi atingido! Volte ao início da fase.';
render();
}
}

function animateEnemies() {
const max = arena.clientWidth - 24;
state.enemies.forEach((enemy) => {
enemy.x += enemy.vx;
enemy.y += enemy.vy;
if (enemy.x <= 0 || enemy.x >= max) enemy.vx *= -1;
if (enemy.y <= 0 || enemy.y >= max) enemy.vy *= -1;
});
render();
checkCollisions();
requestAnimationFrame(animateEnemies);
}

function render() {
place(heroEl, state.hero.x, state.hero.y);
place(state.npc.el, state.npc.x, state.npc.y);
state.enemies.forEach((enemy) => place(enemy.el, enemy.x, enemy.y));
}

function nextPhase() {
if (state.phase === PHASES.length - 1) {
messageEl.textContent = 'Parabéns! Você concluiu todas as fases desta demo.';
return;
}
state.phase += 1;
resetPhase();
}

document.querySelectorAll('[data-dir]').forEach((btn) => {
btn.addEventListener('click', () => {
if (state.lives <= 0) return;
const dir = btn.dataset.dir;
if (dir === 'left') moveHero(-18, 0);
if (dir === 'right') moveHero(18, 0);
if (dir === 'up') moveHero(0, -18);
if (dir === 'down') moveHero(0, 18);
});
});

document.getElementById('nextPhaseBtn').addEventListener('click', nextPhase);
document.getElementById('restartBtn').addEventListener('click', () => {
state.phase = 0;
state.lives = 3;
resetPhase();
});

window.addEventListener('keydown', (event) => {
if (state.lives <= 0) return;
if (event.key === 'ArrowLeft') moveHero(-16, 0);
if (event.key === 'ArrowRight') moveHero(16, 0);
if (event.key === 'ArrowUp') moveHero(0, -16);
if (event.key === 'ArrowDown') moveHero(0, 16);
});

resetPhase();
requestAnimationFrame(animateEnemies);