Jogo Espacial: Space Defender

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 &copy; <?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.']);