Galaxy Defender
by BlazePanther14813 lines30.4 KB
<!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=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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.