🎮ArcadeLab

Красный Шарик: Квадраты и Босс

by TurboGamer88
454 lines17.2 KB
▶ Play
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Красный Шарик: Квадраты и Босс</title>
<style>
  body { margin: 0; background: #0a0a12; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; font-family: Arial; overflow: hidden; touch-action: none; }
  canvas { background: #1a1a2e; border: 2px solid #e94560; border-radius: 8px; touch-action: none; max-width: 100vw; max-height: 80vh; }
  #hud { font-size: 14px; margin-bottom: 4px; color: #e94560; font-weight: bold; text-align: center; }
  .btn { position: absolute; bottom: 20px; width: 65px; height: 65px; border-radius: 50%; background: rgba(233,69,96,0.2); color: #e94560; font-size: 26px; border: 2px solid #e94560; display: flex; align-items: center; justify-content: center; user-select: none; touch-action: none; }
  #left { left: 12px; }
  #right { left: 88px; }
  #jump { right: 12px; background: rgba(0,150,0,0.3); border-color: #0f0; color: #0f0; }
</style>
</head>
<body>
<div id="hud">🔴 Красный Шарик | Уровень: 1 | Убито: 0</div>
<canvas id="game" width="600" height="400"></canvas>
<button class="btn" id="left">◀</button>
<button class="btn" id="right">▶</button>
<button class="btn" id="jump">▲</button>

<script>
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const hud = document.getElementById('hud');

let player, platforms, enemies, bombs, coins, boss, particles;
let keys = {}, score = 0, lives = 3, currentLevel = 1, gameOver = false, win = false;
let cameraX = 0, invincibleTimer = 0;
let bossActive = false;

const GRAVITY = 0.65;
const ACCEL = 0.9;
const MAX_SPEED = 6;
const FRICTION = 0.92;
const JUMP_FORCE = -14;
const MAX_FALL = 18;

let audioCtx = null;
function initAudio() { if (!audioCtx) try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); } catch(e) {} }
function playSound(f, d, t, v) {
  if (!audioCtx) return;
  try {
    const o = audioCtx.createOscillator(), g = audioCtx.createGain();
    o.type = t || 'square'; o.frequency.value = f;
    g.gain.setValueAtTime(v || 0.08, audioCtx.currentTime);
    g.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + d);
    o.connect(g); g.connect(audioCtx.destination); o.start(); o.stop(audioCtx.currentTime + d);
  } catch(e) {}
}
function sfxJump() { playSound(350, 0.1, 'square', 0.06); }
function sfxHit() { playSound(150, 0.2, 'sawtooth', 0.12); }
function sfxKill() { playSound(600, 0.1, 'square', 0.1); setTimeout(() => playSound(400, 0.15, 'square', 0.1), 60); }
function sfxBossHit() { playSound(200, 0.15, 'sawtooth', 0.15); }
function sfxBoom() { playSound(60, 0.4, 'sawtooth', 0.2); setTimeout(() => playSound(40, 0.3, 'sawtooth', 0.15), 100); }
function sfxWin() { playSound(523, 0.12, 'sine', 0.1); setTimeout(() => playSound(659, 0.12, 'sine', 0.1), 120); setTimeout(() => playSound(784, 0.25, 'sine', 0.1), 240); }

function initLevel(level) {
  currentLevel = level;
  // КРАСНЫЙ ШАРИК
  player = { x: 100, y: 300, r: 20, vx: 0, vy: 0, onGround: false, rotation: 0, facing: 1 };
  platforms = []; enemies = []; bombs = []; coins = []; particles = []; boss = null;
  score = 0; lives = 3; gameOver = false; win = false; bossActive = false;
  cameraX = 0; invincibleTimer = 0;
  updateHUD();

  // Земля
  platforms.push({ x: 0, y: 360, w: 2500, h: 40, type: 'ground' });

  // Платформы
  let px = 300;
  for (let i = 0; i < 8; i++) {
    const py = 300 - (i % 2) * 60;
    const pw = 100 + Math.random() * 50;
    platforms.push({ x: px, y: py, w: pw, h: 20, type: 'platform' });
    px += 250 + Math.random() * 80;
  }

  // 1. Голубые квадраты (1 HP)
  for (let i = 0; i < 4; i++) {
    enemies.push({ x: 400 + i * 250, y: 320, w: 30, h: 30, type: 'blue', hp: 1, maxHp: 1, alive: true, color: '#3498db', vx: 1.5, minX: 350 + i * 250, maxX: 550 + i * 250 });
  }
  // 2. Золотые квадраты (2 HP)
  for (let i = 0; i < 3; i++) {
    enemies.push({ x: 700 + i * 300, y: 320, w: 30, h: 30, type: 'gold', hp: 2, maxHp: 2, alive: true, color: '#ffd700', vx: 1, minX: 650 + i * 300, maxX: 850 + i * 300 });
  }

  // 3. Чёрные бомбы (взрываются)
  for (let i = 0; i < 3; i++) {
    bombs.push({ x: 1000 + i * 250, y: 340, w: 28, h: 28, alive: true, fuse: 0, armed: false, vx: 0 });
  }

  // Босс на 3 уровне (финальный)
  if (level === 3) {
    bossActive = true;
    boss = {
      x: 2000, y: 240, w: 90, h: 90,
      hp: 8, maxHp: 8, alive: true, vx: 1.5, minX: 1800, maxX: 2200,
      hitTimer: 0
    };
  }

  // Монеты
  for (let i = 0; i < 12; i++) {
    coins.push({ x: 200 + i * 180, y: 280 + (i % 3) * 20, r: 8, collected: false });
  }
}

