🎮ArcadeLab

Песочница: Убей Персонажей

by HyperRider36
450 lines16.6 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 #ff8c00; border-radius: 8px; touch-action: none; max-width: 100vw; max-height: 80vh; }
  #hud { font-size: 13px; margin-bottom: 3px; color: #ff8c00; font-weight: bold; text-align: center; }
  #toolbar { position: absolute; bottom: 10px; left: 0; width: 100%; display: flex; justify-content: center; gap: 6px; pointer-events: none; flex-wrap: wrap; padding: 0 5px; box-sizing: border-box; }
  .tool { width: 45px; height: 45px; border-radius: 10px; background: rgba(255,140,0,0.2); color: #fff; font-size: 18px; border: 2px solid #ff8c00; display: flex; align-items: center; justify-content: center; user-select: none; pointer-events: auto; touch-action: none; }
  .tool.active { background: rgba(255,140,0,0.6); box-shadow: 0 0 10px #ff8c00; }
  #clearBtn { background: rgba(255,0,102,0.3); border-color: #ff0066; }
</style>
</head>
<body>
<div id="hud">🎮 Песочница | Объектов: 0 | Убито: 0</div>
<canvas id="game" width="700" height="400"></canvas>
<div id="toolbar">
  <div class="tool active" data-tool="melon">🍉</div>
  <div class="tool" data-tool="ball">⚽</div>
  <div class="tool" data-tool="box">📦</div>
  <div class="tool" data-tool="trap">🪤</div>
  <div class="tool" data-tool="spike">🔺</div>
  <div class="tool" data-tool="bomb">💣</div>
  <div class="tool" data-tool="cannon">💥</div>
  <div class="tool" id="clearBtn">🗑</div>
</div>

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

let objects = [];
let particles = [];
let selectedTool = 'melon';
let dragged = null;
let dragOffsetX = 0, dragOffsetY = 0;
let mouseX = 0, mouseY = 0;
let killedCount = 0;

const groundY = 360;
const GRAVITY = 0.5;
const FRICTION = 0.98;
const BOUNCE = 0.3;

// Звук
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 || 'sine'; o.frequency.value = f;
    g.gain.setValueAtTime(v || 0.06, 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 sfxSpawn() { playSound(600, 0.08, 'sine', 0.05); }
function sfxThud() { playSound(80, 0.1, 'sawtooth', 0.08); }
function sfxBoom() { playSound(60, 0.3, 'sawtooth', 0.15); setTimeout(() => playSound(40, 0.2, 'sawtooth', 0.1), 80); }
function sfxKill() { playSound(400, 0.1, 'square', 0.08); setTimeout(() => playSound(250, 0.12, 'square', 0.06), 50); }

// === СОЗДАНИЕ ОБЪЕКТОВ ===
function createMelon(x, y) {
  return { type: 'melon', alive: true, x, y, w: 20, h: 28, vx: 0, vy: 0, angle: 0, va: 0, hp: 3, hitTimer: 0, color: '#4CAF50' };
}
function createBall(x, y) {
  return { type: 'ball', alive: true, x, y, r: 14, vx: 0, vy: 0, angle: 0, va: 0 };
}
function createBox(x, y) {
  return { type: 'box', alive: true, x, y, w: 40, h: 40, vx: 0, vy: 0, angle: 0, va: 0 };
}
function createTrap(x, y) {
  return { type: 'trap', alive: true, x, y, w: 40, h: 20, vx: 0, vy: 0, angle: 0, va: 0, armed: true, triggered: false };
}
function createSpike(x, y) {
  return { type: 'spike', alive: true, x, y, w: 30, h: 30, vx: 0, vy: 0, angle: 0, va: 0 };
}
function createBomb(x, y) {
  return { type: 'bomb', alive: true, x, y, w: 24, h: 24, vx: 0, vy: 0, angle: 0, va: 0, fuse: 120, armed: false };
}
function createCannon(x, y) {
  return { type: 'cannon', alive: true, x, y, w: 40, h: 24, vx: 0, vy: 0, angle: 0, va: 0, cooldown: 0, fallen: false };
}

function spawnAt(x, y) {
  initAudio();
  if (y > groundY - 30) y = groundY - 30;
  if (x < 40) x = 40;
  if (x > canvas.width - 40) x = canvas.width - 40;
  if (selectedTool === 'melon') objects.push(createMelon(x - 10, y - 28));
  else if (selectedTool === 'ball') objects.push(createBall(x, y - 14));
  else if (selectedTool === 'box') objects.push(createBox(x - 20, y - 40));
  else if (selectedTool === 'trap') objects.push(createTrap(x - 20, y - 10));
  else if (selectedTool === 'spike') objects.push(createSpike(x - 15, y - 15));
  else if (selectedTool === 'bomb') objects.push(createBomb(x - 12, y - 24));
  else if (selectedTool === 'cannon') objects.push(createCannon(x - 20, y - 12));
  sfxSpawn();
  updateHUD();
}

function updateHUD() {
  const melons = objects.filter(o => o.type === 'melon' && o.alive).length;
  hud.textContent = `🎮 Песочница | Объектов: ${objects.length} | Мелотов: ${melons} | Убито: ${killedCount}`;
}

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 - 2, life: 20 + Math.random() * 15, maxLife: 35, color, size: 2 + Math.random() * 4 });
  }
}

