Um jogo completo em HTML5 Canvas + JavaScript puro, sem dependências externas, pronto para ser publicado em qualquer hospedagem.
O jogo é dividido em 3 arquivos simples: index.html, style.css e game.js. Também está incluído um pequeno backend PHP opcional para o ranking global (funciona com arquivo JSON, sem banco de dados).
Estrutura de Arquivos
/space/ ├── index.html ├── style.css ├── game.js ├── ranking.php ← opcional (ranking global) └── ranking.json ← criado automaticamente
1- Arquivo index.html
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Space Defender - Jogo Espacial</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="game-wrapper">
<!-- Cabeçalho com HUD -->
<header id="hud">
<div class="hud-item">
<span class="hud-label">PONTOS</span>
<span class="hud-value" id="score">0</span>
</div>
<div class="hud-item">
<span class="hud-label">VIDAS</span>
<span class="hud-value" id="lives">❤️❤️❤️</span>
</div>
<div class="hud-item">
<span class="hud-label">NÍVEL</span>
<span class="hud-value" id="level">1</span>
</div>
<div class="hud-item">
<span class="hud-label">RECORDE</span>
<span class="hud-value" id="highscore">0</span>
</div>
</header>
<!-- Área do jogo -->
<div id="canvas-container">
<canvas id="game-canvas" width="800" height="600"></canvas>
<!-- Tela inicial -->
<div id="screen-start" class="screen active">
<h1>SPACE DEFENDER</h1>
<p class="tagline">Defenda a galáxia dos invasores</p>
<div class="controls">
<div class="control-item">
<span class="key">↑ ↓ ← →</span>
<span class="desc">Mover a nave</span>
</div>
<div class="control-item">
<span class="key">ESPAÇO</span>
<span class="desc">Atirar</span>
</div>
<div class="control-item">
<span class="key">P</span>
<span class="desc">Pausar</span>
</div>
</div>
<button class="btn-start" id="btn-start">▶ INICIAR MISSÃO</button>
<button class="btn-secondary" id="btn-ranking">🏆 VER RANKING</button>
</div>
<!-- Tela de pausa -->
<div id="screen-pause" class="screen">
<h2>PAUSADO</h2>
<p>Pressione <strong>P</strong> para continuar</p>
</div>
<!-- Tela de fim de jogo -->
<div id="screen-gameover" class="screen">
<h2>MISSÃO ENCERRADA</h2>
<div class="final-stats">
<div class="stat">
<span class="stat-label">Pontuação Final</span>
<span class="stat-value" id="final-score">0</span>
</div>
<div class="stat">
<span class="stat-label">Naves Destruídas</span>
<span class="stat-value" id="final-kills">0</span>
</div>
<div class="stat">
<span class="stat-label">Nível Alcançado</span>
<span class="stat-value" id="final-level">1</span>
</div>
</div>
<div class="form-name" id="form-name">
<label for="player-name">Digite seu nome para o ranking:</label>
<input type="text" id="player-name" maxlength="15" placeholder="PILOTO">
<button class="btn-primary" id="btn-save-score">SALVAR PONTUAÇÃO</button>
</div>
<div class="gameover-actions">
<button class="btn-start" id="btn-restart">🔄 JOGAR NOVAMENTE</button>
<button class="btn-secondary" id="btn-ranking2">🏆 VER RANKING</button>
</div>
</div>
<!-- Tela de ranking -->
<div id="screen-ranking" class="screen">
<h2>🏆 TOP 10 PILOTOS</h2>
<div class="ranking-container">
<table id="ranking-table">
<thead>
<tr>
<th>#</th>
<th>PILOTO</th>
<th>PONTOS</th>
</tr>
</thead>
<tbody id="ranking-body">
<tr><td colspan="3" class="loading">Carregando...</td></tr>
</tbody>
</table>
</div>
<button class="btn-start" id="btn-back">← VOLTAR</button>
</div>
</div>
<footer id="game-footer">
<p>Space Defender © <?php echo date('Y'); ?> - Desenvolvido em HTML5 + JavaScript</p>
</footer>
</div>
<script src="game.js"></script>
</body>
</html>
2- Arquivo style.css
/* ============================================
SPACE DEFENDER - Estilo Espacial (CORRIGIDO)
============================================ */
:root {
--space-dark: #05060f;
--space-blue: #0a0e27;
--nebula-purple: #2a1a5e;
--neon-cyan: #00f0ff;
--neon-pink: #ff006e;
--neon-yellow: #ffbe0b;
--neon-green: #06ffa5;
--text: #e0e6ff;
--text-soft: #8892b0;
--border-glow: rgba(0, 240, 255, 0.4);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
height: 100%;
overflow: hidden;
}
body {
font-family: 'Courier New', 'Consolas', monospace;
background: radial-gradient(ellipse at center, var(--space-blue) 0%, var(--space-dark) 100%);
color: var(--text);
display: flex;
align-items: center;
justify-content: center;
padding: 8px;
background-attachment: fixed;
}
body::before {
content: '';
position: fixed;
inset: 0;
background-image:
radial-gradient(1px 1px at 20% 30%, white, transparent),
radial-gradient(1px 1px at 60% 70%, white, transparent),
radial-gradient(1px 1px at 50% 50%, white, transparent),
radial-gradient(1px 1px at 80% 10%, white, transparent),
radial-gradient(1px 1px at 90% 60%, white, transparent),
radial-gradient(1px 1px at 33% 90%, white, transparent),
radial-gradient(1px 1px at 15% 75%, white, transparent),
radial-gradient(1px 1px at 70% 40%, white, transparent);
background-size: 200% 200%;
animation: starfield 60s linear infinite;
pointer-events: none;
opacity: 0.6;
}
@keyframes starfield {
from { background-position: 0 0; }
to { background-position: 200% 200%; }
}
/* ========== WRAPPER RESPONSIVO ========== */
#game-wrapper {
width: 100%;
max-width: 820px;
max-height: calc(100vh - 16px);
display: flex;
flex-direction: column;
position: relative;
z-index: 1;
}
/* ====== HUD ====== */
#hud {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
background: rgba(10, 14, 39, 0.85);
border: 2px solid var(--border-glow);
border-radius: 8px 8px 0 0;
padding: 8px 14px;
backdrop-filter: blur(8px);
box-shadow: 0 0 20px rgba(0, 240, 255, 0.15);
flex-shrink: 0;
}
.hud-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
}
.hud-label {
font-size: 0.6rem;
color: var(--text-soft);
letter-spacing: 2px;
font-weight: 700;
}
.hud-value {
font-size: 1.1rem;
font-weight: 800;
color: var(--neon-cyan);
text-shadow: 0 0 10px var(--neon-cyan);
letter-spacing: 1px;
}
#lives {
font-size: 0.95rem;
letter-spacing: 2px;
}
/* ====== CANVAS RESPONSIVO ====== */
#canvas-container {
position: relative;
border: 2px solid var(--border-glow);
border-top: none;
border-radius: 0 0 8px 8px;
overflow: hidden;
box-shadow: 0 0 30px rgba(0, 240, 255, 0.2), inset 0 0 60px rgba(42, 26, 94, 0.3);
background: var(--space-dark);
flex: 1;
min-height: 0;
/* Mantém proporção e nunca ultrapassa a viewport */
aspect-ratio: 4 / 3;
max-height: calc(100vh - 90px);
margin: 0 auto;
width: 100%;
}
#game-canvas {
display: block;
width: 100%;
height: 100%;
background: var(--space-dark);
}
/* ====== TELAS ====== */
.screen {
position: absolute;
inset: 0;
background: rgba(5, 6, 15, 0.92);
backdrop-filter: blur(6px);
display: none;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
text-align: center;
z-index: 10;
animation: fadeIn 0.35s ease;
overflow-y: auto;
}
.screen.active { display: flex; }
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.screen h1 {
font-size: clamp(1.4rem, 5vw, 2.8rem);
font-weight: 900;
letter-spacing: 6px;
color: var(--neon-cyan);
text-shadow:
0 0 10px var(--neon-cyan),
0 0 30px var(--neon-cyan),
0 0 60px var(--neon-pink);
margin-bottom: 10px;
animation: pulse-glow 2s ease-in-out infinite;
}
.screen h2 {
font-size: clamp(1rem, 3.5vw, 1.8rem);
font-weight: 800;
letter-spacing: 4px;
color: var(--neon-pink);
text-shadow: 0 0 20px var(--neon-pink);
margin-bottom: 16px;
}
@keyframes pulse-glow {
0%, 100% { text-shadow: 0 0 10px var(--neon-cyan), 0 0 30px var(--neon-cyan), 0 0 60px var(--neon-pink); }
50% { text-shadow: 0 0 20px var(--neon-cyan), 0 0 50px var(--neon-cyan), 0 0 90px var(--neon-pink); }
}
.tagline {
color: var(--text-soft);
letter-spacing: 2px;
margin-bottom: 20px;
font-size: 0.85rem;
}
/* Controles */
.controls {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
margin-bottom: 20px;
width: 100%;
max-width: 500px;
}
.control-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
padding: 8px;
border: 1px solid var(--border-glow);
border-radius: 6px;
background: rgba(0, 240, 255, 0.04);
}
.key {
font-weight: 800;
color: var(--neon-yellow);
font-size: 0.85rem;
text-shadow: 0 0 8px var(--neon-yellow);
}
.desc {
color: var(--text-soft);
font-size: 0.65rem;
letter-spacing: 1px;
text-transform: uppercase;
}
/* Botões */
.btn-start, .btn-primary, .btn-secondary {
font-family: inherit;
font-size: 0.9rem;
font-weight: 800;
letter-spacing: 3px;
padding: 12px 28px;
border-radius: 6px;
cursor: pointer;
transition: all 0.25s ease;
margin: 5px;
text-transform: uppercase;
}
.btn-start {
background: linear-gradient(135deg, var(--neon-pink), var(--nebula-purple));
color: white;
border: 2px solid var(--neon-pink);
box-shadow: 0 0 20px rgba(255, 0, 110, 0.5);
}
.btn-start:hover {
transform: translateY(-2px) scale(1.03);
box-shadow: 0 0 35px rgba(255, 0, 110, 0.9);
}
.btn-primary {
background: transparent;
color: var(--neon-cyan);
border: 2px solid var(--neon-cyan);
box-shadow: 0 0 15px rgba(0, 240, 255, 0.4);
font-size: 0.8rem;
padding: 10px 22px;
}
.btn-primary:hover {
background: var(--neon-cyan);
color: var(--space-dark);
box-shadow: 0 0 30px var(--neon-cyan);
}
.btn-secondary {
background: transparent;
color: var(--text-soft);
border: 2px solid var(--text-soft);
font-size: 0.8rem;
padding: 10px 22px;
}
.btn-secondary:hover {
color: var(--text);
border-color: var(--text);
}
/* ====== GAME OVER ====== */
.final-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin: 15px 0 20px;
width: 100%;
max-width: 500px;
}
.stat {
display: flex;
flex-direction: column;
gap: 3px;
padding: 12px 8px;
border: 1px solid var(--border-glow);
border-radius: 6px;
background: rgba(0, 240, 255, 0.04);
}
.stat-label {
font-size: 0.6rem;
color: var(--text-soft);
letter-spacing: 1.5px;
text-transform: uppercase;
}
.stat-value {
font-size: 1.3rem;
font-weight: 800;
color: var(--neon-cyan);
text-shadow: 0 0 12px var(--neon-cyan);
}
.form-name {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 15px;
width: 100%;
max-width: 380px;
}
.form-name label {
font-size: 0.75rem;
color: var(--text-soft);
letter-spacing: 1px;
text-transform: uppercase;
}
#player-name {
padding: 10px 14px;
background: rgba(0, 240, 255, 0.05);
border: 2px solid var(--border-glow);
border-radius: 6px;
color: var(--neon-cyan);
font-family: inherit;
font-size: 1rem;
font-weight: 700;
letter-spacing: 3px;
text-align: center;
text-transform: uppercase;
transition: all 0.25s;
}
#player-name:focus {
outline: none;
border-color: var(--neon-cyan);
box-shadow: 0 0 20px rgba(0, 240, 255, 0.5);
}
#player-name::placeholder {
color: var(--text-soft);
opacity: 0.5;
}
.gameover-actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
justify-content: center;
}
/* ====== RANKING ====== */
.ranking-container {
width: 100%;
max-width: 500px;
max-height: 45vh;
overflow-y: auto;
border: 2px solid var(--border-glow);
border-radius: 8px;
background: rgba(10, 14, 39, 0.7);
margin-bottom: 15px;
}
#ranking-table {
width: 100%;
border-collapse: collapse;
font-size: 0.85rem;
}
#ranking-table th {
background: rgba(0, 240, 255, 0.1);
color: var(--neon-cyan);
padding: 10px;
text-align: left;
font-size: 0.7rem;
letter-spacing: 2px;
text-transform: uppercase;
position: sticky;
top: 0;
}
#ranking-table td {
padding: 9px 10px;
border-bottom: 1px solid rgba(0, 240, 255, 0.1);
color: var(--text);
font-weight: 600;
letter-spacing: 1px;
}
#ranking-table tr:last-child td { border-bottom: none; }
#ranking-table tr.top-1 td {
color: var(--neon-yellow);
text-shadow: 0 0 8px var(--neon-yellow);
font-weight: 800;
}
#ranking-table tr.top-2 td { color: #c0c0c0; }
#ranking-table tr.top-3 td { color: #cd7f32; }
#ranking-table .loading {
text-align: center;
color: var(--text-soft);
font-style: italic;
padding: 25px;
}
/* ====== FOOTER ====== */
#game-footer {
text-align: center;
padding: 6px;
color: var(--text-soft);
font-size: 0.65rem;
letter-spacing: 1px;
flex-shrink: 0;
}
/* ====== RESPONSIVO ====== */
@media (max-width: 700px) {
.screen h1 { letter-spacing: 3px; }
.screen h2 { letter-spacing: 2px; }
.controls { grid-template-columns: 1fr; }
.final-stats { grid-template-columns: 1fr; }
#hud { grid-template-columns: repeat(2, 1fr); gap: 6px; padding: 8px; }
.hud-value { font-size: 0.95rem; }
.btn-start, .btn-primary, .btn-secondary {
font-size: 0.75rem;
padding: 10px 18px;
letter-spacing: 2px;
}
}
/* Notebooks com altura reduzida */
@media (max-height: 720px) {
#canvas-container { max-height: calc(100vh - 80px); }
.screen h1 { margin-bottom: 6px; }
.tagline { margin-bottom: 12px; }
.controls { margin-bottom: 12px; }
}
3- Arquivo game.js
/* ==========================================================
SPACE DEFENDER - VERSÃO FINAL SEM TRAVAMENTO
- Removido AudioContext (causa principal do lag)
- Removido shadowBlur (segunda causa)
- Delta time consistente
- Sem vazamento de memória
========================================================== */
(function () {
'use strict';
// ===================== CONFIGURAÇÕES =====================
const CONFIG = {
canvasWidth: 800,
canvasHeight: 600,
playerSpeed: 300,
bulletSpeed: 480,
enemyBulletSpeed: 240,
enemyBaseSpeed: 75,
enemyShootChance: 0.4,
enemySpawnRate: 1.2,
powerupSpawnRate: 8,
startingLives: 3,
playerInvulnerableTime: 1.5,
playerCooldown: 0.22,
// Limites RESTRITIVOS para performance garantida
MAX_PARTICLES: 120, // antes 250
MAX_ENEMIES: 8, // antes 14
MAX_ENEMY_BULLETS: 20, // antes 35
MAX_BULLETS: 8, // antes 12
RESPAWN_DELAY: 1.5,
RESPAWN_INVULNERABLE: 2.0,
// ✅ NOVO: limites rígidos de spawn por segundo
MAX_PARTICLES_PER_EXPLOSION: 12,
MAX_ENEMY_BULLETS_PER_SEC: 3
};
// ===================== ESTADO =====================
const state = {
running: false,
paused: false,
gameOver: false,
score: 0,
lives: CONFIG.startingLives,
kills: 0,
level: 1,
highScore: parseInt(localStorage.getItem('sd_highscore') || '0'),
enemyTimer: 0,
powerupTimer: 0,
playerDead: false,
respawnTimer: 0,
lastTime: 0,
// ✅ Controle de taxa de tiros inimigos
enemyBulletBudget: 0
};
const player = {
x: CONFIG.canvasWidth / 2,
y: CONFIG.canvasHeight - 70,
width: 40,
height: 40,
invulnerable: 0,
cooldown: 0,
flamePhase: 0
};
let bullets = [];
let enemyBullets = [];
let enemies = [];
let particles = [];
let stars = [];
let powerups = [];
const keys = {
ArrowLeft: false,
ArrowRight: false,
ArrowUp: false,
ArrowDown: false,
Space: false
};
// ===================== DOM =====================
const canvas = document.getElementById('game-canvas');
const ctx = canvas.getContext('2d', { alpha: false });
const hudScore = document.getElementById('score');
const hudLives = document.getElementById('lives');
const hudLevel = document.getElementById('level');
const hudHighScore = document.getElementById('highscore');
const screenStart = document.getElementById('screen-start');
const screenPause = document.getElementById('screen-pause');
const screenGameOver = document.getElementById('screen-gameover');
const screenRanking = document.getElementById('screen-ranking');
const finalScore = document.getElementById('final-score');
const finalKills = document.getElementById('final-kills');
const finalLevel = document.getElementById('final-level');
const inputName = document.getElementById('player-name');
const btnSaveScore = document.getElementById('btn-save-score');
const formName = document.getElementById('form-name');
let bgGradient = null;
// ===================== INICIALIZAÇÃO =====================
function init() {
canvas.width = CONFIG.canvasWidth;
canvas.height = CONFIG.canvasHeight;
bgGradient = ctx.createRadialGradient(
CONFIG.canvasWidth / 2, CONFIG.canvasHeight / 2, 50,
CONFIG.canvasWidth / 2, CONFIG.canvasHeight / 2, CONFIG.canvasHeight
);
bgGradient.addColorStop(0, '#0a0e27');
bgGradient.addColorStop(1, '#05060f');
createStars();
updateHUD();
document.addEventListener('keydown', onKeyDown);
document.addEventListener('keyup', onKeyUp);
document.getElementById('btn-start').addEventListener('click', startGame);
document.getElementById('btn-restart').addEventListener('click', startGame);
document.getElementById('btn-ranking').addEventListener('click', showRanking);
document.getElementById('btn-ranking2').addEventListener('click', showRanking);
document.getElementById('btn-back').addEventListener('click', backToStart);
btnSaveScore.addEventListener('click', saveScore);
requestAnimationFrame(gameLoop);
}
function createStars() {
stars = [];
for (let i = 0; i < 60; i++) {
stars.push({
x: Math.random() * CONFIG.canvasWidth,
y: Math.random() * CONFIG.canvasHeight,
size: Math.random() * 1.8 + 0.3,
speed: Math.random() * 30 + 6,
brightness: Math.random() * 0.5 + 0.5
});
}
}
// ===================== TECLADO =====================
function onKeyDown(e) {
if (keys.hasOwnProperty(e.code)) {
keys[e.code] = true;
e.preventDefault();
}
if (e.code === 'KeyP' && state.running && !state.gameOver) {
state.paused = !state.paused;
screenPause.classList.toggle('active', state.paused);
}
}
function onKeyUp(e) {
if (keys.hasOwnProperty(e.code)) {
keys[e.code] = false;
e.preventDefault();
}
}
// ===================== HUD =====================
function updateHUD() {
hudScore.textContent = state.score.toLocaleString('pt-BR');
hudLives.textContent = '❤️'.repeat(state.lives) + '🖤'.repeat(CONFIG.startingLives - state.lives);
hudLevel.textContent = state.level;
hudHighScore.textContent = state.highScore.toLocaleString('pt-BR');
}
// ===================== CONTROLE =====================
function startGame() {
state.running = true;
state.paused = false;
state.gameOver = false;
state.score = 0;
state.lives = CONFIG.startingLives;
state.kills = 0;
state.level = 1;
state.enemyTimer = 0.5;
state.powerupTimer = CONFIG.powerupSpawnRate;
state.playerDead = false;
state.respawnTimer = 0;
state.lastTime = performance.now();
state.enemyBulletBudget = 0;
resetPlayer();
player.invulnerable = 1.0;
// ✅ Reutiliza os arrays em vez de recriar (evita pressão no GC)
bullets.length = 0;
enemyBullets.length = 0;
enemies.length = 0;
particles.length = 0;
powerups.length = 0;
formName.style.display = 'flex';
btnSaveScore.disabled = false;
btnSaveScore.textContent = 'SALVAR PONTUAÇÃO';
inputName.value = '';
updateHUD();
hideAllScreens();
}
function resetPlayer() {
player.x = CONFIG.canvasWidth / 2;
player.y = CONFIG.canvasHeight - 70;
player.cooldown = 0;
}
function hideAllScreens() {
screenStart.classList.remove('active');
screenPause.classList.remove('active');
screenGameOver.classList.remove('active');
screenRanking.classList.remove('active');
}
function gameOver() {
state.running = false;
state.gameOver = true;
if (state.score > state.highScore) {
state.highScore = state.score;
localStorage.setItem('sd_highscore', state.highScore);
}
updateHUD();
finalScore.textContent = state.score.toLocaleString('pt-BR');
finalKills.textContent = state.kills;
finalLevel.textContent = state.level;
screenGameOver.classList.add('active');
}
// ===================== GAME LOOP =====================
function gameLoop(now) {
requestAnimationFrame(gameLoop);
if (!state.lastTime) state.lastTime = now;
let dt = (now - state.lastTime) / 1000;
state.lastTime = now;
// Clamp: sem isso, ao voltar de uma aba inativa o jogo "teleporta"
if (dt > 0.05) dt = 0.05;
if (dt <= 0) return;
if (!state.running || state.paused) {
drawBackground();
if (state.paused) drawEntities();
return;
}
updateStars(dt);
if (state.playerDead) {
state.respawnTimer -= dt;
if (state.respawnTimer <= 0) {
state.playerDead = false;
resetPlayer();
player.invulnerable = CONFIG.RESPAWN_INVULNERABLE;
}
} else {
updatePlayer(dt);
}
updateBullets(dt);
updateEnemies(dt);
updateEnemyBullets(dt);
updatePowerups(dt);
updateParticles(dt);
checkCollisions();
updateLevel();
// ✅ Controle de taxa de tiros inimigos (por segundo)
state.enemyBulletBudget = Math.min(
CONFIG.MAX_ENEMY_BULLETS_PER_SEC,
state.enemyBulletBudget + dt * CONFIG.MAX_ENEMY_BULLETS_PER_SEC
);
// Spawn de inimigos
state.enemyTimer -= dt;
if (state.enemyTimer <= 0 && enemies.length < CONFIG.MAX_ENEMIES) {
spawnEnemy();
const interval = Math.max(0.5, CONFIG.enemySpawnRate - state.level * 0.08);
state.enemyTimer = interval;
}
// Spawn de power-ups
state.powerupTimer -= dt;
if (state.powerupTimer <= 0) {
spawnPowerup();
state.powerupTimer = CONFIG.powerupSpawnRate;
}
drawBackground();
drawEntities();
}
// ===================== ATUALIZAÇÕES =====================
function updateStars(dt) {
for (let i = 0; i < stars.length; i++) {
const s = stars[i];
s.y += s.speed * dt;
if (s.y > CONFIG.canvasHeight) {
s.y = 0;
s.x = Math.random() * CONFIG.canvasWidth;
}
}
}
function updatePlayer(dt) {
const speed = CONFIG.playerSpeed * dt;
if (keys.ArrowLeft) player.x -= speed;
if (keys.ArrowRight) player.x += speed;
if (keys.ArrowUp) player.y -= speed;
if (keys.ArrowDown) player.y += speed;
player.x = Math.max(player.width / 2, Math.min(CONFIG.canvasWidth - player.width / 2, player.x));
player.y = Math.max(player.height / 2, Math.min(CONFIG.canvasHeight - player.height / 2, player.y));
if (player.invulnerable > 0) player.invulnerable -= dt;
if (player.cooldown > 0) player.cooldown -= dt;
player.flamePhase += dt * 20;
if (keys.Space && player.cooldown <= 0 && bullets.length < CONFIG.MAX_BULLETS) {
bullets.push({
x: player.x,
y: player.y - player.height / 2,
width: 3,
height: 14,
speed: CONFIG.bulletSpeed
});
player.cooldown = CONFIG.playerCooldown;
// ✅ SEM SOM — era o principal causador do lag
}
}
function updateBullets(dt) {
for (let i = bullets.length - 1; i >= 0; i--) {
bullets[i].y -= bullets[i].speed * dt;
if (bullets[i].y < -20) bullets.splice(i, 1);
}
}
function updateEnemyBullets(dt) {
for (let i = enemyBullets.length - 1; i >= 0; i--) {
enemyBullets[i].y += enemyBullets[i].speed * dt;
if (enemyBullets[i].y > CONFIG.canvasHeight + 20) enemyBullets.splice(i, 1);
}
}
function updateEnemies(dt) {
const speed = CONFIG.enemyBaseSpeed + state.level * 12;
for (let i = enemies.length - 1; i >= 0; i--) {
const e = enemies[i];
e.y += speed * dt;
e.x += Math.sin((e.y + e.offset) * 0.02) * 40 * dt;
// ✅ Controle de taxa: usa budget
if (state.level >= 2 &&
enemyBullets.length < CONFIG.MAX_ENEMY_BULLETS &&
state.enemyBulletBudget >= 1 &&
Math.random() < CONFIG.enemyShootChance * state.level * dt) {
enemyBullets.push({
x: e.x,
y: e.y + e.height / 2,
width: 4,
height: 12,
speed: CONFIG.enemyBulletSpeed
});
state.enemyBulletBudget -= 1;
}
if (e.y > CONFIG.canvasHeight + 60) {
enemies.splice(i, 1);
}
}
}
function updatePowerups(dt) {
for (let i = powerups.length - 1; i >= 0; i--) {
const p = powerups[i];
p.y += 90 * dt;
p.rotation += dt * 3;
p.pulse += dt * 6;
if (p.y > CONFIG.canvasHeight + 40) powerups.splice(i, 1);
}
}
function updateParticles(dt) {
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.x += p.vx * dt;
p.y += p.vy * dt;
const friction = Math.pow(0.05, dt);
p.vx *= friction;
p.vy *= friction;
p.life -= dt;
if (p.life <= 0) particles.splice(i, 1);
}
}
function updateLevel() {
const newLevel = Math.floor(state.kills / 10) + 1;
if (newLevel !== state.level) {
state.level = newLevel;
updateHUD();
}
}
// ===================== SPAWN =====================
function spawnEnemy() {
const w = 34, h = 34;
enemies.push({
x: Math.random() * (CONFIG.canvasWidth - w) + w / 2,
y: -h,
width: w,
height: h,
offset: Math.random() * 100,
hp: state.level >= 4 && Math.random() < 0.3 ? 2 : 1
});
}
function spawnPowerup() {
const type = Math.random() < 0.25 ? 'life' : 'points';
powerups.push({
x: Math.random() * (CONFIG.canvasWidth - 60) + 30,
y: -30,
width: 26,
height: 26,
type: type,
rotation: 0,
pulse: 0
});
}
function createExplosion(x, y, color, count) {
// ✅ Limita partículas por explosão
count = Math.min(count, CONFIG.MAX_PARTICLES_PER_EXPLOSION);
if (particles.length + count > CONFIG.MAX_PARTICLES) {
particles.splice(0, particles.length + count - CONFIG.MAX_PARTICLES);
}
for (let i = 0; i < count; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = Math.random() * 250 + 60;
particles.push({
x: x,
y: y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
life: 0.5 + Math.random() * 0.3,
maxLife: 0.8,
color: color,
size: Math.random() * 3 + 1
});
}
}
// ===================== COLISÕES =====================
function checkCollisions() {
// Tiro vs inimigo
for (let i = bullets.length - 1; i >= 0; i--) {
const b = bullets[i];
for (let j = enemies.length - 1; j >= 0; j--) {
const e = enemies[j];
if (circleRectCollision(b.x, b.y, 2, e)) {
bullets.splice(i, 1);
e.hp--;
if (e.hp <= 0) {
createExplosion(e.x, e.y, '#ff006e', 12);
enemies.splice(j, 1);
state.kills++;
state.score += 100;
updateHUD();
} else {
createExplosion(e.x, e.y, '#ffbe0b', 4);
}
break;
}
}
}
if (state.playerDead) return;
// Inimigo vs jogador
if (player.invulnerable <= 0) {
for (let j = enemies.length - 1; j >= 0; j--) {
const e = enemies[j];
if (circleRectCollision(player.x, player.y, player.width / 2 - 4, e)) {
createExplosion(e.x, e.y, '#ff006e', 15);
enemies.splice(j, 1);
hitPlayer();
return;
}
}
for (let i = enemyBullets.length - 1; i >= 0; i--) {
const b = enemyBullets[i];
if (circleRectCollision(player.x, player.y, player.width / 2 - 4, b)) {
enemyBullets.splice(i, 1);
hitPlayer();
return;
}
}
}
// Power-up vs jogador
for (let i = powerups.length - 1; i >= 0; i--) {
const p = powerups[i];
if (circleRectCollision(player.x, player.y, player.width / 2, p)) {
if (p.type === 'life') {
if (state.lives < 5) state.lives++;
state.score += 250;
createExplosion(p.x, p.y, '#06ffa5', 10);
} else {
state.score += 500;
createExplosion(p.x, p.y, '#ffbe0b', 8);
}
powerups.splice(i, 1);
updateHUD();
}
}
}
function hitPlayer() {
state.lives--;
createExplosion(player.x, player.y, '#00f0ff', 18);
updateHUD();
if (state.lives <= 0) {
state.playerDead = true;
gameOver();
return;
}
state.playerDead = true;
state.respawnTimer = CONFIG.RESPAWN_DELAY;
// ✅ Esvazia sem recriar o array
enemyBullets.length = 0;
}
function circleRectCollision(cx, cy, radius, rect) {
const closestX = Math.max(rect.x - rect.width / 2, Math.min(cx, rect.x + rect.width / 2));
const closestY = Math.max(rect.y - rect.height / 2, Math.min(cy, rect.y + rect.height / 2));
const dx = cx - closestX;
const dy = cy - closestY;
return (dx * dx + dy * dy) < (radius * radius);
}
// ===================== DESENHO (SEM shadowBlur) =====================
function drawBackground() {
ctx.fillStyle = bgGradient;
ctx.fillRect(0, 0, CONFIG.canvasWidth, CONFIG.canvasHeight);
// Estrelas sem shadow
for (let i = 0; i < stars.length; i++) {
const s = stars[i];
ctx.globalAlpha = s.brightness;
ctx.fillStyle = '#ffffff';
ctx.fillRect(s.x, s.y, s.size, s.size);
}
ctx.globalAlpha = 1;
}
function drawEntities() {
// ✅ Partículas (só retângulos, SEM sombra)
for (let i = 0; i < particles.length; i++) {
const p = particles[i];
ctx.globalAlpha = Math.max(0, p.life / p.maxLife);
ctx.fillStyle = p.color;
ctx.fillRect(p.x, p.y, p.size, p.size);
}
ctx.globalAlpha = 1;
// Power-ups (estrela desenhada com fill puro)
for (let i = 0; i < powerups.length; i++) {
drawPowerup(powerups[i]);
}
// ✅ Tiros: 2 retângulos com núcleo brilhante, SEM sombra
for (let i = 0; i < bullets.length; i++) {
const b = bullets[i];
// Núcleo externo (mais claro)
ctx.fillStyle = '#80f8ff';
ctx.fillRect(b.x - b.width / 2 - 1, b.y, b.width + 2, b.height);
// Núcleo interno (branco)
ctx.fillStyle = '#ffffff';
ctx.fillRect(b.x - b.width / 2, b.y + 2, b.width, b.height - 4);
}
for (let i = 0; i < enemyBullets.length; i++) {
const b = enemyBullets[i];
ctx.fillStyle = '#ff4d94';
ctx.fillRect(b.x - b.width / 2 - 1, b.y, b.width + 2, b.height);
ctx.fillStyle = '#ffffff';
ctx.fillRect(b.x - b.width / 2, b.y + 2, b.width, b.height - 4);
}
// Inimigos
for (let i = 0; i < enemies.length; i++) {
drawEnemy(enemies[i]);
}
// Jogador
if (!state.playerDead) {
drawPlayer();
}
}
function drawPowerup(p) {
ctx.save();
ctx.translate(p.x, p.y);
ctx.rotate(p.rotation);
const color = p.type === 'life' ? '#06ffa5' : '#ffbe0b';
// ✅ Sem shadowBlur: desenha uma "aura" com círculo semitransparente
ctx.globalAlpha = 0.35;
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(0, 0, 16, 0, Math.PI * 2);
ctx.fill();
ctx.globalAlpha = 1;
ctx.fillStyle = color;
ctx.beginPath();
for (let i = 0; i < 5; i++) {
const angle = (i * 2 * Math.PI / 5) - Math.PI / 2;
ctx.lineTo(Math.cos(angle) * 13, Math.sin(angle) * 13);
const angle2 = angle + Math.PI / 5;
ctx.lineTo(Math.cos(angle2) * 6, Math.sin(angle2) * 6);
}
ctx.closePath();
ctx.fill();
ctx.restore();
ctx.font = 'bold 12px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle = 'white';
ctx.fillText(p.type === 'life' ? '+' : '$', p.x, p.y + 1);
}
function drawPlayer() {
if (player.invulnerable > 0 && Math.floor(performance.now() / 80) % 2 === 0) return;
ctx.save();
ctx.translate(player.x, player.y);
// ✅ Aura simulada (sem shadowBlur)
ctx.globalAlpha = 0.3;
ctx.fillStyle = '#00f0ff';
ctx.beginPath();
ctx.arc(0, 0, 26, 0, Math.PI * 2);
ctx.fill();
ctx.globalAlpha = 1;
// Corpo
ctx.fillStyle = '#00f0ff';
ctx.beginPath();
ctx.moveTo(0, -player.height / 2);
ctx.lineTo(-player.width / 2, player.height / 2);
ctx.lineTo(0, player.height / 4);
ctx.lineTo(player.width / 2, player.height / 2);
ctx.closePath();
ctx.fill();
// Cockpit
ctx.fillStyle = '#0a0e27';
ctx.beginPath();
ctx.arc(0, -4, 5, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#06ffa5';
ctx.beginPath();
ctx.arc(0, -4, 2.5, 0, Math.PI * 2);
ctx.fill();
// Chama
const flame = 6 + Math.sin(player.flamePhase) * 3;
ctx.fillStyle = '#ffbe0b';
ctx.beginPath();
ctx.moveTo(-8, player.height / 2 - 4);
ctx.lineTo(8, player.height / 2 - 4);
ctx.lineTo(0, player.height / 2 + flame);
ctx.closePath();
ctx.fill();
ctx.restore();
}
function drawEnemy(e) {
ctx.save();
ctx.translate(e.x, e.y);
const color = e.hp > 1 ? '#ffbe0b' : '#ff006e';
// ✅ Aura simulada (sem shadowBlur)
ctx.globalAlpha = 0.25;
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(0, 0, e.width * 0.7, 0, Math.PI * 2);
ctx.fill();
ctx.globalAlpha = 1;
ctx.fillStyle = color;
ctx.beginPath();
ctx.moveTo(0, e.height / 2);
ctx.lineTo(-e.width / 2, 0);
ctx.lineTo(-e.width / 3, -e.height / 2);
ctx.lineTo(e.width / 3, -e.height / 2);
ctx.lineTo(e.width / 2, 0);
ctx.closePath();
ctx.fill();
ctx.fillStyle = '#0a0e27';
ctx.beginPath();
ctx.arc(0, 0, 6, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(0, 0, 3, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
// ===================== RANKING =====================
function showRanking() {
hideAllScreens();
screenRanking.classList.add('active');
loadRanking();
}
function backToStart() {
hideAllScreens();
screenStart.classList.add('active');
}
function loadRanking() {
const tbody = document.getElementById('ranking-body');
tbody.innerHTML = '<tr><td colspan="3" class="loading">Carregando...</td></tr>';
fetch('ranking.php?action=list')
.then(r => r.json())
.then(data => {
if (!data || data.length === 0) {
tbody.innerHTML = '<tr><td colspan="3" class="loading">Nenhuma pontuação registrada ainda. Seja o primeiro!</td></tr>';
return;
}
tbody.innerHTML = data.map((entry, i) => {
const cls = i === 0 ? 'top-1' : i === 1 ? 'top-2' : i === 2 ? 'top-3' : '';
const medal = i === 0 ? '🥇' : i === 1 ? '🥈' : i === 2 ? '🥉' : (i + 1);
return `<tr class="${cls}">
<td>${medal}</td>
<td>${escapeHtml(entry.name)}</td>
<td>${Number(entry.score).toLocaleString('pt-BR')}</td>
</tr>`;
}).join('');
})
.catch(() => {
tbody.innerHTML = '<tr><td colspan="3" class="loading">Erro ao carregar. Verifique se ranking.php está no servidor.</td></tr>';
});
}
function saveScore() {
const name = inputName.value.trim().toUpperCase().substring(0, 15);
if (!name) {
inputName.focus();
inputName.placeholder = 'DIGITE SEU NOME!';
return;
}
btnSaveScore.disabled = true;
btnSaveScore.textContent = 'SALVANDO...';
const formData = new FormData();
formData.append('action', 'save');
formData.append('name', name);
formData.append('score', state.score);
fetch('ranking.php', { method: 'POST', body: formData })
.then(r => r.json())
.then(data => {
if (data.success) {
btnSaveScore.textContent = '✅ SALVO!';
formName.style.display = 'none';
} else {
btnSaveScore.disabled = false;
btnSaveScore.textContent = 'SALVAR PONTUAÇÃO';
alert('Erro ao salvar: ' + (data.error || 'desconhecido'));
}
})
.catch(() => {
btnSaveScore.disabled = false;
btnSaveScore.textContent = 'SALVAR PONTUAÇÃO';
alert('Erro de conexão. O ranking.php está publicado?');
});
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
window.addEventListener('load', init);
})();
4- Arquivo ranking.php
<?php
// ranking.php - Backend do ranking do Space Defender
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
$ARQ = __DIR__ . '/ranking.json';
$MAX_ENTRADAS = 10;
// ---------- FUNÇÕES ----------
function carregar() {
global $ARQ;
if (!file_exists($ARQ)) return [];
$dados = json_decode(file_get_contents($ARQ), true);
return is_array($dados) ? $dados : [];
}
function salvar($lista) {
global $ARQ;
file_put_contents($ARQ, json_encode($lista, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE), LOCK_EX);
}
function limpar_nome($nome) {
$nome = trim($nome);
$nome = preg_replace('/[^A-Za-z0-9À-ÿ ]/u', '', $nome);
return mb_substr($nome, 0, 15);
}
// ---------- ROTEAMENTO ----------
$action = $_GET['action'] ?? $_POST['action'] ?? 'list';
if ($action === 'list') {
$lista = carregar();
// Ordena por pontuação decrescente
usort($lista, fn($a, $b) => $b['score'] <=> $a['score']);
$lista = array_slice($lista, 0, $MAX_ENTRADAS);
echo json_encode($lista);
exit;
}
if ($action === 'save') {
$name = limpar_nome($_POST['name'] ?? '');
$score = intval($_POST['score'] ?? 0);
if ($name === '' || $score <= 0) {
echo json_encode(['success' => false, 'error' => 'Nome ou pontuação inválidos.']);
exit;
}
$lista = carregar();
$lista[] = [
'name' => $name,
'score' => $score,
'date' => date('d/m/Y H:i')
];
// Ordena e mantém apenas o top 10
usort($lista, fn($a, $b) => $b['score'] <=> $a['score']);
$lista = array_slice($lista, 0, $MAX_ENTRADAS);
salvar($lista);
echo json_encode(['success' => true]);
exit;
}
if ($action === 'reset') {
// Use apenas para testes — remova em produção!
salvar([]);
echo json_encode(['success' => true]);
exit;
}
echo json_encode(['success' => false, 'error' => 'Ação desconhecida.']);

