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().finalScore(game.score)}
${t().crosslinks} ./tanks · ./blocks
${t().hint}