// === ФИЗИКА ===
function updatePhysics() {
  objects.forEach(obj => {
    if (!obj.alive) return;
    if (dragged === obj) return;

    if (obj.type === 'melon' || obj.type === 'ball' || obj.type === 'box' || obj.type === 'trap' || obj.type === 'spike' || obj.type === 'bomb' || obj.type === 'cannon') {
      obj.vy += GRAVITY;
      obj.vx *= FRICTION;
      obj.x += obj.vx;
      obj.y += obj.vy;
      obj.angle += obj.va;
      obj.va *= 0.98;

      const h = obj.h || (obj.r * 2);
      const w = obj.w || (obj.r * 2);
      if (obj.y + h > groundY) {
        obj.y = groundY - h;
        obj.vy = -obj.vy * BOUNCE;
        if (Math.abs(obj.vy) > 1.5) sfxThud();
      }
      if (obj.x < 0) { obj.x = 0; obj.vx = -obj.vx * BOUNCE; }
      if (obj.x + w > canvas.width) { obj.x = canvas.width - w; obj.vx = -obj.vx * BOUNCE; }

      if (obj.type === 'bomb') {
        if (!obj.armed && (Math.abs(obj.x - mouseX) < 100 || Math.abs(obj.vy) > 3)) { obj.armed = true; obj.fuse = 120; }
        if (obj.armed) {
          obj.fuse--;
          if (obj.fuse <= 0) {
            obj.alive = false;
            sfxBoom();
            spawnParticles(obj.x + obj.w/2, obj.y + obj.h/2, '#ff4400', 20);
            objects.forEach(o => {
              if (o.type === 'melon' && o.alive) {
                const dx = o.x + 10 - (obj.x + obj.w/2);
                const dy = o.y + 14 - (obj.y + obj.h/2);
                const dist = Math.sqrt(dx*dx + dy*dy);
                if (dist < 120) {
                  const force = (120 - dist) / 120 * 10;
                  o.vx += (dx / (dist || 1)) * force;
                  o.vy += (dy / (dist || 1)) * force - 3;
                  o.hp -= 2;
                  o.hitTimer = 30;
                  if (o.hp <= 0) { o.alive = false; killedCount++; sfxKill(); spawnParticles(o.x + 10, o.y + 14, '#4CAF50', 10); }
                }
              }
            });
            updateHUD();
          }
        }
      }

      if (obj.type === 'cannon' && obj.fallen) {
        if (obj.cooldown > 0) obj.cooldown--;
        if (obj.cooldown === 0) {
          let target = null, minDist = Infinity;
          objects.forEach(o => {
            if (o.type === 'melon' && o.alive) {
              const d = Math.hypot(o.x - obj.x, o.y - obj.y);
              if (d < minDist) { minDist = d; target = o; }
            }
          });
          if (target) {
            const dx = target.x - obj.x;
            const dy = target.y - obj.y;
            const len = Math.hypot(dx, dy);
            objects.push({ type: 'bullet', alive: true, x: obj.x + obj.w/2, y: obj.y + obj.h/2, vx: dx/len*8, vy: dy/len*8, r: 5, life: 120 });
            obj.cooldown = 90;
            sfxSpawn();
          } else obj.cooldown = 60;
        }
      }
    }

    if (obj.type === 'bullet') {
      obj.x += obj.vx; obj.y += obj.vy; obj.life--;
      if (obj.life <= 0 || obj.y > groundY) { obj.alive = false; return; }
      objects.forEach(o => {
        if (o.type === 'melon' && o.alive && o !== obj) {
          if (obj.x > o.x - 10 && obj.x < o.x + 20 && obj.y > o.y - 5 && obj.y < o.y + 33) {
            o.hp--; o.hitTimer = 20;
            o.vx += obj.vx * 0.3; o.vy -= 2;
            obj.alive = false;
            if (o.hp <= 0) { o.alive = false; killedCount++; sfxKill(); spawnParticles(o.x + 10, o.y + 14, '#4CAF50', 10); }
            updateHUD();
          }
        }
      });
    }
  });

  objects = objects.filter(o => o.alive || o.type === 'melon');
}