function updateHUD() {
  let text = `🔴 Красный Шарик | Уровень: ${currentLevel} | Убито: ${score} | ❤${lives}`;
  if (boss && boss.alive) text += ` | БОСС: ${boss.hp}/${boss.maxHp}`;
  hud.textContent = text;
}

function spawnParticles(x, y, color, count) {
  for (let i = 0; i < count; i++) {
    particles.push({ x, y, vx: (Math.random() - 0.5) * 8, vy: (Math.random() - 0.5) * 8 - 1, life: 20 + Math.random() * 15, maxLife: 35, color, size: 2 + Math.random() * 4 });
  }
}

function damagePlayer(fromX) {
  lives--; invincibleTimer = 90;
  player.vy = -8; player.vx = (player.x < fromX ? -1 : 1) * 6;
  sfxHit(); spawnParticles(player.x, player.y, '#ff0066', 10);
  if (lives <= 0) { gameOver = true; }
  updateHUD();
}

// === КРАСНЫЙ ШАРИК ===
function drawPlayer() {
  const px = player.x - cameraX, py = player.y;
  if (invincibleTimer > 0 && Math.floor(invincibleTimer / 4) % 2 === 0) return;

  // Тень
  ctx.fillStyle = 'rgba(0,0,0,0.25)';
  ctx.beginPath(); ctx.ellipse(px, py + player.r + 2, player.r * 0.8, 5, 0, 0, Math.PI * 2); ctx.fill();

  // Тело (вращается)
  ctx.save();
  ctx.translate(px, py);
  ctx.rotate(player.rotation);
  const grd = ctx.createRadialGradient(-player.r/3, -player.r/3, 2, 0, 0, player.r);
  grd.addColorStop(0, '#e94560'); grd.addColorStop(1, '#b02a40');
  ctx.beginPath(); ctx.arc(0, 0, player.r, 0, Math.PI * 2);
  ctx.fillStyle = grd; ctx.fill();
  ctx.strokeStyle = '#b02a40'; ctx.lineWidth = 3; ctx.stroke();
  ctx.restore();

  // Блик
  ctx.fillStyle = 'rgba(255,255,255,0.5)';
  ctx.beginPath(); ctx.arc(px - player.r*0.35, py - player.r*0.35, player.r*0.25, 0, Math.PI * 2); ctx.fill();

  // Глаза (фиксированные)
  ctx.fillStyle = '#fff';
  ctx.beginPath(); ctx.arc(px - 6, py - 4, 6, 0, Math.PI * 2); ctx.arc(px + 6, py - 4, 6, 0, Math.PI * 2); ctx.fill();
  ctx.fillStyle = '#000';
  ctx.beginPath(); ctx.arc(px - 6 + player.facing*2, py - 4, 3, 0, Math.PI * 2); ctx.arc(px + 6 + player.facing*2, py - 4, 3, 0, Math.PI * 2); ctx.fill();
}

function drawEnemy(e) {
  if (!e.alive) return;
  const ex = e.x - cameraX;
  ctx.fillStyle = e.color;
  ctx.fillRect(ex, e.y, e.w, e.h);
  ctx.strokeStyle = '#000'; ctx.lineWidth = 2; ctx.strokeRect(ex, e.y, e.w, e.h);
  // Глаза
  ctx.fillStyle = '#fff';
  ctx.fillRect(ex + 5, e.y + 8, 6, 6);
  ctx.fillRect(ex + e.w - 11, e.y + 8, 6, 6);
  ctx.fillStyle = '#000';
  ctx.fillRect(ex + 7, e.y + 10, 3, 3);
  ctx.fillRect(ex + e.w - 9, e.y + 10, 3, 3);
  // HP (для золотых)
  if (e.maxHp > 1) {
    ctx.fillStyle = '#000'; ctx.fillRect(ex, e.y - 8, e.w, 4);
    ctx.fillStyle = e.hp === 2 ? '#0f0' : '#ff0'; ctx.fillRect(ex, e.y - 8, e.w * (e.hp / e.maxHp), 4);
  }
}

