import { getLang, initLangToggle } from './i18n.js'; const CANVAS_W = 420; const CANVAS_H = 520; const PLAYER_Y = CANVAS_H - 40; const PLAYER_W = 28; const PLAYER_H = 14; const PLAYER_SPEED = 220; // px/s const BULLET_SPEED = 380; const ENEMY_BULLET_SPEED = 180; const FIRE_COOLDOWN_MS = 350; const ENEMY_COLS = 8; const ENEMY_ROWS = 4; const ENEMY_W = 28; const ENEMY_H = 18; const ENEMY_GAP_X = 12; const ENEMY_GAP_Y = 16; const ENEMY_TOP = 50; const ENEMY_STEP_DOWN = 18; const STR = { ru: { hint: 'стрелки — двигать, пробел — огонь', score: 'счёт', lives: 'жизни', wave: 'волна', gameOver: 'ИГРА ОКОНЧЕНА', finalScore: (s) => `счёт: ${s}`, again: 'ещё раз', crosslinks: 'ещё игры:', fire: 'огонь', }, en: { hint: 'arrows to move, space to fire', score: 'score', lives: 'lives', wave: 'wave', gameOver: 'GAME OVER', finalScore: (s) => `score: ${s}`, again: 'play again', crosslinks: 'more games:', fire: 'fire', }, }; function t() { return STR[getLang()]; } function rectsOverlap(a, b) { return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y; } class InvadersGame { constructor(canvas) { this.canvas = canvas; this.ctx = canvas.getContext('2d'); canvas.width = CANVAS_W; canvas.height = CANVAS_H; this.input = { left: false, right: false, fire: false }; this.reset(); } reset() { this.score = 0; this.lives = 3; this.wave = 1; this.over = false; this.player = { x: CANVAS_W / 2 - PLAYER_W / 2, y: PLAYER_Y }; this.bullets = []; this.enemyBullets = []; this.fireTimer = 0; this.hitFlash = 0; this.spawnWave(); } spawnWave() { this.enemies = []; const totalWidth = ENEMY_COLS * (ENEMY_W + ENEMY_GAP_X) - ENEMY_GAP_X; const startX = (CANVAS_W - totalWidth) / 2; for (let row = 0; row < ENEMY_ROWS; row++) { for (let col = 0; col < ENEMY_COLS; col++) { this.enemies.push({ x: startX + col * (ENEMY_W + ENEMY_GAP_X), y: ENEMY_TOP + row * (ENEMY_H + ENEMY_GAP_Y), w: ENEMY_W, h: ENEMY_H, alive: true, points: (ENEMY_ROWS - row) * 10, }); } } this.enemyDir = 1; this.enemySpeed = 30 + (this.wave - 1) * 12; this.enemyShootTimer = 0; } aliveEnemies() { return this.enemies.filter((e) => e.alive); } tick(dtMs) { if (this.over) return; const dt = dtMs / 1000; if (this.input.left) this.player.x -= PLAYER_SPEED * dt; if (this.input.right) this.player.x += PLAYER_SPEED * dt; this.player.x = Math.max(0, Math.min(CANVAS_W - PLAYER_W, this.player.x)); this.fireTimer -= dtMs; if (this.input.fire && this.fireTimer <= 0) { this.bullets.push({ x: this.player.x + PLAYER_W / 2 - 2, y: this.player.y, w: 4, h: 10 }); this.fireTimer = FIRE_COOLDOWN_MS; } for (const b of this.bullets) b.y -= BULLET_SPEED * dt; this.bullets = this.bullets.filter((b) => b.y + b.h > 0); for (const b of this.enemyBullets) b.y += ENEMY_BULLET_SPEED * dt; this.enemyBullets = this.enemyBullets.filter((b) => b.y < CANVAS_H); const alive = this.aliveEnemies(); if (alive.length === 0) { this.wave += 1; this.spawnWave(); return; } let hitEdge = false; for (const e of alive) { e.x += this.enemyDir * this.enemySpeed * dt; if (e.x <= 0 || e.x + e.w >= CANVAS_W) hitEdge = true; } if (hitEdge) { this.enemyDir *= -1; for (const e of alive) { e.y += ENEMY_STEP_DOWN; if (e.y + e.h >= this.player.y) { this.over = true; } } } this.enemyShootTimer -= dtMs; if (this.enemyShootTimer <= 0 && alive.length > 0) { const shooter = alive[Math.floor(Math.random() * alive.length)]; this.enemyBullets.push({ x: shooter.x + shooter.w / 2 - 2, y: shooter.y + shooter.h, w: 4, h: 10 }); this.enemyShootTimer = Math.max(250, 900 - this.wave * 60); } // пуля игрока vs враги for (const bullet of this.bullets) { for (const e of alive) { if (!e.alive) continue; if (rectsOverlap(bullet, e)) { e.alive = false; bullet.hit = true; this.score += e.points; } } } this.bullets = this.bullets.filter((b) => !b.hit); // пуля врага vs игрок const playerRect = { x: this.player.x, y: this.player.y, w: PLAYER_W, h: PLAYER_H }; for (const b of this.enemyBullets) { if (rectsOverlap(b, playerRect)) { b.hit = true; this.lives -= 1; this.hitFlash = 200; if (this.lives <= 0) this.over = true; } } this.enemyBullets = this.enemyBullets.filter((b) => !b.hit); if (this.hitFlash > 0) this.hitFlash -= dtMs; } draw() { const ctx = this.ctx; ctx.fillStyle = '#0a0e0c'; ctx.fillRect(0, 0, CANVAS_W, CANVAS_H); for (const e of this.enemies) { if (!e.alive) continue; ctx.fillStyle = '#39e075'; ctx.fillRect(e.x, e.y, e.w, e.h); ctx.fillStyle = '#0a0e0c'; ctx.fillRect(e.x + 4, e.y + 4, 4, 4); ctx.fillRect(e.x + e.w - 8, e.y + 4, 4, 4); } ctx.fillStyle = this.hitFlash > 0 ? '#ffffff' : '#00ff41'; ctx.fillRect(this.player.x, this.player.y, PLAYER_W, PLAYER_H); ctx.fillRect(this.player.x + PLAYER_W / 2 - 3, this.player.y - 6, 6, 6); ctx.fillStyle = '#c7c7c7'; for (const b of this.bullets) ctx.fillRect(b.x, b.y, b.w, b.h); ctx.fillStyle = '#9fa3a0'; for (const b of this.enemyBullets) ctx.fillRect(b.x, b.y, b.w, b.h); } } let game = null; let rafId = null; function updateStats() { document.getElementById('arcade-score').textContent = game.score; document.getElementById('arcade-lives').textContent = game.lives; document.getElementById('arcade-wave').textContent = game.wave; } function showGameOver() { const overlay = document.getElementById('arcade-result-overlay'); overlay.innerHTML = `