// === РИСОВАНИЕ ===
function drawGround() {
  ctx.fillStyle = '#2d1f0d';
  ctx.fillRect(0, groundY, canvas.width, canvas.height - groundY);
  ctx.fillStyle = '#4CAF50';
  ctx.fillRect(0, groundY, canvas.width, 6);
}

function drawMelon(o) {
  const hurt = o.hitTimer > 0;
  ctx.save();
  ctx.translate(o.x + o.w/2, o.y + o.h/2);
  ctx.rotate(o.angle);
  ctx.fillStyle = hurt ? '#ff4444' : '#4CAF50';
  ctx.beginPath(); ctx.ellipse(0, 0, o.w/2, o.h/2, 0, 0, Math.PI * 2); ctx.fill();
  ctx.strokeStyle = '#2E7D32'; ctx.lineWidth = 2; ctx.stroke();
  if (!hurt) {
    ctx.strokeStyle = '#1B5E20'; ctx.lineWidth = 1;
    for (let i = -2; i <= 2; i++) { ctx.beginPath(); ctx.moveTo(i * 4, -o.h/2 + 3); ctx.lineTo(i * 4, o.h/2 - 3); ctx.stroke(); }
  }
  ctx.fillStyle = '#fff';
  ctx.beginPath(); ctx.arc(-5, -4, 3, 0, Math.PI * 2); ctx.arc(5, -4, 3, 0, Math.PI * 2); ctx.fill();
  ctx.fillStyle = '#000';
  ctx.beginPath(); ctx.arc(-4, -4, 1.5, 0, Math.PI * 2); ctx.arc(6, -4, 1.5, 0, Math.PI * 2); ctx.fill();
  ctx.restore();
}

function drawBall(o) {
  ctx.save();
  ctx.translate(o.x, o.y);
  ctx.rotate(o.angle);
  ctx.fillStyle = '#fff';
  ctx.beginPath(); ctx.arc(0, 0, o.r, 0, Math.PI * 2); ctx.fill();
  ctx.strokeStyle = '#000'; ctx.lineWidth = 2; ctx.stroke();
  ctx.fillStyle = '#000';
  ctx.beginPath(); ctx.arc(-4, -4, 3, 0, Math.PI * 2); ctx.arc(4, -4, 3, 0, Math.PI * 2); ctx.arc(0, 4, 3, 0, Math.PI * 2); ctx.fill();
  ctx.restore();
}

function drawBox(o) {
  ctx.save(); ctx.translate(o.x + o.w/2, o.y + o.h/2); ctx.rotate(o.angle);
  ctx.fillStyle = '#d4a017'; ctx.fillRect(-o.w/2, -o.h/2, o.w, o.h);
  ctx.strokeStyle = '#8B4513'; ctx.lineWidth = 3; ctx.strokeRect(-o.w/2, -o.h/2, o.w, o.h);
  ctx.strokeStyle = '#8B4513'; ctx.lineWidth = 2;
  ctx.beginPath(); ctx.moveTo(-o.w/2, -o.h/2); ctx.lineTo(o.w/2, o.h/2); ctx.stroke();
  ctx.beginPath(); ctx.moveTo(o.w/2, -o.h/2); ctx.lineTo(-o.w/2, o.h/2); ctx.stroke();
  ctx.restore();
}

