🎮ArcadeLab

Galaxy Defender

by BlazePanther14
813 lines30.4 KB
▶ Play
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Galaxy Defender</title>
<style>
  * { margin:0; padding:0; box-sizing:border-box; -webkit-tap-highlight-color:transparent; }
  html, body {
    height:100%; overflow:hidden; background:#03040a;
    font-family: -apple-system, system-ui, sans-serif;
    touch-action: none; user-select:none;
  }
  #wrap {
    position:relative; width:100%; height:100%;
    display:flex; align-items:center; justify-content:center;
  }
  canvas {
    display:block; background:#03040a;
    max-width:100%; max-height:100%;
    box-shadow: 0 0 50px rgba(0,120,255,0.25);
  }
  #overlay {
    position:absolute; inset:0; display:flex; flex-direction:column;
    align-items:center; justify-content:center; text-align:center;
    color:#fff; background:rgba(3,4,15,0.86);
    padding:24px;
  }
  .screen { display:flex; flex-direction:column; align-items:center; }
  .screen h1 {
    font-size:32px; margin-bottom:6px; letter-spacing:1px;
    color:#5ce1ff; text-shadow:0 0 12px rgba(92,225,255,0.7);
  }
  .screen p { font-size:14px; opacity:0.85; margin-bottom:14px; max-width:300px; line-height:1.5; }
  .score { font-size:18px; margin-bottom:18px; color:#ffd23f; }
  button {
    font-size:17px; font-weight:700; padding:13px 32px; border:none;
    border-radius:30px; background:#ffd23f; color:#3a2a00;
    box-shadow:0 6px 0 #c99f1e; cursor:pointer; margin:6px 0;
  }
  button:active { transform:translateY(4px); box-shadow:0 2px 0 #c99f1e; }
  button.secondary {
    background:transparent; color:#5ce1ff; box-shadow:none;
    border:2px solid #5ce1ff; padding:10px 26px; font-size:14px;
  }
  button.secondary:active { transform:translateY(2px); }
  #leaderboardList { list-style:none; width:280px; margin-bottom:16px; max-height:260px; overflow-y:auto; }
  #leaderboardList li {
    display:flex; justify-content:space-between; padding:7px 10px;
    background:rgba(255,255,255,0.06); border-radius:8px; margin-bottom:5px; font-size:14px;
  }
  #leaderboardList li .rank { color:#ffd23f; font-weight:700; width:24px; }
  #leaderboardList li .name { flex:1; text-align:left; padding-left:6px; }
  #leaderboardList .empty { opacity:0.6; font-size:13px; padding:14px; }
  #nameEntry { display:flex; gap:8px; margin-bottom:8px; }
  #nameInput {
    font-size:16px; padding:10px 12px; border-radius:20px; border:2px solid #5ce1ff;
    background:rgba(255,255,255,0.08); color:#fff; width:150px; text-align:center;
  }
  #nameInput::placeholder{ color:rgba(255,255,255,0.5); }
  #muteBtn {
    position:absolute; top:12px; right:12px; z-index:5;
    width:40px; height:40px; border-radius:50%; padding:0; margin:0;
    background:rgba(255,255,255,0.12); color:#fff; box-shadow:none; font-size:18px;
    display:flex; align-items:center; justify-content:center;
  }
  #hint { position:absolute; bottom:6px; left:0; right:0; text-align:center; color:rgba(255,255,255,0.3); font-size:10px; }
</style>
</head>
<body>
<div id="wrap">
  <canvas id="game" width="420" height="720"></canvas>

  <button id="muteBtn">🔊</button>

  <div id="overlay">
    <div id="startScreen" class="screen">
      <h1>GALAXY DEFENDER</h1>
      <p>Слева — джойстик (двигай корабль в любую сторону), справа — кнопка ОГОНЬ (держи для очереди). На компьютере: стрелки/WASD + пробел.</p>
      <div class="score" id="hi">Рекорд: 0</div>
      <button id="startBtn">Играть</button>
      <button id="showLeaderboardBtn" class="secondary">Таблица лидеров</button>
    </div>

    <div id="gameOverScreen" class="screen" style="display:none">
      <h1>ИГРА ОКОНЧЕНА</h1>
      <p id="goStats">Счёт: 0</p>
      <div id="nameEntry" style="display:none">
        <input id="nameInput" maxlength="12" placeholder="Твоё имя">
      </div>
      <button id="saveScoreBtn" style="display:none">Сохранить результат</button>
      <button id="restartBtn">Заново</button>
      <button id="goLeaderboardBtn" class="secondary">Таблица лидеров</button>
    </div>

    <div id="leaderboardScreen" class="screen" style="display:none">
      <h1>ТОП-10</h1>
      <ol id="leaderboardList"></ol>
      <button id="backBtn">Назад</button>
    </div>
  </div>

  <div id="hint">Собирай бонусы • Проходи волны • Побеждай боссов</div>