function drawBomb(b) {
  if (!b.alive) return;
  const bx = b.x - cameraX;
  const blink = b.armed && Math.floor(b.fuse / 10) % 2 === 0;
  ctx.fillStyle = blink ? '#ff0000' : '#222';
  ctx.beginPath(); ctx.arc(bx + b.w/2, b.y + b.h/2, b.w/2, 0, Math.PI * 2); ctx.fill();
  ctx.strokeStyle = '#000'; ctx.lineWidth = 2; ctx.stroke();
  // Фитиль
  ctx.strokeStyle = '#ff6600'; ctx.lineWidth = 2;
  ctx.beginPath(); ctx.moveTo(bx + b.w/2, b.y); ctx.lineTo(bx + b.w/2 + 5, b.y - 8); ctx.stroke();
  if (Math.floor(Date.now() / 100) % 2 === 0) {
    ctx.fillStyle = '#ffdd00'; ctx.beginPath(); ctx.arc(bx + b.w/2 + 5, b.y - 8, 4, 0, Math.PI * 2); ctx.fill();
  }
  if (b.armed) {
    ctx.fillStyle = '#fff'; ctx.font = 'bold 12px Arial'; ctx.textAlign = 'center';
    ctx.fillText(Math.ceil(b.fuse / 60), bx + b.w/2, b.y + b.h/2 + 4); ctx.textAlign = 'left';
  }
}

function drawBoss() {
  if (!boss || !boss.alive) return;
  const bx = boss.x - cameraX;
  const flash = boss.hitTimer > 0 && Math.floor(boss.hitTimer / 3) % 2 === 0;

  // Тело
  ctx.fillStyle = flash ? '#fff' : '#8e44ad';
  ctx.fillRect(bx, boss.y, boss.w, boss.h);
  ctx.strokeStyle = '#5b2c6f'; ctx.lineWidth = 4; ctx.strokeRect(bx, boss.y, boss.w, boss.h);

  // Красные глаза
  ctx.fillStyle = '#ff0066';
  ctx.beginPath(); ctx.arc(bx + 25, boss.y + 35, 12, 0, Math.PI * 2); ctx.arc(bx + 65, boss.y + 35, 12, 0, Math.PI * 2); ctx.fill();
  ctx.fillStyle = '#000';
  ctx.beginPath(); ctx.arc(bx + 25, boss.y + 35, 5, 0, Math.PI * 2); ctx.arc(bx + 65, boss.y + 35, 5, 0, Math.PI * 2); ctx.fill();

  // Круглые руки
  ctx.fillStyle = '#8e44ad';
  ctx.beginPath(); ctx.arc(bx - 10, boss.y + boss.h/2, 20, 0, Math.PI * 2); ctx.fill();
  ctx.strokeStyle = '#5b2c6f'; ctx.lineWidth = 3; ctx.stroke();
  ctx.beginPath(); ctx.arc(bx + boss.w + 10, boss.y + boss.h/2, 20, 0, Math.PI * 2); ctx.fill();
  ctx.stroke();

  // HP
  ctx.fillStyle = '#000'; ctx.fillRect(bx, boss.y - 15, boss.w, 8);
  ctx.fillStyle = '#ff0066'; ctx.fillRect(bx, boss.y - 15, boss.w * (boss.hp / boss.maxHp), 8);
}