function drawTrap(o) {
  ctx.save(); ctx.translate(o.x + o.w/2, o.y + o.h/2); ctx.rotate(o.angle);
  ctx.fillStyle = '#555';
  ctx.fillRect(-o.w/2, -o.h/2, o.w, o.h);
  ctx.strokeStyle = '#333'; ctx.lineWidth = 2; ctx.strokeRect(-o.w/2, -o.h/2, o.w, o.h);
  ctx.fillStyle = '#aaa';
  for (let i = -2; i <= 2; i++) {
    ctx.beginPath();
    ctx.moveTo(i * 8, -o.h/2);
    ctx.lineTo(i * 8 + 3, -o.h/2 - 6);
    ctx.lineTo(i * 8 + 6, -o.h/2);
    ctx.fill();
  }
  ctx.restore();
}

function drawSpike(o) {
  ctx.save(); ctx.translate(o.x + o.w/2, o.y + o.h/2); ctx.rotate(o.angle);
  ctx.fillStyle = '#e94560';
  ctx.beginPath();
  ctx.moveTo(0, -o.h/2);
  ctx.lineTo(o.w/2, o.h/2);
  ctx.lineTo(-o.w/2, o.h/2);
  ctx.closePath(); ctx.fill();
  ctx.strokeStyle = '#aa0044'; ctx.lineWidth = 2; ctx.stroke();
  ctx.restore();
}