${t().gameOver}

${t().finalScore(game.score)}

`; overlay.style.display = 'flex'; } function loop(time) { if (game.lastTime === undefined || game.lastTime === null) game.lastTime = time; const dt = time - game.lastTime; game.lastTime = time; const wasOver = game.over; game.tick(dt); game.draw(); updateStats(); if (game.over && !wasOver) showGameOver(); rafId = requestAnimationFrame(loop); } function restart() { const overlay = document.getElementById('arcade-result-overlay'); overlay.style.display = 'none'; overlay.innerHTML = ''; game.reset(); } function bindKeyboard() { window.addEventListener('keydown', (e) => { if (!game) return; if (game.over) { if (e.code === 'Space' || e.code === 'Enter') restart(); return; } if (e.code === 'ArrowLeft') { game.input.left = true; e.preventDefault(); } if (e.code === 'ArrowRight') { game.input.right = true; e.preventDefault(); } if (e.code === 'Space') { game.input.fire = true; e.preventDefault(); } }); window.addEventListener('keyup', (e) => { if (!game) return; if (e.code === 'ArrowLeft') game.input.left = false; if (e.code === 'ArrowRight') game.input.right = false; if (e.code === 'Space') game.input.fire = false; }); } function bindTouchControls(root) { const press = (key, value) => { if (game) game.input[key] = value; }; root.querySelectorAll('[data-input]').forEach((btn) => { const key = btn.dataset.input; btn.addEventListener('mousedown', () => press(key, true)); btn.addEventListener('mouseup', () => press(key, false)); btn.addEventListener('mouseleave', () => press(key, false)); btn.addEventListener('touchstart', (e) => { e.preventDefault(); press(key, true); }); btn.addEventListener('touchend', (e) => { e.preventDefault(); press(key, false); }); }); } function renderShell(root) { root.innerHTML = `
${t().score.toUpperCase()} 0
${t().lives.toUpperCase()} 3
${t().wave.toUpperCase()} 1

${t().hint}

`; } function updateStaticText() { const byId = (id) => document.getElementById(id); if (byId('arcade-crosslinks-label')) byId('arcade-crosslinks-label').textContent = t().crosslinks; if (byId('arcade-score-label')) byId('arcade-score-label').textContent = t().score.toUpperCase(); if (byId('arcade-lives-label')) byId('arcade-lives-label').textContent = t().lives.toUpperCase(); if (byId('arcade-wave-label')) byId('arcade-wave-label').textContent = t().wave.toUpperCase(); if (byId('arcade-hint')) byId('arcade-hint').textContent = t().hint; if (byId('arcade-fire-btn')) byId('arcade-fire-btn').textContent = t().fire; if (game?.over) showGameOver(); } function init() { const root = document.getElementById('arcade-root'); renderShell(root); const canvas = document.getElementById('arcade-canvas'); game = new InvadersGame(canvas); bindTouchControls(root.querySelector('.arcade-controls')); root.querySelector('#arcade-result-overlay').addEventListener('click', (e) => { if (e.target.closest('[data-action="again"]')) restart(); }); if (rafId) cancelAnimationFrame(rafId); rafId = requestAnimationFrame(loop); } bindKeyboard(); init(); initLangToggle(updateStaticText);