🎮ArcadeLab

Робот-стрелок: Боты + Игрок

by HyperRider36
647 lines19.5 KB
▶ Play
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Робот-стрелок: Боты + Игрок</title>
<style>
  body {
    margin: 0;
    background: #0a0e1a;
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
    font-family: 'Courier New', monospace;
    color: #fff;
    overflow: hidden;
  }
  #game {
    position: relative;
    width: 900px; height: 600px;
    background: radial-gradient(circle at 50% 50%, #1a2238 0%, #0a0e1a 100%);
    border: 3px solid #2a3a5a;
    border-radius: 8px;
    overflow: hidden;
  }
  #game::before {
    content: "";
    position: absolute; inset: 0;
    background-image:
      linear-gradient(rgba(80,160,255,0.08) 1px, transparent 1px),
      linear-gradient(90deg, rgba(80,160,255,0.08) 1px, transparent 1px);
    background-size: 40px 40px;
    pointer-events: none;
  }

  /* Робот (общий стиль) */
  .robot {
    position: absolute;
    width: 46px; height: 46px;
    z-index: 50;
    transition: none;
  }
  .robot-body {
    position: absolute; inset: 0;
    border-radius: 8px;
    border: 3px solid;
    box-sizing: border-box;
  }
  .robot-eye {
    position: absolute;
    top: 12px;
    width: 12px; height: 12px;
    background: #fff;
    border-radius: 50%;
    box-shadow: 0 0 8px #fff;
  }
  .robot-eye.l { left: 8px; }
  .robot-eye.r { right: 8px; }
  .robot-eye::after {
    content: "";
    position: absolute;
    top: 3px; left: 3px;
    width: 6px; height: 6px;
    background: #0a0e1a;
    border-radius: 50%;
  }

  /* Игрок 1 — синий */
  .p1 .robot-body { background: linear-gradient(135deg,#4fc3f7,#0288d1); border-color:#01579b; }
  .p1 { filter: drop-shadow(0 0 10px #4fc3f7); }

  /* Игрок 2 — зелёный */
  .p2 .robot-body { background: linear-gradient(135deg,#81c784,#2e7d32); border-color:#1b5e20; }
  .p2 { filter: drop-shadow(0 0 10px #81c784); }
  .p2 .robot-eye::after { background: #0a0e1a; }

  /* Бот — красный */
  .bot .robot-body { background: linear-gradient(135deg,#ff6b6b,#b71c1c); border-color:#7f0000; }
  .bot { filter: drop-shadow(0 0 8px #ff4444); z-index: 40; }
  .bot .robot-eye { background: #ffeb3b; box-shadow: 0 0 6px #ffeb3b; }

  /* Полоска HP над роботом */
  .hpbar {
    position: absolute;
    top: -10px; left: 0;
    width: 46px; height: 5px;
    background: #333;
    border-radius: 3px;
    overflow: hidden;
  }
  .hpbar > div {
    height: 100%;
    background: #4caf50;
    transition: width 0.15s;
  }

  /* Пули */
  .bullet {
    position: absolute;
    width: 10px; height: 10px;
    border-radius: 50%;
    z-index: 30;
  }
  .bullet.bp1 { background:#4fc3f7; box-shadow: 0 0 12px #4fc3f7, 0 0 4px #fff; }
  .bullet.bp2 { background:#81c784; box-shadow: 0 0 12px #81c784, 0 0 4px #fff; }
  .bullet.bot { background:#ff5252; box-shadow: 0 0 12px #ff5252; }

  /* Взрыв */
  .boom {
    position: absolute;
    width: 60px; height: 60px;
    border-radius: 50%;
    background: radial-gradient(circle,#fff 0%,#ffeb3b 30%,#ff5252 60%,transparent 100%);
    pointer-events: none;
    z-index: 60;
    animation: boomAnim 0.4s ease-out forwards;
  }
  @keyframes boomAnim {
    0%   { transform: scale(0.2); opacity: 1; }
    100% { transform: scale(1.8); opacity: 0; }
  }

  /* HUD */
  .hud {
    position: absolute;
    top: 12px; left: 12px;
    display: flex; gap: 18px;
    font-size: 14px;
    z-index: 100;
    background: rgba(0,0,0,0.55);
    padding: 8px 16px;
    border-radius: 8px;
    border: 1px solid #2a3a5a;
  }
  .hud b { color: #4fc3f7; }
  .hud .p2stat b { color: #81c784; }

  /* Экран старта/конца */
  .overlay {
    position: absolute; inset: 0;
    background: rgba(0,0,0,0.88);
    display: none;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    z-index: 200;
    text-align: center;
    padding: 20px;
  }
  .overlay.show { display: flex; }
  .overlay h1 {
    font-size: 38px;
    margin: 0 0 14px;
    color: #4fc3f7;
    text-shadow: 0 0 20px #4fc3f7;
  }
  .overlay p { font-size: 16px; margin: 6px 0; color: #ccc; line-height: 1.7; }
  .overlay .btn-row { display: flex; gap: 14px; margin-top: 22px; flex-wrap: wrap; justify-content: center; }
  .overlay button {
    padding: 12px 28px;
    font-size: 16px;
    background: #4fc3f7;
    border: none;
    border-radius: 8px;
    cursor: pointer;
    color: #0a0e1a;
    font-weight: bold;
    font-family: inherit;
    transition: transform 0.1s, background 0.2s;
  }
  .overlay button:hover { background: #81d4fa; transform: scale(1.05); }
  .overlay button.green { background: #81c784; }
  .overlay button.green:hover { background: #a5d6a7; }
  .overlay .keys {
    display: flex; gap: 40px; margin-top: 20px; text-align: left;
    flex-wrap: wrap; justify-content: center;
  }
  .overlay .keys div {
    background: rgba(255,255,255,0.06);
    padding: 14px 22px;
    border-radius: 10px;
    border: 1px solid rgba(255,255,255,0.1);
  }
  .overlay .keys h3 { margin: 0 0 8px; font-size: 15px; }
  .overlay .keys kbd {
    background: #1a2238;
    border: 1px solid #2a3a5a;
    border-radius: 4px;
    padding: 2px 7px;
    font-size: 13px;
    color: #4fc3f7;
  }
</style>
</head>
<body>
<div id="game">
  <div class="hud">
    <span>❤️ P1: <b id="hp1">100</b></span>
    <span>☠️ Ботов убито: <b id="kills">0</b></span>
    <span>🌊 Волна: <b id="wave">1</b></span>
    <span class="p2stat" id="p2hud" style="display:none">💚 P2: <b id="hp2">100</b></span>
  </div>

  <div class="overlay show" id="overlay">
    <h1 id="ovTitle">🤖 РОБОТ-СТРЕЛОК</h1>
    <p id="ovText">Выбери режим:</p>
    <div class="btn-row">
      <button onclick="startGame('solo')">🎯 ОДИН ПРОТИВ БОТОВ</button>
      <button class="green" onclick="startGame('pvp')">👥 ДВА ИГРОКА (PvP)</button>
    </div>
    <div class="keys" id="keysInfo">
      <div>
        <h3 style="color:#4fc3f7">Игрок 1</h3>
        <kbd>W</kbd><kbd>A</kbd><kbd>S</kbd><kbd>D</kbd> — движение<br>
        <kbd>Пробел</kbd> / <kbd>ЛКМ</kbd> — стрельба
      </div>
    </div>
  </div>
</div>

<script>
  const game = document.getElementById('game');
  const overlay = document.getElementById('overlay');
  const ovTitle = document.getElementById('ovTitle');
  const ovText = document.getElementById('ovText');
  const hp1El = document.getElementById('hp1');
  const hp2El = document.getElementById('hp2');
  const p2hud = document.getElementById('p2hud');
  const killsEl = document.getElementById('kills');
  const waveEl = document.getElementById('wave');
  const keysInfo = document.getElementById('keysInfo');

  const W = 900, H = 600;
  const R_SIZE = 46;

  // ==== Игроки ====
  let p1 = null, p2 = null; // {el, x, y, hp, speed, alive, lastShot}
  let enemies = [];  // {el, x, y, hp, speed, lastShot, alive, hpbar}
  let bullets = [];  // {el, x, y, vx, vy, from, owner}
  let running = false;
  let mode = 'solo';
  let kills = 0, wave = 1, spawnTimer = 0;
  let lastTime = 0;
  let gameTime = 0;
  let mouse = { x: W/2, y: H/2 };
  let keys = {};

  // ==== Создание робота ====
  function makeRobot(cls, x, y) {
    const el = document.createElement('div');
    el.className = 'robot ' + cls;
    el.innerHTML = `
      <div class="robot-body"></div>
      <div class="robot-eye l"></div>
      <div class="robot-eye r"></div>
      <div class="hpbar"><div style="width:100%"></div></div>
    `;
    el.style.left = x + 'px';
    el.style.top = y + 'px';
    game.appendChild(el);
    return el;
  }

  function updateHpBar(robot) {
    const bar = robot.el.querySelector('.hpbar > div');
    if (bar) bar.style.width = Math.max(0, robot.hp) + '%';
  }

  // ==== Запуск ====
  function startGame(m) {
    mode = m;
    overlay.classList.remove('show');

    // очистка
    game.querySelectorAll('.robot, .bullet, .boom').forEach(e => e.remove());
    p1 = p2 = null;
    enemies = []; bullets = [];
    kills = 0; wave = 1; spawnTimer = 0; gameTime = 0;

    p1 = {
      el: makeRobot('p1', 150, H/2 - R_SIZE/2),
      x: 150, y: H/2 - R_SIZE/2,
      hp: 100, speed: 4.2, alive: true, lastShot: 0, facing: {x:1,y:0}
    };
    p1.el.style.left = p1.x + 'px';
    p1.el.style.top = p1.y + 'px';

    if (mode === 'pvp') {
      p2 = {
        el: makeRobot('p2', W - 150 - R_SIZE, H/2 - R_SIZE/2),
        x: W - 150 - R_SIZE, y: H/2 - R_SIZE/2,
        hp: 100, speed: 4.2, alive: true, lastShot: 0
      };
      p2.el.style.left = p2.x + 'px';
      p2.el.style.top = p2.y + 'px';
      p2hud.style.display = 'inline';
    } else {
      p2hud.style.display = 'none';
    }

    hp1El.textContent = 100;
    hp2El.textContent = 100;
    killsEl.textContent = 0;
    waveEl.textContent = 1;

    running = true;
    lastTime = performance.now();
    requestAnimationFrame(loop);
  }

  // ==== Управление ====
  document.addEventListener('keydown', e => {
    const k = e.key.toLowerCase();
    keys[k] = true;
    if (e.code === 'Space') { e.preventDefault(); shootHuman(p1, mouse.x, mouse.y); }
    if (k === 'enter') shootHuman(p2, null, null); // P2 стреляет вперёд
    // P2 стрельба — Ctrl или стрелка вниз? Лучше: стрелять автоматически по стрелке направления
  });
  document.addEventListener('keyup', e => { keys[e.key.toLowerCase()] = false; });

  game.addEventListener('mousemove', e => {
    const r = game.getBoundingClientRect();
    mouse.x = e.clientX - r.left;
    mouse.y = e.clientY - r.top;
  });
  game.addEventListener('mousedown', e => {
    if (!running) return;
    if (mode === 'solo' || (mode === 'pvp' && e.button === 0)) {
      shootHuman(p1, mouse.x, mouse.y);
    }
  });
  game.addEventListener('contextmenu', e => e.preventDefault());

  // ==== Стрельба ====
  function shootHuman(shooter, tx, ty) {
    if (!running || !shooter || !shooter.alive) return;
    const now = performance.now();
    if (now - shooter.lastShot < 140) return;
    shooter.lastShot = now;

    let dx, dy;
    const cx = shooter.x + R_SIZE/2;
    const cy = shooter.y + R_SIZE/2;

    if (shooter === p1) {
      dx = tx - cx; dy = ty - cy;
      const len = Math.hypot(dx, dy) || 1;
      dx /= len; dy /= len;
    } else {
      // P2 стреляет в направлении движения или вправо
      dx = 1; dy = 0;
      if (keys['arrowup'])    { dx = 0; dy = -1; }
      if (keys['arrowdown'])  { dx = 0; dy =  1; }
      if (keys['arrowleft'])  { dx = -1; dy = 0; }
      if (keys['arrowright']) { dx =  1; dy = 0; }
    }

    const speed = 11;
    spawnBullet(cx, cy, dx*speed, dy*speed,
                shooter === p1 ? 'bp1' : 'bp2',
                shooter === p1 ? 'p1' : 'p2');
  }

  function spawnBullet(x, y, vx, vy, cls, owner) {
    const el = document.createElement('div');
    el.className = 'bullet ' + cls;
    el.style.left = x + 'px';
    el.style.top = y + 'px';
    game.appendChild(el);
    bullets.push({ el, x, y, vx, vy, cls, owner, life: 0 });
  }

  // ==== Спавн ботов ====
  function spawnBot() {
    const side = Math.floor(Math.random() * 4);
    let x, y;
    const pad = 50;
    if (side === 0) { x = Math.random() * (W - R_SIZE); y = -pad; }
    else if (side === 1) { x = Math.random() * (W - R_SIZE); y = H + pad; }
    else if (side === 2) { x = -pad; y = Math.random() * (H - R_SIZE); }
    else { x = W + pad; y = Math.random() * (H - R_SIZE); }

    const el = makeRobot('bot', x, y);
    enemies.push({
      el, x, y, hp: 3, alive: true,
      speed: 1.3 + Math.random() * 0.7 + wave * 0.12,
      lastShot: performance.now() + 500 + Math.random() * 1000,
      baseCooldown: Math.max(400, 1300 - wave * 60)
    });
  }

  // ==== Взрыв ====
  function boom(x, y) {
    const b = document.createElement('div');
    b.className = 'boom';
    b.style.left = (x - 30) + 'px';
    b.style.top  = (y - 30) + 'px';
    game.appendChild(b);
    setTimeout(() => b.remove(), 400);
  }

  // ==== Цикл ====
  function loop(now) {
    if (!running) return;
    const dt = Math.min(50, now - lastTime);
    lastTime = now;
    gameTime += dt;

    updateHuman(p1, 'p1', dt);
    if (p2) updateHuman(p2, 'p2', dt);
    updateBots(dt, now);
    updateBullets(dt);
    updateSpawning(dt);

    // Проверка конца
    if ((!p1 || !p1.alive) && (!p2 || !p2.alive || mode === 'solo')) {
      endGame();
      return;
    }

    requestAnimationFrame(loop);
  }

  function updateHuman(h, id, dt) {
    if (!h.alive) return;
    let dx = 0, dy = 0;
    if (id === 'p1') {
      if (keys['w'] || keys['ц'])          dy -= 1;
      if (keys['s'] || keys['ы'])          dy += 1;
      if (keys['a'] || keys['ф'])          dx -= 1;
      if (keys['d'] || keys['в'])          dx += 1;
    } else {
      if (keys['arrowup'])    dy -= 1;
      if (keys['arrowdown'])  dy += 1;
      if (keys['arrowleft'])  dx -= 1;
      if (keys['arrowright']) dx += 1;
    }
    if (dx && dy) { dx *= 0.707; dy *= 0.707; }
    h.x += dx * h.speed * (dt/16.67);
    h.y += dy * h.speed * (dt/16.67);
    h.x = Math.max(0, Math.min(W - R_SIZE, h.x));
    h.y = Math.max(0, Math.min(H - R_SIZE, h.y));
    h.el.style.left = h.x + 'px';
    h.el.style.top  = h.y + 'px';
  }

  function updateBots(dt, now) {
    // ближайшая цель
    const targets = [];
    if (p1 && p1.alive) targets.push(p1);
    if (p2 && p2.alive) targets.push(p2);

    for (const e of enemies) {
      if (!e.alive) continue;
      if (targets.length === 0) continue;

      // ближайший живой игрок
      let target = targets[0];
      let bestD = Infinity;
      for (const t of targets) {
        const d = Math.hypot(t.x - e.x, t.y - e.y);
        if (d < bestD) { bestD = d; target = t; }
      }

      const cx = e.x + R_SIZE/2;
      const cy = e.y + R_SIZE/2;
      const tcx = target.x + R_SIZE/2;
      const tcy = target.y + R_SIZE/2;
      const ang = Math.atan2(tcy - cy, tcx - cx);

      // движение к цели
      const step = e.speed * (dt/16.67);
      e.x += Math.cos(ang) * step;
      e.y += Math.sin(ang) * step;
      e.x = Math.max(-20, Math.min(W - R_SIZE + 20, e.x));
      e.y = Math.max(-20, Math.min(H - R_SIZE + 20, e.y));
      e.el.style.left = e.x + 'px';
      e.el.style.top  = e.y + 'px';

      // стрельба
      if (now - e.lastShot > e.baseCooldown && bestD < 500) {
        e.lastShot = now;
        spawnBullet(cx, cy,
                    Math.cos(ang) * 6.5,
                    Math.sin(ang) * 6.5,
                    'bot', 'bot');
      }
    }
  }

  function updateBullets(dt) {
    for (let i = bullets.length - 1; i >= 0; i--) {
      const b = bullets[i];
      b.x += b.vx * (dt/16.67);
      b.y += b.vy * (dt/16.67);
      b.life += dt;
      b.el.style.left = b.x + 'px';
      b.el.style.top  = b.y + 'px';

      // за границей / старая пуля
      if (b.x < -20 || b.x > W + 20 || b.y < -20 || b.y > H + 20 || b.life > 3000) {
        b.el.remove(); bullets.splice(i, 1); continue;
      }

      // попадание
      let hit = false;

      if (b.owner === 'p1' || b.owner === 'p2') {
        // по ботам
        for (const e of enemies) {
          if (!e.alive) continue;
          if (b.x > e.x && b.x < e.x + R_SIZE &&
              b.y > e.y && b.y < e.y + R_SIZE) {
            e.hp -= 1;
            updateHpBar(e);
            hit = true;
            if (e.hp <= 0) {
              e.alive = false;
              boom(e.x + R_SIZE/2, e.y + R_SIZE/2);
              e.el.remove();
              kills++;
              killsEl.textContent = kills;
            }
            break;
          }
        }
        // PvP: пули P1 бьют P2 и наоборот
        if (!hit && mode === 'pvp') {
          const other = b.owner === 'p1' ? p2 : p1;
          if (other && other.alive &&
              b.x > other.x && b.x < other.x + R_SIZE &&
              b.y > other.y && b.y < other.y + R_SIZE) {
            other.hp -= 8;
            updateHpBar(other);
            updatePlayerHpUI();
            hit = true;
            if (other.hp <= 0) {
              other.alive = false;
              boom(other.x + R_SIZE/2, other.y + R_SIZE/2);
              other.el.style.opacity = '0.3';
            }
          }
        }
      } else if (b.owner === 'bot') {
        // по игрокам
        for (const pl of [p1, p2]) {
          if (!pl || !pl.alive) continue;
          if (b.x > pl.x && b.x < pl.x + R_SIZE &&
              b.y > pl.y && b.y < pl.y + R_SIZE) {
            pl.hp -= 7;
            updateHpBar(pl);
            updatePlayerHpUI();
            hit = true;
            if (pl.hp <= 0) {
              pl.alive = false;
              boom(pl.x + R_SIZE/2, pl.y + R_SIZE/2);
              pl.el.style.opacity = '0.3';
            }
            break;
          }
        }
      }

      if (hit) {
        b.el.remove();
        bullets.splice(i, 1);
      }
    }
  }

  function updatePlayerHpUI() {
    hp1El.textContent = Math.max(0, Math.round(p1 ? p1.hp : 0));
    if (p2) hp2El.textContent = Math.max(0, Math.round(p2.hp));
  }

  function updateSpawning(dt) {
    // волна растёт со временем
    const newWave = 1 + Math.floor(gameTime / 15000);
    if (newWave !== wave) {
      wave = newWave;
      waveEl.textContent = wave;
    }

    spawnTimer -= dt;
    const aliveBots = enemies.filter(e => e.alive).length;
    const maxBots = Math.min(2 + wave, 10);
    if (spawnTimer <= 0 && aliveBots < maxBots) {
      spawnBot();
      spawnTimer = Math.max(400, 1800 - wave * 120);
    }
  }

  function endGame() {
    running = false;

    // определяем результат
    let title = '', text = '';
    if (mode === 'solo') {
      title = '☠️ ТЫ ПАЛ В БОЮ';
      text = `Ботов убито: ${kills}<br>Волна: ${wave}`;
    } else {
      // PvP — кто выжил
      const a1 = p1 && p1.alive;
      const a2 = p2 && p2.alive;
      if (a1 && !a2)      title = '🏆 ПОБЕДИЛ ИГРОК 1 (синий)!';
      else if (!a1 && a2) title = '🏆 ПОБЕДИЛ ИГРОК 2 (зелёный)!';
      else                title = '💀 НИЧЬЯ — оба пали';
      text = `Ботов убито: ${kills}<br>Волна: ${wave}`;
    }

    ovTitle.innerHTML = title;
    ovText.innerHTML = text;

    // кнопки
    const btnRow = overlay.querySelector('.btn-row');
    btnRow.innerHTML = `
      <button onclick="startGame('solo')">🎯 ОДИН ПРОТИВ БОТОВ</button>
      <button class="green" onclick="startGame('pvp')">👥 ДВА ИГРОКА</button>
    `;

    // инфо о клавишах
    keysInfo.innerHTML = `
      <div>
        <h3 style="color:#4fc3f7">Игрок 1</h3>
        <kbd>W</kbd><kbd>A</kbd><kbd>S</kbd><kbd>D</kbd> — движение<br>
        <kbd>Пробел</kbd> / <kbd>ЛКМ</kbd> — стрельба в курсор
      </div>
      <div>
        <h3 style="color:#81c784">Игрок 2 (PvP)</h3>
        <kbd>↑</kbd><kbd>↓</kbd><kbd>←</kbd><kbd>→</kbd> — движение<br>
        <kbd>Enter</kbd> — стрельба в направлении движения
      </div>
    `;

    overlay.classList.add('show');
  }

  // Запускаем анимацию HUD раз в кадр (даже до старта)
  function idleLoop() {
    if (running) requestAnimationFrame(loop);
    else requestAnimationFrame(idleLoop);
  }
  // Запускаем цикл только когда игра стартует — но HUD обновим в loop
  requestAnimationFrame(idleLoop);
</script>
</body>
</html>

Game Source: Робот-стрелок: Боты + Игрок

Creator: HyperRider36

Libraries: none

Complexity: complex (647 lines, 19.5 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-hyperrider36-mvte2fpf" to link back to the original. Then publish at arcadelab.ai/publish.