</div>

<script>
(function(){
  const canvas = document.getElementById('game');
  const ctx = canvas.getContext('2d');
  const overlay = document.getElementById('overlay');
  const startScreen = document.getElementById('startScreen');
  const gameOverScreen = document.getElementById('gameOverScreen');
  const leaderboardScreen = document.getElementById('leaderboardScreen');
  const startBtn = document.getElementById('startBtn');
  const restartBtn = document.getElementById('restartBtn');
  const showLeaderboardBtn = document.getElementById('showLeaderboardBtn');
  const goLeaderboardBtn = document.getElementById('goLeaderboardBtn');
  const backBtn = document.getElementById('backBtn');
  const saveScoreBtn = document.getElementById('saveScoreBtn');
  const nameEntry = document.getElementById('nameEntry');
  const nameInput = document.getElementById('nameInput');
  const hiEl = document.getElementById('hi');
  const goStats = document.getElementById('goStats');
  const leaderboardList = document.getElementById('leaderboardList');
  const muteBtn = document.getElementById('muteBtn');
  const W = canvas.width, H = canvas.height;

  // ---------- sound ----------
  let audioCtx = null;
  let muted = false;
  try { muted = localStorage.getItem('gd_muted') === '1'; } catch(e){}
  updateMuteBtn();

  function ensureAudio(){
    if (!audioCtx){
      try { audioCtx = new (window.AudioContext||window.webkitAudioContext)(); } catch(e){ return; }
    }
    if (audioCtx.state === 'suspended') audioCtx.resume();
  }
  function updateMuteBtn(){ muteBtn.textContent = muted ? '🔇' : '🔊'; }
  muteBtn.addEventListener('click', ()=>{
    muted = !muted;
    try { localStorage.setItem('gd_muted', muted?'1':'0'); } catch(e){}
    updateMuteBtn();
  });

  function tone(freq, dur, type, vol, glideTo){
    if (muted || !audioCtx) return;
    const t0 = audioCtx.currentTime;
    const osc = audioCtx.createOscillator();
    const gain = audioCtx.createGain();
    osc.type = type || 'square';
    osc.frequency.setValueAtTime(freq, t0);
    if (glideTo) osc.frequency.exponentialRampToValueAtTime(glideTo, t0+dur);
    gain.gain.setValueAtTime(vol||0.12, t0);
    gain.gain.exponentialRampToValueAtTime(0.001, t0+dur);
    osc.connect(gain).connect(audioCtx.destination);
    osc.start(t0); osc.stop(t0+dur);
  }
  function noiseBurst(dur, vol){
    if (muted || !audioCtx) return;
    const size = Math.floor(audioCtx.sampleRate*dur);
    const buffer = audioCtx.createBuffer(1, size, audioCtx.sampleRate);
    const data = buffer.getChannelData(0);
    for (let i=0;i<size;i++){ data[i] = (Math.random()*2-1)*(1-i/size); }
    const src = audioCtx.createBufferSource();
    src.buffer = buffer;
    const gain = audioCtx.createGain();
    gain.gain.setValueAtTime(vol||0.2, audioCtx.currentTime);
    gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime+dur);
    src.connect(gain).connect(audioCtx.destination);
    src.start();
  }
  const sfx = {
    shoot: ()=> tone(880,0.07,'square',0.05,640),
    hit: ()=> tone(420,0.05,'square',0.05),
    explode: (big)=>{ noiseBurst(big?0.5:0.3, big?0.3:0.2); tone(big?70:120, big?0.5:0.28, 'sawtooth', 0.12, 40); },
    playerHit: ()=> tone(220,0.25,'sawtooth',0.18,60),
    powerup: ()=>{ tone(523,0.08,'square',0.12,784); setTimeout(()=>tone(784,0.1,'square',0.1),80); },
    waveClear: ()=>{ tone(660,0.12,'triangle',0.15,880); setTimeout(()=>tone(990,0.16,'triangle',0.15),120); },
    bossAlert: ()=> tone(70,0.7,'sawtooth',0.22,45),
    gameOver: ()=> tone(280,0.5,'sawtooth',0.2,70)
  };

  // ---------- leaderboard ----------
  function loadLeaderboard(){
    try { return JSON.parse(localStorage.getItem('gd_leaderboard')||'[]'); } catch(e){ return []; }
  }
  function saveLeaderboardList(list){
    try { localStorage.setItem('gd_leaderboard', JSON.stringify(list)); } catch(e){}
  }
  function qualifies(score){
    const list = loadLeaderboard();
    if (list.length < 10) return true;
    return score > list[list.length-1].score;
  }
  function addScore(name, score, waveNum){
    const list = loadLeaderboard();
    list.push({ name: (name||'Игрок').slice(0,12), score, wave: waveNum });
    list.sort((a,b)=>b.score-a.score);
    const trimmed = list.slice(0,10);
    saveLeaderboardList(trimmed);
    return trimmed;
  }
  function renderLeaderboard(){
    const list = loadLeaderboard();
    leaderboardList.innerHTML = '';
    if (!list.length){
      leaderboardList.innerHTML = '<div class="empty">Пока нет результатов — сыграй первым!</div>';
      return;
    }
    list.forEach((e,i)=>{
      const li = document.createElement('li');
      li.innerHTML = '<span class="rank">'+(i+1)+'</span><span class="name">'+escapeHtml(e.name)+'</span><span>'+e.score+' очк. (в'+e.wave+')</span>';
      leaderboardList.appendChild(li);
    });
  }
  function escapeHtml(s){
    return String(s).replace(/[&<>"']/g, c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
  }

  // ---------- screen management ----------
  function showScreen(name){
    startScreen.style.display = name==='start' ? 'flex' : 'none';
    gameOverScreen.style.display = name==='gameover' ? 'flex' : 'none';
    leaderboardScreen.style.display = name==='leaderboard' ? 'flex' : 'none';
    overlay.style.display = 'flex';
  }
  function hideOverlay(){ overlay.style.display = 'none'; }

  let returnToGameOver = false;
  showLeaderboardBtn.addEventListener('click', ()=>{ returnToGameOver=false; renderLeaderboard(); showScreen('leaderboard'); });
  goLeaderboardBtn.addEventListener('click', ()=>{ returnToGameOver=true; renderLeaderboard(); showScreen('leaderboard'); });
  backBtn.addEventListener('click', ()=>{ showScreen(returnToGameOver ? 'gameover' : 'start'); });

  // ---------- state ----------
  let state = 'ready';
  let best = 0;
  let score = 0, wave = 0, waveTimer = 0, waveMessage = '';
  let player, bullets, enemyBullets, enemies, particles, powerups;
  let spawnQueue = [];
  let spawnTimer = 0;
  let stars = [];
  let keys = {};
  let shake = 0;

  function loadBest(){ try { return parseInt(localStorage.getItem('gd_best')||'0',10); } catch(e){ return 0; } }
  function saveBest(v){ try { localStorage.setItem('gd_best', String(v)); } catch(e){} }

  function initStars(){
    stars = [];
    for (let i=0;i<80;i++){
      stars.push({ x: Math.random()*W, y: Math.random()*H, r: Math.random()*1.8+0.3, speed: Math.random()*1.5+0.5 });
    }
  }

  function resetGame(){
    player = {
      x: W/2, y: H-110, w: 30, h: 34, speed: 4.2,
      lives: 3, maxLives: 5, invincible: 0,
      fireCooldown: 0, baseCooldown: 14,
      rapid: 0, multishot: 0, shield: 0
    };
    bullets = []; enemyBullets = []; enemies = []; particles = []; powerups = [];
    spawnQueue = []; spawnTimer = 0; score = 0; wave = 0; shake = 0;
    nextWave();
  }

  function nextWave(){
    wave++;
    if (wave % 5 === 0){
      spawnQueue = [{type:'boss', delay:20}];
      setTimeout(()=>{ if(state==='playing') sfx.bossAlert(); }, 300);
    } else {
      const count = 5 + Math.min(10, wave);
      spawnQueue = [];
      for (let i=0;i<count;i++){
        const roll = Math.random();
        let type = 'drone';
        if (roll < 0.28) type = 'shooter';
        else if (roll < 0.55) type = 'zigzag';
        spawnQueue.push({ type, delay: i * Math.max(18, 34 - wave) });
      }
    }
    spawnTimer = 0;
    state = 'playing';
  }

  function spawnEnemy(type){
    if (type === 'boss'){
      enemies.push({ type:'boss', x: W/2, y:-80, w:100, h:80, hp: 24+wave*2, maxHp: 24+wave*2,
        vx:1.6, vy:0.6, shootTimer:70, targetY:100, dir:1, anim:0, flash:0 });
      return;
    }
    const x = 30 + Math.random()*(W-60);
    if (type === 'drone'){
      enemies.push({ type, x, y:-30, w:28, h:24, hp:1, vy:1.6+Math.random()*0.6, vx:0, anim:Math.random()*10, flash:0 });
    } else if (type === 'shooter'){
      enemies.push({ type, x, y:-30, w:30, h:28, hp:2, vy:1.1, vx:0, shootTimer:60+Math.random()*40, anim:0, flash:0 });
    } else if (type === 'zigzag'){
      enemies.push({ type, x, y:-30, w:30, h:26, hp:2, vy:1.4, vx:0, phase:Math.random()*Math.PI*2, baseX:x, anim:0, flash:0 });
    }
  }

  // ---------- input ----------
  const joy = { active:false, id:null, baseX:80, baseY:H-110, knobX:80, knobY:H-110, dx:0, dy:0, maxDist:46 };
  const fireBtn = { active:false, id:null, x:W-80, y:H-110, r:46 };

  function canvasPos(clientX, clientY){
    const rect = canvas.getBoundingClientRect();
    return { x:(clientX-rect.left)*(W/rect.width), y:(clientY-rect.top)*(H/rect.height) };
  }
  function dist(x1,y1,x2,y2){ return Math.hypot(x1-x2, y1-y2); }

  canvas.addEventListener('touchstart', (e)=>{
    e.preventDefault();
    ensureAudio();
    for (const t of e.changedTouches){
      const p = canvasPos(t.clientX, t.clientY);
      if (p.x < W/2 && !joy.active){
        joy.active = true; joy.id = t.identifier;
        joy.baseX = p.x; joy.baseY = p.y; joy.knobX = p.x; joy.knobY = p.y;
        joy.dx = 0; joy.dy = 0;
      } else if (p.x >= W/2 && !fireBtn.active){
        fireBtn.active = true; fireBtn.id = t.identifier;
      }
    }
  }, {passive:false});

  canvas.addEventListener('touchmove', (e)=>{
    e.preventDefault();
    for (const t of e.changedTouches){
      if (joy.active && t.identifier === joy.id){
        const p = canvasPos(t.clientX, t.clientY);
        let dx = p.x - joy.baseX, dy = p.y - joy.baseY;
        const d = Math.hypot(dx,dy);
        if (d > joy.maxDist){ dx = dx/d*joy.maxDist; dy = dy/d*joy.maxDist; }
        joy.knobX = joy.baseX+dx; joy.knobY = joy.baseY+dy;
        joy.dx = dx/joy.maxDist; joy.dy = dy/joy.maxDist;
      }
    }
  }, {passive:false});

  function releaseTouch(e){
    for (const t of e.changedTouches){
      if (joy.active && t.identifier === joy.id){
        joy.active = false; joy.id = null; joy.dx = 0; joy.dy = 0;
        joy.knobX = joy.baseX; joy.knobY = joy.baseY;
      }
      if (fireBtn.active && t.identifier === fireBtn.id){
        fireBtn.active = false; fireBtn.id = null;
      }
    }
  }
  canvas.addEventListener('touchend', releaseTouch, {passive:false});
  canvas.addEventListener('touchcancel', releaseTouch, {passive:false});

  window.addEventListener('keydown', (e)=>{ keys[e.code]=true; if(e.code==='Space') e.preventDefault(); ensureAudio(); });
  window.addEventListener('keyup', (e)=>{ keys[e.code]=false; });

  function isFiring(){ return fireBtn.active || !!keys['Space']; }
  function moveVector(){
    let dx = joy.dx, dy = joy.dy;
    if (keys['ArrowLeft']||keys['KeyA']) dx -= 1;
    if (keys['ArrowRight']||keys['KeyD']) dx += 1;
    if (keys['ArrowUp']||keys['KeyW']) dy -= 1;
    if (keys['ArrowDown']||keys['KeyS']) dy += 1;
    dx = Math.max(-1, Math.min(1, dx));
    dy = Math.max(-1, Math.min(1, dy));
    return {dx,dy};
  }

  function explode(x,y,color,count){
    for (let i=0;i<count;i++){
      const a = Math.random()*Math.PI*2, sp = Math.random()*3+1;
      particles.push({ x,y, vx:Math.cos(a)*sp, vy:Math.sin(a)*sp, life:24+Math.random()*10, color });
    }
  }

  // ---------- update ----------
  function update(){
    for (const s of stars){
      s.y += s.speed;
      if (s.y > H){ s.y = 0; s.x = Math.random()*W; }
    }
    if (shake > 0) shake--;
    if (state !== 'playing') return;

    const mv = moveVector();
    player.x += mv.dx * player.speed;
    player.y += mv.dy * player.speed;
    player.x = Math.max(24, Math.min(W-24, player.x));
    player.y = Math.max(60, Math.min(H-40, player.y));
    if (player.invincible > 0) player.invincible--;
    if (player.rapid > 0) player.rapid--;
    if (player.multishot > 0) player.multishot--;
    if (player.shield > 0) player.shield--;

    if (player.fireCooldown > 0) player.fireCooldown--;
    if (isFiring() && player.fireCooldown <= 0){
      const cd = player.rapid > 0 ? player.baseCooldown/2.2 : player.baseCooldown;
      player.fireCooldown = cd;
      sfx.shoot();
      if (player.multishot > 0){
        bullets.push({x:player.x, y:player.y-20, vx:0, vy:-9});
        bullets.push({x:player.x, y:player.y-20, vx:-2.4, vy:-8.4});
        bullets.push({x:player.x, y:player.y-20, vx:2.4, vy:-8.4});
      } else {
        bullets.push({x:player.x, y:player.y-20, vx:0, vy:-9.5});
      }
    }

    bullets = bullets.filter(b=>b.y > -20 && b.x>-20 && b.x<W+20);
    for (const b of bullets){ b.x += b.vx; b.y += b.vy; }
    enemyBullets = enemyBullets.filter(b=>b.y < H+20);
    for (const b of enemyBullets){ b.x += b.vx; b.y += b.vy; }

    if (spawnQueue.length){
      spawnTimer++;
      while (spawnQueue.length && spawnQueue[0].delay <= spawnTimer){
        spawnEnemy(spawnQueue.shift().type);
      }
    }

    for (const en of enemies){
      en.anim += 0.08;
      if (en.flash > 0) en.flash--;
      if (en.type === 'drone'){
        en.y += en.vy;
      } else if (en.type === 'shooter'){
        en.y += en.vy;
        en.shootTimer--;
        if (en.shootTimer <= 0 && en.y > 0 && en.y < H-140){
          en.shootTimer = 70 + Math.random()*30;
          enemyBullets.push({x:en.x, y:en.y+14, vx:0, vy:4.6});
        }
      } else if (en.type === 'zigzag'){
        en.y += en.vy;
        en.phase += 0.06;
        en.x = en.baseX + Math.sin(en.phase)*50;
      } else if (en.type === 'boss'){
        if (en.y < en.targetY) en.y += en.vy;
        en.x += en.dir * en.vx;
        if (en.x < 80 || en.x > W-80) en.dir *= -1;
        en.shootTimer--;
        if (en.shootTimer <= 0 && en.y >= en.targetY-5){
          en.shootTimer = 100;
          for (let i=-2;i<=2;i++) enemyBullets.push({x:en.x, y:en.y+30, vx:i*1.6, vy:5});
        }
      }
    }
    enemies = enemies.filter(en=>{
      if (en.type !== 'boss' && en.y > H-70){ damagePlayer(1); return false; }
      return true;
    });

    for (const b of bullets){
      for (const en of enemies){
        if (b.hit) continue;
        if (Math.abs(b.x-en.x) < en.w/2 && Math.abs(b.y-en.y) < en.h/2){
          b.hit = true; en.hp--; en.flash = 6;
          sfx.hit();
          explode(b.x,b.y,'#8fe3ff',4);
          if (en.hp <= 0) killEnemy(en);
        }
      }
    }
    bullets = bullets.filter(b=>!b.hit);
    enemies = enemies.filter(en=>en.hp > 0);

    for (const b of enemyBullets){
      if (b.hit) continue;
      if (dist(b.x,b.y,player.x,player.y) < 16 && player.invincible<=0){
        b.hit = true; damagePlayer(1);
      }
    }
    enemyBullets = enemyBullets.filter(b=>!b.hit);

    for (const en of enemies){
      if (player.invincible<=0 && dist(en.x,en.y,player.x,player.y) < (en.w/2+14)){
        damagePlayer(1);
        if (en.type !== 'boss'){ en.hp = 0; explode(en.x,en.y,'#ff6b6b',10); }
      }
    }
    enemies = enemies.filter(en=>en.hp > 0);

    for (const p of powerups){ p.y += 2; }
    powerups = powerups.filter(p=>{
      if (p.y > H+20) return false;
      if (dist(p.x,p.y,player.x,player.y) < 22){ applyPowerup(p.type); sfx.powerup(); return false; }
      return true;
    });

    for (const pt of particles){ pt.x += pt.vx; pt.y += pt.vy; pt.life--; }
    particles = particles.filter(pt=>pt.life>0);

    if (spawnQueue.length === 0 && enemies.length === 0){
      score += wave*10;
      waveMessage = 'Волна ' + wave + ' пройдена!';
      waveTimer = 80;
      state = 'waveclear';
      sfx.waveClear();
    }
  }

  function killEnemy(en){
    const pts = {drone:10, shooter:20, zigzag:15, boss:250}[en.type] || 10;
    score += pts;
    explode(en.x,en.y, en.type==='boss' ? '#ffb347' : '#ffd23f', en.type==='boss'?36:10);
    sfx.explode(en.type==='boss');
    if (en.type==='boss') shake = 24;
    en.hp = 0;
    const dropChance = en.type === 'boss' ? 1 : 0.22;
    if (Math.random() < dropChance){
      const types = ['shield','rapid','multishot','heart'];
      const type = en.type==='boss' ? 'heart' : types[Math.floor(Math.random()*types.length)];
      powerups.push({x:en.x, y:en.y, type});
    }
  }

  function applyPowerup(type){
    if (type === 'shield') player.shield = 420;
    else if (type === 'rapid') player.rapid = 420;
    else if (type === 'multishot') player.multishot = 420;
    else if (type === 'heart') player.lives = Math.min(player.maxLives, player.lives+1);
  }

  function damagePlayer(n){
    if (player.invincible > 0) return;
    if (player.shield > 0){ player.shield = 0; player.invincible = 60; explode(player.x,player.y,'#5ce1ff',14); sfx.playerHit(); return; }
    player.lives -= n;
    player.invincible = 90;
    shake = 14;
    explode(player.x,player.y,'#ff6b6b',16);
    sfx.playerHit();
    if (player.lives <= 0) gameOver();
  }

  function gameOver(){
    state = 'gameover';
    sfx.gameOver();
    if (score > best){ best = score; saveBest(best); }
    hiEl.textContent = 'Рекорд: ' + best;
    goStats.textContent = 'Счёт: ' + score + '  •  Волна: ' + wave;
    if (qualifies(score)){
      nameEntry.style.display = 'flex';
      saveScoreBtn.style.display = 'inline-block';
      nameInput.value = '';
    } else {
      nameEntry.style.display = 'none';
      saveScoreBtn.style.display = 'none';
    }
    showScreen('gameover');
  }

  saveScoreBtn.addEventListener('click', ()=>{
    addScore(nameInput.value.trim(), score, wave);
    nameEntry.style.display = 'none';
    saveScoreBtn.style.display = 'none';
    renderLeaderboard();
    returnToGameOver = true;
    showScreen('leaderboard');
  });

  function updateWaveClear(){
    waveTimer--;
    if (waveTimer <= 0) nextWave();
  }

  // ---------- draw ----------
  function drawStars(){
    ctx.fillStyle = '#fff';
    for (const s of stars){
      ctx.globalAlpha = 0.5 + s.r/2;
      ctx.beginPath(); ctx.arc(s.x,s.y,s.r,0,Math.PI*2); ctx.fill();
    }
    ctx.globalAlpha = 1;
  }

  function drawPlayer(){
    if (player.invincible>0 && Math.floor(player.invincible/6)%2===0) return;
    ctx.save();
    ctx.translate(player.x, player.y);
    const grad = ctx.createLinearGradient(0,-20,0,18);
    grad.addColorStop(0,'#8fe3ff'); grad.addColorStop(1,'#2f8fd4');
    ctx.fillStyle = grad;
    ctx.beginPath();
    ctx.moveTo(0,-22); ctx.lineTo(7,-4); ctx.lineTo(18,20); ctx.lineTo(6,12);
    ctx.lineTo(0,18); ctx.lineTo(-6,12); ctx.lineTo(-18,20); ctx.lineTo(-7,-4);
    ctx.closePath(); ctx.fill();
    ctx.strokeStyle = 'rgba(255,255,255,0.5)'; ctx.lineWidth = 1; ctx.stroke();
    ctx.fillStyle = '#1a2a3a';
    ctx.beginPath(); ctx.ellipse(0,-4,4.5,7,0,0,Math.PI*2); ctx.fill();
    const flameLen = 10 + Math.random()*10;
    const fg = ctx.createLinearGradient(0,16,0,16+flameLen);
    fg.addColorStop(0,'#ffe27a'); fg.addColorStop(1,'rgba(255,120,20,0)');
    ctx.fillStyle = fg;
    ctx.beginPath(); ctx.moveTo(-5,16); ctx.lineTo(5,16); ctx.lineTo(0,16+flameLen); ctx.closePath(); ctx.fill();
    if (player.shield > 0){
      ctx.strokeStyle = 'rgba(92,225,255,0.7)'; ctx.lineWidth = 2;
      ctx.beginPath(); ctx.arc(0,0,28,0,Math.PI*2); ctx.stroke();
    }
    ctx.restore();
  }

  function drawEnemy(en){
    ctx.save();
    ctx.translate(en.x, en.y);
    const flashColor = en.flash > 0 ? '#ffffff' : null;

    if (en.type === 'drone'){
      // insectoid alien: oval body, glowing eyes, twitching antennae
      const wob = Math.sin(en.anim)*3;
      ctx.rotate(Math.sin(en.anim*0.5)*0.08);
      ctx.fillStyle = flashColor || '#3ddc6b';
      ctx.beginPath();
      ctx.ellipse(0,0, en.w/2, en.h/2, 0, 0, Math.PI*2);
      ctx.fill();
      ctx.strokeStyle = '#1f8f43'; ctx.lineWidth = 2; ctx.stroke();
      // antennae
      ctx.strokeStyle = '#3ddc6b'; ctx.lineWidth = 2;
      ctx.beginPath(); ctx.moveTo(-6,-en.h/2); ctx.lineTo(-10+wob, -en.h/2-8); ctx.stroke();
      ctx.beginPath(); ctx.moveTo(6,-en.h/2); ctx.lineTo(10-wob, -en.h/2-8); ctx.stroke();
      // eyes
      ctx.fillStyle = '#ffffff';
      ctx.beginPath(); ctx.arc(-6,-1,4,0,Math.PI*2); ctx.arc(6,-1,4,0,Math.PI*2); ctx.fill();
      ctx.fillStyle = '#0a0a0a';
      ctx.beginPath(); ctx.arc(-6,-1,2,0,Math.PI*2); ctx.arc(6,-1,2,0,Math.PI*2); ctx.fill();
      // legs
      ctx.strokeStyle = '#1f8f43';
      for (let i=-1;i<=1;i+=2){
        ctx.beginPath(); ctx.moveTo(i*8, 6); ctx.lineTo(i*14, 14+wob*0.3); ctx.stroke();
      }
    }
    else if (en.type === 'shooter'){
      // robot with cannon
      ctx.fillStyle = flashColor || '#e04b4b';
      ctx.beginPath();
      ctx.moveTo(-en.w/2, -en.h/2); ctx.lineTo(en.w/2, -en.h/2);
      ctx.lineTo(en.w/2-3, en.h/2-4); ctx.lineTo(-en.w/2+3, en.h/2-4);
      ctx.closePath(); ctx.fill();
      ctx.strokeStyle = '#8a1f1f'; ctx.lineWidth = 2; ctx.stroke();
      // side pods
      ctx.fillStyle = '#8a1f1f';
      ctx.fillRect(-en.w/2-4, -4, 5, 12);
      ctx.fillRect(en.w/2-1, -4, 5, 12);
      // cannon
      ctx.fillStyle = '#2b2b2b';
      ctx.fillRect(-4, en.h/2-6, 8, 12);
      // blinking eye
      const blink = (Math.sin(en.anim*3)+1)/2;
      ctx.fillStyle = `rgba(255,255,${Math.floor(80+blink*100)},1)`;
      ctx.beginPath(); ctx.arc(0,-2,4,0,Math.PI*2); ctx.fill();
    }
    else if (en.type === 'zigzag'){
      // jellyfish-like alien with waving tentacles
      ctx.fillStyle = flashColor || '#ffd23f';
      ctx.beginPath();
      ctx.arc(0,-2, en.w/2, Math.PI, 0);
      ctx.closePath(); ctx.fill();
      ctx.strokeStyle = '#c99f1e'; ctx.lineWidth = 2; ctx.stroke();
      ctx.strokeStyle = '#e0b93a'; ctx.lineWidth = 3; ctx.lineCap = 'round';
      for (let i=-2;i<=2;i++){
        const wob = Math.sin(en.anim*2 + i)*6;
        ctx.beginPath();
        ctx.moveTo(i*6, 0);
        ctx.quadraticCurveTo(i*6+wob*0.5, 8, i*6+wob, 16);
        ctx.stroke();
      }
      ctx.fillStyle = '#0a0a0a';
      ctx.beginPath(); ctx.arc(-6,-4,2.4,0,Math.PI*2); ctx.arc(6,-4,2.4,0,Math.PI*2); ctx.fill();
    }
    else if (en.type === 'boss'){
      ctx.fillStyle = flashColor || '#a34bff';
      ctx.beginPath();
      ctx.moveTo(-50,-8); ctx.lineTo(-24,-34); ctx.lineTo(24,-34); ctx.lineTo(50,-8);
      ctx.lineTo(34,28); ctx.lineTo(-34,28); ctx.closePath(); ctx.fill();
      ctx.strokeStyle = '#5c1f8f'; ctx.lineWidth = 3; ctx.stroke();
      // weapon pods
      ctx.fillStyle = '#5c1f8f';
      const podY = Math.sin(en.anim)*2;
      ctx.beginPath(); ctx.arc(-46, podY, 9, 0, Math.PI*2); ctx.fill();
      ctx.beginPath(); ctx.arc(46, podY, 9, 0, Math.PI*2); ctx.fill();
      // central eye
      ctx.fillStyle = '#ffffff';
      ctx.beginPath(); ctx.arc(0,-4,14,0,Math.PI*2); ctx.fill();
      const pulse = 5 + Math.sin(en.anim*3)*2;
      ctx.fillStyle = '#ff2f6b';
      ctx.beginPath(); ctx.arc(0,-4,pulse,0,Math.PI*2); ctx.fill();
      ctx.restore();
      // hp bar
      ctx.fillStyle = 'rgba(255,255,255,0.2)';
      ctx.fillRect(W/2-75, 14, 150, 9);
      ctx.fillStyle = '#ff6b6b';
      ctx.fillRect(W/2-75, 14, 150*Math.max(0,en.hp/en.maxHp), 9);
      ctx.strokeStyle = 'rgba(255,255,255,0.4)'; ctx.strokeRect(W/2-75, 14, 150, 9);
      return;
    }
    ctx.restore();
  }

  function drawBullets(){
    ctx.fillStyle = '#8fe3ff';
    for (const b of bullets){
      ctx.save();
      ctx.shadowColor = '#8fe3ff'; ctx.shadowBlur = 6;
      ctx.fillRect(b.x-2, b.y-9, 4, 13);
      ctx.restore();
    }
    ctx.fillStyle = '#ff8080';
    for (const b of enemyBullets){
      ctx.save();
      ctx.shadowColor = '#ff8080'; ctx.shadowBlur = 6;
      ctx.beginPath(); ctx.arc(b.x,b.y,4,0,Math.PI*2); ctx.fill();
      ctx.restore();
    }
  }

  function drawParticles(){
    for (const p of particles){
      ctx.globalAlpha = Math.max(0, p.life/30);
      ctx.fillStyle = p.color;
      ctx.beginPath(); ctx.arc(p.x,p.y,2.4,0,Math.PI*2); ctx.fill();
    }
    ctx.globalAlpha = 1;
  }

  function drawPowerups(){
    const labels = { shield:'S', rapid:'R', multishot:'M', heart:'+' };
    const colors = { shield:'#5ce1ff', rapid:'#ffd23f', multishot:'#c86bff', heart:'#ff6b6b' };
    for (const p of powerups){
      ctx.fillStyle = colors[p.type];
      ctx.beginPath(); ctx.arc(p.x,p.y,12,0,Math.PI*2); ctx.fill();
      ctx.fillStyle = '#0a0a0a';
      ctx.font = 'bold 13px sans-serif';
      ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
      ctx.fillText(labels[p.type], p.x, p.y+1);
    }
  }

  function drawHUD(){
    ctx.textAlign = 'left'; ctx.textBaseline = 'top';
    ctx.fillStyle = '#fff'; ctx.font = 'bold 20px sans-serif';
    ctx.fillText('Счёт: ' + score, 14, 14);
    ctx.font = 'bold 14px sans-serif'; ctx.fillStyle = '#8fe3ff';
    ctx.fillText('Волна ' + wave, 14, 40);
    for (let i=0;i<player.maxLives;i++){
      ctx.fillStyle = i < player.lives ? '#ff6b6b' : 'rgba(255,255,255,0.15)';
      ctx.beginPath(); ctx.arc(W-30-i*22, 22, 8, 0, Math.PI*2); ctx.fill();
    }
    if (state === 'waveclear'){
      ctx.textAlign = 'center';
      ctx.fillStyle = '#ffd23f'; ctx.font = 'bold 26px sans-serif';
      ctx.fillText(waveMessage, W/2, H/2-20);
    }
  }

  function drawControls(){
    ctx.beginPath();
    ctx.arc(joy.active?joy.baseX:80, joy.active?joy.baseY:H-110, 46, 0, Math.PI*2);
    ctx.fillStyle = 'rgba(255,255,255,0.10)'; ctx.fill();
    ctx.beginPath();
    ctx.arc(joy.active?joy.knobX:80, joy.active?joy.knobY:H-110, 22, 0, Math.PI*2);
    ctx.fillStyle = 'rgba(255,255,255,0.28)'; ctx.fill();
    ctx.beginPath();
    ctx.arc(fireBtn.x, fireBtn.y, fireBtn.r, 0, Math.PI*2);
    ctx.fillStyle = fireBtn.active ? 'rgba(255,107,107,0.45)' : 'rgba(255,107,107,0.22)';
    ctx.fill();
    ctx.fillStyle = 'rgba(255,255,255,0.85)';
    ctx.font = 'bold 13px sans-serif';
    ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
    ctx.fillText('ОГОНЬ', fireBtn.x, fireBtn.y);
  }

  function draw(){
    ctx.save();
    if (shake > 0){
      ctx.translate((Math.random()-0.5)*shake*0.6, (Math.random()-0.5)*shake*0.6);
    }
    ctx.clearRect(-20,-20,W+40,H+40);
    drawStars();
    if (state === 'playing' || state === 'waveclear'){
      drawPowerups();
      for (const en of enemies) drawEnemy(en);
      drawBullets();
      drawParticles();
      drawPlayer();
      drawHUD();
      drawControls();
    }
    ctx.restore();
  }

  function loop(){
    if (state === 'waveclear') updateWaveClear();
    update();
    draw();
    requestAnimationFrame(loop);
  }

  startBtn.addEventListener('click', ()=>{ ensureAudio(); hideOverlay(); resetGame(); });
  restartBtn.addEventListener('click', ()=>{ ensureAudio(); hideOverlay(); resetGame(); });

  best = loadBest();
  hiEl.textContent = 'Рекорд: ' + best;
  initStars();
  draw();
  loop();
})();
</script>
</body>
</html>

Game Source: Galaxy Defender

Creator: BlazePanther14

Libraries: none

Complexity: complex (813 lines, 30.4 KB)

The full source code is displayed above on this page.

Remix Instructions

To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: galaxy-defender-blazepanther14" to link back to the original. Then publish at arcadelab.ai/publish.