function drawBomb(o) {
  ctx.save(); ctx.translate(o.x + o.w/2, o.y + o.h/2); ctx.rotate(o.angle);
  const blink = o.armed && Math.floor(o.fuse / 10) % 2 === 0;
  ctx.fillStyle = blink ? '#ff0000' : '#222';
  ctx.beginPath(); ctx.arc(0, 0, o.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(0, -o.w/2); ctx.lineTo(5, -o.w/2 - 8); ctx.stroke();
  if (Math.floor(Date.now() / 100) % 2 === 0) {
    ctx.fillStyle = '#ffdd00'; ctx.beginPath(); ctx.arc(5, -o.w/2 - 8, 3, 0, Math.PI * 2); ctx.fill();
  }
  if (o.armed) {
    ctx.fillStyle = '#fff'; ctx.font = 'bold 10px Arial'; ctx.textAlign = 'center';
    ctx.fillText(Math.ceil(o.fuse / 60), 0, 3); ctx.textAlign = 'left';
  }
  ctx.restore();
}

function drawCannon(o) {
  ctx.save(); ctx.translate(o.x + o.w/2, o.y + o.h/2); ctx.rotate(o.angle);
  ctx.fillStyle = '#555';
  ctx.fillRect(-o.w/2, 0, o.w, o.h/2);
  ctx.fillStyle = '#333';
  ctx.fillRect(-o.w/2 + 5, -o.h/2, o.w - 10, o.h/2);
  ctx.strokeStyle = '#000'; ctx.lineWidth = 2;
  ctx.strokeRect(-o.w/2 + 5, -o.h/2, o.w - 10, o.h/2);
  ctx.fillStyle = '#ff6600';
  ctx.beginPath(); ctx.arc(-o.w/2 + 5, 0, 4, 0, Math.PI * 2); ctx.fill();
  ctx.restore();
}

function drawBullet(o) {
  ctx.fillStyle = '#ffdd00';
  ctx.beginPath(); ctx.arc(o.x, o.y, 5, 0, Math.PI * 2); ctx.fill();
  ctx.fillStyle = 'rgba(255,221,0,0.3)';
  ctx.beginPath(); ctx.arc(o.x, o.y, 10, 0, Math.PI * 2); ctx.fill();
}

function draw() {
  const grad = ctx.createLinearGradient(0, 0, 0, canvas.height);
  grad.addColorStop(0, '#1a1a2e'); grad.addColorStop(1, '#2a2a4e');
  ctx.fillStyle = grad; ctx.fillRect(0, 0, canvas.width, canvas.height);
  drawGround();

  objects.forEach(o => {
    if (!o.alive && o.type !== 'melon') return;
    if (o.type === 'melon') drawMelon(o);
    else if (o.type === 'ball') drawBall(o);
    else if (o.type === 'box') drawBox(o);
    else if (o.type === 'trap') drawTrap(o);
    else if (o.type === 'spike') drawSpike(o);
    else if (o.type === 'bomb') drawBomb(o);
    else if (o.type === 'cannon') drawCannon(o);
    else if (o.type === 'bullet') drawBullet(o);
  });

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

  if (dragged) {
    ctx.strokeStyle = 'rgba(255,140,0,0.6)'; ctx.lineWidth = 2;
    ctx.setLineDash([5, 5]);
    ctx.strokeRect(mouseX - 30, mouseY - 30, 60, 60);
    ctx.setLineDash([]);
  }
}

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

// === УПРАВЛЕНИЕ ===
function getPos(e) {
  const rect = canvas.getBoundingClientRect();
  const t = e.touches ? e.touches[0] : e;
  return { x: (t.clientX - rect.left) * (canvas.width / rect.width), y: (t.clientY - rect.top) * (canvas.height / rect.height) };
}

function hitTest(x, y) {
  for (let i = objects.length - 1; i >= 0; i--) {
    const o = objects[i];
    if (!o.alive && o.type !== 'melon') continue;
    const w = o.w || o.r * 2;
    const h = o.h || o.r * 2;
    if (x > o.x - 5 && x < o.x + w + 5 && y > o.y - 5 && y < o.y + h + 5) return o;
  }
  return null;
}

function onStart(e) {
  e.preventDefault(); initAudio();
  const p = getPos(e); mouseX = p.x; mouseY = p.y;
  const hit = hitTest(p.x, p.y);
  if (hit) {
    dragged = hit;
    dragOffsetX = p.x - hit.x;
    dragOffsetY = p.y - hit.y;
    // Капкан срабатывает при клике
    if (hit.type === 'trap') {
      hit.triggered = true;
      sfxThud();
      objects.forEach(o => {
        if (o.type === 'melon' && o.alive && Math.abs(o.x - hit.x) < 50) {
          o.hp -= 2; o.hitTimer = 20;
          if (o.hp <= 0) { o.alive = false; killedCount++; sfxKill(); spawnParticles(o.x, o.y, '#4CAF50', 10); }
        }
      });
      updateHUD();
    }
  } else {
    spawnAt(p.x, p.y);
  }
}

function onMove(e) {
  e.preventDefault();
  if (!dragged) return;
  const p = getPos(e); mouseX = p.x; mouseY = p.y;
  dragged.x = p.x - dragOffsetX;
  dragged.y = p.y - dragOffsetY;
  dragged.vx = 0; dragged.vy = 0;
}

function onEnd(e) {
  e.preventDefault();
  if (dragged) {
    const p = getPos(e);
    dragged.vx = (p.x - mouseX) * 0.5;
    dragged.vy = (p.y - mouseY) * 0.5;
    dragged = null;
  }
}

canvas.addEventListener('touchstart', onStart, { passive: false });
canvas.addEventListener('touchmove', onMove, { passive: false });
canvas.addEventListener('touchend', onEnd, { passive: false });
canvas.addEventListener('mousedown', onStart);
canvas.addEventListener('mousemove', onMove);
canvas.addEventListener('mouseup', onEnd);

document.querySelectorAll('.tool[data-tool]').forEach(el => {
  el.addEventListener('click', () => {
    selectedTool = el.dataset.tool;
    document.querySelectorAll('.tool').forEach(t => t.classList.remove('active'));
    el.classList.add('active');
    sfxSpawn();
  });
});

document.getElementById('clearBtn').addEventListener('click', () => {
  objects = []; particles = []; dragged = null; killedCount = 0;
  updateHUD();
  sfxSpawn();
});

initAudio();
updateHUD();
loop();
</script>
</body>
</html>

Game Source: Песочница: Убей Персонажей

Creator: HyperRider36

Libraries: none

Complexity: complex (450 lines, 16.6 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-mvtm9a6i" to link back to the original. Then publish at arcadelab.ai/publish.