function draw() {
  const grad = ctx.createLinearGradient(0, 0, 0, canvas.height);
  grad.addColorStop(0, '#87CEEB'); grad.addColorStop(1, '#b8e0f0');
  ctx.fillStyle = grad; ctx.fillRect(0, 0, canvas.width, canvas.height);

  // Облака
  ctx.fillStyle = 'rgba(255,255,255,0.7)';
  for (let i = 0; i < 8; i++) {
    const cx = (i * 200 - cameraX * 0.3) % 1200;
    ctx.beginPath(); ctx.arc(cx, 50 + (i % 3) * 30, 25, 0, Math.PI * 2); ctx.arc(cx + 25, 50 + (i % 3) * 30, 20, 0, Math.PI * 2); ctx.fill();
  }

  // Платформы
  platforms.forEach(p => {
    ctx.fillStyle = '#8B4513'; ctx.fillRect(p.x - cameraX, p.y, p.w, p.h);
    ctx.fillStyle = '#4CAF50'; ctx.fillRect(p.x - cameraX, p.y, p.w, 6);
  });

  // Монеты
  coins.forEach(c => {
    if (c.collected) return;
    const bob = Math.sin(Date.now() / 200 + c.x) * 3;
    ctx.fillStyle = '#ffd700';
    ctx.beginPath(); ctx.arc(c.x - cameraX, c.y + bob, c.r, 0, Math.PI * 2); ctx.fill();
    ctx.strokeStyle = '#daa520'; ctx.lineWidth = 2; ctx.stroke();
  });

  enemies.forEach(e => drawEnemy(e));
  bombs.forEach(b => drawBomb(b));
  drawBoss();

  particles.forEach(p => {
    ctx.globalAlpha = p.life / p.maxLife;
    ctx.fillStyle = p.color;
    ctx.fillRect(p.x - cameraX, p.y, p.size, p.size);
    ctx.globalAlpha = 1;
  });

  if (!gameOver) drawPlayer();

  if (gameOver) {
    ctx.fillStyle = 'rgba(0,0,0,0.7)'; ctx.fillRect(0, 0, canvas.width, canvas.height);
    ctx.fillStyle = '#e94560'; ctx.font = 'bold 30px Arial'; ctx.textAlign = 'center';
    ctx.fillText('ИГРА ОКОНЧЕНА', canvas.width/2, canvas.height/2);
    ctx.fillStyle = '#fff'; ctx.font = '18px Arial';
    ctx.fillText('Убито: ' + score, canvas.width/2, canvas.height/2 + 40);
  }

  if (win) {
    ctx.fillStyle = 'rgba(0,0,0,0.7)'; ctx.fillRect(0, 0, canvas.width, canvas.height);
    ctx.fillStyle = '#ffd700'; ctx.font = 'bold 30px Arial'; ctx.textAlign = 'center';
    ctx.fillText('🎉 БОСС ПОБЕЖДЁН!', canvas.width/2, canvas.height/2);
    ctx.fillStyle = '#fff'; ctx.font = '18px Arial';
    ctx.fillText('Ты прошёл игру!', canvas.width/2, canvas.height/2 + 40);
  }
}

