Загрузка данных
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Зомби Башня: Последний Рубеж</title>
<style>
* {
box-sizing: border-box;
user-select: none;
margin: 0;
padding: 0;
}
body, html {
width: 100%;
height: 100%;
overflow: hidden;
background-color: #111;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
#gameCanvas {
display: block;
background: #1e241e;
cursor: crosshair;
}
#uiOverlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 15px;
color: #fff;
text-shadow: 2px 2px 4px #000;
}
.stats {
font-size: 18px;
font-weight: bold;
}
#hpBarContainer {
width: 200px;
height: 20px;
background: #444;
border: 2px solid #fff;
border-radius: 10px;
overflow: hidden;
margin-top: 5px;
}
#hpBar {
width: 100%;
height: 100%;
background: #e74c3c;
transition: width 0.1s;
}
#upgradeModal {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.85);
display: none;
flex-direction: column;
justify-content: center;
align-items: center;
pointer-events: auto;
z-index: 10;
}
#upgradeModal h2 {
color: #f1c40f;
margin-bottom: 20px;
font-size: 28px;
}
.cards-container {
display: flex;
gap: 15px;
flex-wrap: wrap;
justify-content: center;
}
.card {
background: #2c3e50;
border: 2px solid #3498db;
border-radius: 8px;
padding: 15px;
width: 180px;
text-align: center;
cursor: pointer;
transition: transform 0.2s, background 0.2s;
}
.card:hover {
transform: scale(1.05);
background: #34495e;
}
.card h3 {
font-size: 16px;
color: #2ecc71;
margin-bottom: 8px;
}
.card p {
font-size: 13px;
color: #bdc3c7;
}
#gameOverScreen {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.9);
display: none;
flex-direction: column;
justify-content: center;
align-items: center;
color: #fff;
pointer-events: auto;
z-index: 20;
}
#gameOverScreen h1 {
color: #e74c3c;
font-size: 40px;
margin-bottom: 10px;
}
.btn {
background: #27ae60;
color: white;
border: none;
padding: 12px 24px;
font-size: 18px;
font-weight: bold;
border-radius: 5px;
cursor: pointer;
margin-top: 20px;
}
.btn:hover { background: #2ecc71; }
</style>
</head>
<body>
<canvas id="gameCanvas"></canvas>
<div id="uiOverlay">
<div class="stats">
<div>ВОЛНА: <span id="waveTxt">1</span></div>
<div>МОНЕТЫ: <span id="coinsTxt">0</span></div>
<div>БАШНЯ HP:</div>
<div id="hpBarContainer"><div id="hpBar"></div></div>
</div>
</div>
<div id="upgradeModal">
<h2>ВЫБЕРИТЕ УЛУЧШЕНИЕ</h2>
<div class="cards-container" id="cardsContainer"></div>
</div>
<div id="gameOverScreen">
<h1>ВЫ ПОГИБЛИ</h1>
<p>Вы продержались волн: <span id="finalWave">0</span></p>
<p>Заработано монет: <span id="finalCoins">0</span></p>
<button class="btn" onclick="restartGame()">ИГРАТЬ СНОВА</button>
</div>
<script>
// --- АУДИОСИСТЕМА (Web Audio API) ---
const AudioContext = window.AudioContext || window.webkitAudioContext;
let audioCtx = null;
function initAudio() {
if (!audioCtx) audioCtx = new AudioContext();
}
function playSound(type) {
if (!audioCtx) return;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain);
gain.connect(audioCtx.destination);
const now = audioCtx.currentTime;
if (type === 'shoot') {
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(300, now);
osc.frequency.exponentialRampToValueAtTime(0.01, now + 0.15);
gain.gain.setValueAtTime(0.3, now);
gain.gain.linearRampToValueAtTime(0.01, now + 0.15);
osc.start(now);
osc.stop(now + 0.15);
} else if (type === 'hit') {
osc.type = 'square';
osc.frequency.setValueAtTime(120, now);
osc.frequency.exponentialRampToValueAtTime(0.01, now + 0.08);
gain.gain.setValueAtTime(0.2, now);
gain.gain.linearRampToValueAtTime(0.01, now + 0.08);
osc.start(now);
osc.stop(now + 0.08);
} else if (type === 'hurt') {
osc.type = 'triangle';
osc.frequency.setValueAtTime(80, now);
osc.frequency.linearRampToValueAtTime(30, now + 0.2);
gain.gain.setValueAtTime(0.4, now);
gain.gain.linearRampToValueAtTime(0.01, now + 0.2);
osc.start(now);
osc.stop(now + 0.2);
} else if (type === 'upgrade') {
osc.type = 'sine';
osc.frequency.setValueAtTime(260, now);
osc.frequency.setValueAtTime(400, now + 0.1);
gain.gain.setValueAtTime(0.3, now);
gain.gain.linearRampToValueAtTime(0.01, now + 0.25);
osc.start(now);
osc.stop(now + 0.25);
}
}
// --- НАСТРОЙКИ ХОСТА И СЦЕНЫ ---
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
// --- ИГРОВЫЕ ПЕРЕМЕННЫЕ ---
let gameState = 'PLAYING'; // PLAYING, UPGRADE, GAMEOVER
let wave = 1;
let coins = 0;
let zombiesToSpawn = 0;
let spawnTimer = 0;
const tower = {
x: 0,
y: 0,
radius: 40,
maxHp: 100,
hp: 100,
damage: 25,
fireRate: 300, // мс между выстрелами
lastShot: 0,
multishot: 1,
pierce: 1
};
let bullets = [];
let zombies = [];
let particles = [];
let mousePos = { x: 0, y: 0 };
let isMouseDown = false;
// --- ИНИЦИАЛИЗАЦИЯ ---
function init() {
tower.x = canvas.width / 2;
tower.y = canvas.height / 2;
tower.hp = tower.maxHp;
tower.damage = 25;
tower.fireRate = 300;
tower.multishot = 1;
wave = 1;
coins = 0;
bullets = [];
zombies = [];
particles = [];
updateUI();
startWave();
}
function startWave() {
zombiesToSpawn = 10 + wave * 5;
if (wave % 5 === 0) zombiesToSpawn = 1; // Волна Босса
document.getElementById('waveTxt').innerText = wave;
}
// --- УПРАВЛЕНИЕ ---
window.addEventListener('mousemove', (e) => {
mousePos.x = e.clientX;
mousePos.y = e.clientY;
});
window.addEventListener('mousedown', (e) => {
initAudio();
isMouseDown = true;
mousePos.x = e.clientX;
mousePos.y = e.clientY;
});
window.addEventListener('mouseup', () => isMouseDown = false);
window.addEventListener('touchmove', (e) => {
if (e.touches.length > 0) {
mousePos.x = e.touches[0].clientX;
mousePos.y = e.touches[0].clientY;
}
});
window.addEventListener('touchstart', (e) => {
initAudio();
isMouseDown = true;
if (e.touches.length > 0) {
mousePos.x = e.touches[0].clientX;
mousePos.y = e.touches[0].clientY;
}
});
window.addEventListener('touchend', () => isMouseDown = false);
// --- СТРЕЛЬБА ---
function shoot() {
const now = Date.now();
if (now - tower.lastShot < tower.fireRate) return;
tower.lastShot = now;
playSound('shoot');
const baseAngle = Math.atan2(mousePos.y - tower.y, mousePos.x - tower.x);
const spread = 0.15;
for (let i = 0; i < tower.multishot; i++) {
let angle = baseAngle;
if (tower.multishot > 1) {
angle = baseAngle + (i - (tower.multishot - 1) / 2) * spread;
}
bullets.push({
x: tower.x,
y: tower.y,
vx: Math.cos(angle) * 10,
vy: Math.sin(angle) * 10,
damage: tower.damage,
pierce: tower.pierce
});
}
}
// --- СПАВН ЗОМБИ ---
function spawnZombie() {
let angle = Math.random() * Math.PI * 2;
let dist = Math.max(canvas.width, canvas.height) / 2 + 50;
let x = tower.x + Math.cos(angle) * dist;
let y = tower.y + Math.sin(angle) * dist;
let isBoss = (wave % 5 === 0);
// Типы: 0 - Обычный, 1 - Бегун, 2 - Танк
let type = 0;
if (!isBoss) {
let rand = Math.random();
if (rand > 0.7) type = 1;
else if (rand > 0.9) type = 2;
}
let z = {
x: x,
y: y,
type: type,
isBoss: isBoss,
radius: isBoss ? 35 : (type === 2 ? 22 : 14),
speed: isBoss ? 0.8 : (type === 1 ? 2.5 : (type === 2 ? 0.9 : 1.4)),
hp: isBoss ? (300 + wave * 150) : (type === 2 ? 80 : (type === 1 ? 20 : 35 + wave * 5)),
maxHp: 0,
color: isBoss ? '#8e44ad' : (type === 1 ? '#e67e22' : (type === 2 ? '#27ae60' : '#c0392b')),
reward: isBoss ? 50 : (type === 2 ? 5 : 1)
};
z.maxHp = z.hp;
zombies.push(z);
}
// --- ИГРОВОЙ ЦИКЛ ---
function update() {
if (gameState !== 'PLAYING') return;
if (isMouseDown) shoot();
// Генерация волн
spawnTimer++;
if (zombiesToSpawn > 0 && spawnTimer > 40) {
spawnZombie();
zombiesToSpawn--;
spawnTimer = 0;
}
// Движение пуль
for (let i = bullets.length - 1; i >= 0; i--) {
let b = bullets[i];
b.x += b.vx;
b.y += b.vy;
if (b.x < 0 || b.x > canvas.width || b.y < 0 || b.y > canvas.height) {
bullets.splice(i, 1);
continue;
}
// Попадание в зомби
for (let j = zombies.length - 1; j >= 0; j--) {
let z = zombies[j];
let dist = Math.hypot(b.x - z.x, b.y - z.y);
if (dist < z.radius + 4) {
z.hp -= b.damage;
playSound('hit');
// Частицы крови
createParticles(b.x, b.y, z.color);
b.pierce--;
if (b.pierce <= 0) {
bullets.splice(i, 1);
}
if (z.hp <= 0) {
coins += z.reward;
zombies.splice(j, 1);
updateUI();
}
break;
}
}
}
// Движение зомби
for (let i = zombies.length - 1; i >= 0; i--) {
let z = zombies[i];
let angle = Math.atan2(tower.y - z.y, tower.x - z.x);
z.x += Math.cos(angle) * z.speed;
z.y += Math.sin(angle) * z.speed;
// Зомби атакует башню
let distToTower = Math.hypot(tower.x - z.x, tower.y - z.y);
if (distToTower < tower.radius + z.radius) {
tower.hp -= (z.isBoss ? 2 : 0.5);
playSound('hurt');
createParticles(z.x, z.y, '#e74c3c');
updateUI();
if (!z.isBoss) {
z.hp -= 5;
if (z.hp <= 0) zombies.splice(i, 1);
}
if (tower.hp <= 0) {
gameOver();
}
}
}
// Анимация частиц
for (let i = particles.length - 1; i >= 0; i--) {
let p = particles[i];
p.x += p.vx;
p.y += p.vy;
p.life -= 0.05;
if (p.life <= 0) particles.splice(i, 1);
}
// Проверка окончания волны
if (zombiesToSpawn === 0 && zombies.length === 0) {
wave++;
gameState = 'UPGRADE';
showUpgradeModal();
}
}
function createParticles(x, y, color) {
for (let i = 0; i < 4; i++) {
particles.push({
x: x, y: y,
vx: (Math.random() - 0.5) * 4,
vy: (Math.random() - 0.5) * 4,
life: 1.0,
color: color
});
}
}
// --- ОТРИСОВКА (RENDER) ---
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Сетка на полу
ctx.strokeStyle = '#252e25';
ctx.lineWidth = 1;
let gridSize = 50;
for (let x = 0; x < canvas.width; x += gridSize) {
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, canvas.height); ctx.stroke();
}
for (let y = 0; y < canvas.height; y += gridSize) {
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(canvas.width, y); ctx.stroke();
}
// Отрисовка Башни
ctx.fillStyle = '#7f8c8d';
ctx.beginPath();
ctx.arc(tower.x, tower.y, tower.radius, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = '#2c3e50';
ctx.lineWidth = 6;
ctx.stroke();
// Оружие / Прицел героя
let angle = Math.atan2(mousePos.y - tower.y, mousePos.x - tower.x);
ctx.save();
ctx.translate(tower.x, tower.y);
ctx.rotate(angle);
ctx.fillStyle = '#34495e';
ctx.fillRect(0, -5, 25, 10);
ctx.restore();
// Отрисовка Пуль
ctx.fillStyle = '#f1c40f';
bullets.forEach(b => {
ctx.beginPath();
ctx.arc(b.x, b.y, 4, 0, Math.PI * 2);
ctx.fill();
});
// Отрисовка Зомби
zombies.forEach(z => {
ctx.fillStyle = z.color;
ctx.beginPath();
ctx.arc(z.x, z.y, z.radius, 0, Math.PI * 2);
ctx.fill();
// Полоска здоровья зомби (если ранен)
if (z.hp < z.maxHp) {
ctx.fillStyle = 'red';
ctx.fillRect(z.x - 15, z.y - z.radius - 8, 30, 4);
ctx.fillStyle = 'green';
ctx.fillRect(z.x - 15, z.y - z.radius - 8, (z.hp / z.maxHp) * 30, 4);
}
});
// Отрисовка частиц
particles.forEach(p => {
ctx.fillStyle = p.color;
ctx.globalAlpha = p.life;
ctx.fillRect(p.x, p.y, 3, 3);
ctx.globalAlpha = 1.0;
});
// Линия прицеливания
ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(tower.x, tower.y);
ctx.lineTo(mousePos.x, mousePos.y);
ctx.stroke();
}
// --- UI И УЛУЧШЕНИЯ ---
function updateUI() {
document.getElementById('coinsTxt').innerText = coins;
let hpPercent = Math.max(0, (tower.hp / tower.maxHp) * 100);
document.getElementById('hpBar').style.width = hpPercent + '%';
}
const allUpgrades = [
{ title: 'Урон +30%', desc: 'Увеличивает урон пуль', apply: () => tower.damage *= 1.3 },
{ title: 'Скорострельность', desc: 'Стрельба на 20% быстрее', apply: () => tower.fireRate *= 0.8 },
{ title: 'Дробовик', desc: '+1 дополнительная пуля за выстрел', apply: () => tower.multishot += 1 },
{ title: 'Ремонт Башни', desc: 'Восстанавливает 50% HP', apply: () => tower.hp = Math.min(tower.maxHp, tower.hp + tower.maxHp * 0.5) },
{ title: 'Пробивание', desc: 'Пули пробивают +1 зомби', apply: () => tower.pierce += 1 },
{ title: 'Броня Башни', desc: 'Увеличивает макс. HP на 40', apply: () => { tower.maxHp += 40; tower.hp += 40; } }
];
function showUpgradeModal() {
playSound('upgrade');
const container = document.getElementById('cardsContainer');
container.innerHTML = '';
// Перемешиваем и выбираем 3 карточки
let shuffled = [...allUpgrades].sort(() => 0.5 - Math.random()).slice(0, 3);
shuffled.forEach(upg => {
let card = document.createElement('div');
card.className = 'card';
card.innerHTML = `<h3>${upg.title}</h3><p>${upg.desc}</p>`;
card.onclick = () => {
upg.apply();
updateUI();
document.getElementById('upgradeModal').style.display = 'none';
gameState = 'PLAYING';
startWave();
};
container.appendChild(card);
});
document.getElementById('upgradeModal').style.display = 'flex';
}
function gameOver() {
gameState = 'GAMEOVER';
document.getElementById('finalWave').innerText = wave;
document.getElementById('finalCoins').innerText = coins;
document.getElementById('gameOverScreen').style.display = 'flex';
}
function restartGame() {
document.getElementById('gameOverScreen').style.display = 'none';
gameState = 'PLAYING';
init();
}
// --- ГЛАВНЫЙ ЦИКЛ ---
function loop() {
update();
draw();
requestAnimationFrame(loop);
}
// Запуск
init();
loop();
</script>
</body>
</html>