Песочница: Убей Персонажей
by HyperRider36450 lines16.6 KB
<!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.