function update() {
  if (gameOver || win) return;
  if (invincibleTimer > 0) invincibleTimer--;

  const left = keys['ArrowLeft'] || keys['a'];
  const right = keys['ArrowRight'] || keys['d'];
  const control = player.onGround ? ACCEL : ACCEL * 0.6;

  if (left && !right) { player.vx -= control; player.facing = -1; }
  else if (right && !left) { player.vx += control; player.facing = 1; }
  else if (player.onGround) player.vx *= FRICTION;

  if (player.vx > MAX_SPEED) player.vx = MAX_SPEED;
  if (player.vx < -MAX_SPEED) player.vx = -MAX_SPEED;
  if (Math.abs(player.vx) < 0.1) player.vx = 0;

  player.rotation += player.vx * 0.05;
  player.vy += GRAVITY;
  if (player.vy > MAX_FALL) player.vy = MAX_FALL;

  // X
  player.x += player.vx;
  platforms.forEach(p => {
    if (player.x + player.r > p.x && player.x - player.r < p.x + p.w &&
        player.y + player.r > p.y && player.y - player.r < p.y + p.h) {
      if (player.vx > 0) player.x = p.x - player.r;
      else if (player.vx < 0) player.x = p.x + p.w + player.r;
      player.vx = 0;
    }
  });

  // Y
  player.y += player.vy;
  player.onGround = false;
  platforms.forEach(p => {
    if (player.x + player.r > p.x && player.x - player.r < p.x + p.w &&
        player.y + player.r > p.y && player.y - player.r < p.y + p.h) {
      if (player.vy > 0) { player.y = p.y - player.r; player.vy = 0; player.onGround = true; }
      else if (player.vy < 0) { player.y = p.y + p.h + player.r; player.vy = 0; }
    }
  });

  // Монеты
  coins.forEach(c => {
    if (c.collected) return;
    const dx = player.x - c.x, dy = player.y - c.y;
    if (Math.sqrt(dx*dx + dy*dy) < player.r + c.r + 5) {
      c.collected = true; score++; updateHUD();
    }
  });

  // Враги
  enemies.forEach(e => {
    if (!e.alive) return;
    e.x += e.vx;
    if (e.x < e.minX) { e.x = e.minX; e.vx = Math.abs(e.vx); }
    if (e.x > e.maxX) { e.x = e.maxX; e.vx = -Math.abs(e.vx); }

    if (player.x + player.r > e.x && player.x - player.r < e.x + e.w &&
        player.y + player.r > e.y && player.y - player.r < e.y + e.h) {
      // Прыжок сверху
      if (player.vy > 0 && player.y - player.r < e.y + 10) {
        e.hp--;
        player.vy = -10;
        if (e.hp <= 0) {
          e.alive = false;
          sfxKill();
          spawnParticles(e.x + e.w/2, e.y + e.h/2, e.color, 10);
          score += (e.type === 'gold' ? 3 : 1);
          updateHUD();
        } else {
          sfxBossHit();
        }
      } else if (invincibleTimer === 0) damagePlayer(e.x);
    }
  });

  // Бомбы
  bombs.forEach(b => {
    if (!b.alive) return;
    // Если игрок рядом — взводится
    if (!b.armed && Math.abs(player.x - b.x) < 80 && Math.abs(player.y - b.y) < 80) {
      b.armed = true;
      b.fuse = 120;
    }
    if (b.armed) {
      b.fuse--;
      if (b.fuse <= 0) {
        b.alive = false;
        sfxBoom();
        spawnParticles(b.x + b.w/2, b.y + b.h/2, '#ff4400', 20);
        // Урон по игроку
        if (Math.abs(player.x - b.x) < 100 && Math.abs(player.y - b.y) < 100 && invincibleTimer === 0) {
          damagePlayer(b.x);
        }
      }
    }
  });

  // Босс
  if (boss && boss.alive) {
    if (boss.hitTimer > 0) boss.hitTimer--;
    boss.x += boss.vx;
    if (boss.x < boss.minX) { boss.x = boss.minX; boss.vx = Math.abs(boss.vx); }
    if (boss.x > boss.maxX) { boss.x = boss.maxX; boss.vx = -Math.abs(boss.vx); }

    if (player.x + player.r > boss.x && player.x - player.r < boss.x + boss.w &&
        player.y + player.r > boss.y && player.y - player.r < boss.y + boss.h) {
      if (player.vy > 0 && player.y - player.r < boss.y + 15) {
        boss.hp--;
        boss.hitTimer = 20;
        player.vy = -12;
        sfxBossHit();
        spawnParticles(boss.x + boss.w/2, boss.y, '#8e44ad', 10);
        if (boss.hp <= 0) {
          boss.alive = false;
          sfxWin();
          win = true;
        }
        updateHUD();
      } else if (invincibleTimer === 0) damagePlayer(boss.x);
    }
  }

  cameraX = player.x - 200;
  if (cameraX < 0) cameraX = 0;

  particles.forEach((p, i) => {
    p.x += p.vx; p.y += p.vy; p.vy += 0.2; p.life--;
    if (p.life <= 0) particles.splice(i, 1);
  });

  if (player.y > 500) damagePlayer(player.x);
}

function loop() { update(); draw(); requestAnimationFrame(loop); }

// Управление
document.addEventListener('keydown', e => {
  keys[e.key] = true;
  if (e.key === ' ' || e.key === 'ArrowUp' || e.key === 'w') {
    if (player.onGround) { player.vy = JUMP_FORCE; player.onGround = false; sfxJump(); }
    e.preventDefault();
  }
});
document.addEventListener('keyup', e => { keys[e.key] = false; });

function holdBtn(id, key) {
  const el = document.getElementById(id);
  el.addEventListener('touchstart', e => { e.preventDefault(); keys[key] = true; initAudio(); });
  el.addEventListener('touchend', e => { e.preventDefault(); keys[key] = false; });
  el.addEventListener('mousedown', e => { e.preventDefault(); keys[key] = true; initAudio(); });
  el.addEventListener('mouseup', e => { e.preventDefault(); keys[key] = false; });
}
holdBtn('left', 'ArrowLeft');
holdBtn('right', 'ArrowRight');

function doJump() { initAudio(); if (player.onGround) { player.vy = JUMP_FORCE; player.onGround = false; sfxJump(); } }
document.getElementById('jump').addEventListener('touchstart', e => { e.preventDefault(); doJump(); });
document.getElementById('jump').addEventListener('mousedown', e => { e.preventDefault(); doJump(); });

document.addEventListener('touchstart', initAudio, { once: true });
document.addEventListener('mousedown', initAudio, { once: true });

initLevel(3); // Сразу 3 уровень с боссом
loop();
</script>
</body>
</html>

Game Source: Красный Шарик: Квадраты и Босс

Creator: TurboGamer88

Libraries: none

Complexity: complex (454 lines, 17.2 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: game-turbogamer88-mvtky381" to link back to the original. Then publish at arcadelab.ai/publish.