Лабиринт играть онлайн
by WildFalcon362931 lines111.2 KB
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Лабиринт играть онлайн</title>
<style>
* {
box-sizing: border-box;
user-select: none;
}
body {
margin: 0;
min-height: 100vh;
background: linear-gradient(145deg, #1a2a3a, #0f1a24);
display: flex;
justify-content: center;
align-items: center;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.game-wrapper {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
justify-content: center;
gap: 20px;
}
.left-panel {
background: #1f3340;
border-radius: 20px;
padding: 20px 16px;
box-shadow: inset 0 0 0 2px #3a5a6e, 0 10px 15px rgba(0, 0, 0, 0.5);
display: flex;
flex-direction: column;
align-items: center;
min-width: 180px;
max-width: 220px;
margin-top: 10px;
text-align: center;
}
.pause-btn {
background: #4a6a7f;
border: none;
border-radius: 50%;
width: 64px;
height: 64px;
font-size: 32px;
color: #f0f9ff;
cursor: pointer;
box-shadow: 0 6px 0 #1f3340, 0 8px 12px rgba(0, 0, 0, 0.3);
transition: all 0.08s ease;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 16px;
touch-action: none;
}
.pause-btn:active {
transform: translateY(5px);
box-shadow: 0 1px 0 #1f3340, 0 5px 10px rgba(0, 0, 0, 0.3);
}
.pause-btn:hover {
background: #5c7f97;
}
.instructions {
color: #b8d4e8;
font-size: 0.9rem;
line-height: 1.6;
margin-bottom: 12px;
text-align: left;
width: 100%;
}
.instructions p {
margin: 4px 0;
}
.mode-task {
color: #f7d44a;
font-size: 1.0rem;
font-weight: 600;
background: #0f1a24;
border-radius: 12px;
padding: 10px 12px;
margin: 8px 0 12px 0;
width: 100%;
text-align: center;
border: 1px solid #3a5a6e;
box-shadow: inset 0 0 8px rgba(0,0,0,0.3);
}
.mode-task span {
display: block;
font-size: 0.85rem;
font-weight: 400;
color: #b8d4e8;
margin-top: 4px;
}
.creator {
color: #f7d44a;
font-size: 0.9rem;
font-weight: 500;
border-top: 1px solid #3a5a6e;
padding-top: 10px;
margin-top: 8px;
width: 100%;
}
.game-container {
background: #2a3f4f;
padding: 30px 20px 20px 20px;
border-radius: 40px;
box-shadow: 0 20px 30px rgba(0, 0, 0, 0.7), inset 0 2px 5px rgba(255, 255, 255, 0.1);
display: flex;
flex-direction: column;
align-items: center;
position: relative;
}
canvas {
display: block;
background: #1e2e3b;
border-radius: 20px;
box-shadow: inset 0 0 0 2px #3f5a6b, 0 10px 15px rgba(0, 0, 0, 0.5);
width: 450px;
height: 450px;
image-rendering: pixelated;
touch-action: none;
}
.right-panel {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
margin-top: 10px;
}
.controls-container {
background: #1f3340;
border-radius: 20px;
padding: 15px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
box-shadow: inset 0 0 0 2px #3a5a6e, 0 10px 15px rgba(0, 0, 0, 0.5);
width: 130px;
height: 130px;
}
.arrow-grid {
display: grid;
grid-template-columns: 50px 50px 50px;
grid-template-rows: 50px 50px 50px;
gap: 4px;
justify-items: center;
align-items: center;
}
.arrow-btn {
width: 50px;
height: 50px;
background: #4a6a7f;
border: none;
border-radius: 12px;
color: #f0f9ff;
font-size: 26px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 4px 0 #1f3340, 0 6px 10px rgba(0, 0, 0, 0.3);
transition: all 0.06s ease;
touch-action: none;
}
.arrow-btn:active {
transform: translateY(4px);
box-shadow: 0 1px 0 #1f3340, 0 4px 8px rgba(0, 0, 0, 0.3);
}
.arrow-btn:hover {
background: #5c7f97;
}
.arrow-btn.empty {
background: transparent;
box-shadow: none;
cursor: default;
}
.arrow-btn.empty:hover {
background: transparent;
}
.shop-section {
background: #1f3340;
border-radius: 20px;
padding: 12px 10px;
box-shadow: inset 0 0 0 2px #3a5a6e, 0 10px 15px rgba(0, 0, 0, 0.5);
width: 130px;
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
}
.shop-btn {
background: #4a6a7f;
border: none;
border-radius: 30px;
padding: 6px 12px;
font-size: 0.9rem;
font-weight: 700;
color: #f0f9ff;
cursor: pointer;
box-shadow: 0 4px 0 #1f3340, 0 6px 10px rgba(0, 0, 0, 0.3);
transition: all 0.08s ease;
width: 100%;
letter-spacing: 0.5px;
}
.shop-btn:active {
transform: translateY(4px);
box-shadow: 0 1px 0 #1f3340, 0 4px 8px rgba(0, 0, 0, 0.3);
}
.shop-btn:hover {
background: #5c7f97;
}
.slots-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
width: 100%;
}
.slot-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
cursor: pointer;
}
.slot {
background: #0f1a24;
border: 2px solid #3a5a6e;
border-radius: 8px;
padding: 4px;
width: 100%;
aspect-ratio: 1;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.4rem;
color: #b8d4e8;
box-shadow: inset 0 0 8px rgba(0, 0, 0, 0.5);
transition: border-color 0.2s;
min-height: 36px;
}
.slot:hover {
border-color: #f7d44a;
}
.slot-count {
color: #b8d4e8;
font-size: 0.8rem;
font-weight: 600;
text-align: center;
background: #0f1a24;
padding: 0 6px;
border-radius: 4px;
border: 1px solid #2a3f4f;
min-width: 20px;
line-height: 1.4;
}
.shop-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.75);
backdrop-filter: blur(8px);
z-index: 1000;
justify-content: center;
align-items: center;
}
.shop-overlay.active {
display: flex;
}
.shop-modal {
background: #2a3f4f;
border-radius: 30px;
padding: 30px 35px 35px 35px;
max-width: 500px;
width: 90%;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.8), inset 0 0 0 2px #3f5a6b;
color: #b8d4e8;
position: relative;
max-height: 90vh;
overflow-y: auto;
}
.shop-modal h2 {
color: #f7d44a;
text-align: center;
font-size: 2rem;
margin: 0 0 6px 0;
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
}
.shop-modal .close-hint {
text-align: center;
font-size: 0.8rem;
color: #7e9fb0;
margin-bottom: 20px;
}
.shop-items {
display: flex;
flex-direction: column;
gap: 12px;
}
.shop-item {
display: flex;
align-items: center;
gap: 14px;
background: #1f3340;
border-radius: 16px;
padding: 10px 16px;
border: 1px solid #3a5a6e;
transition: all 0.15s;
}
.shop-item:hover {
border-color: #f7d44a;
background: #26404f;
}
.shop-item .icon {
font-size: 2.2rem;
min-width: 50px;
text-align: center;
}
.shop-item .info {
flex: 1;
}
.shop-item .info .name {
font-weight: 600;
font-size: 1.1rem;
color: #f0f9ff;
}
.shop-item .info .desc {
font-size: 0.85rem;
color: #7e9fb0;
margin-top: 2px;
}
.shop-item .price {
font-size: 1.1rem;
font-weight: 700;
color: #f7d44a;
background: #0f1a24;
padding: 4px 12px;
border-radius: 20px;
border: 1px solid #3a5a6e;
white-space: nowrap;
}
.shop-item .buy-btn {
background: #4a6a7f;
border: none;
border-radius: 30px;
padding: 6px 18px;
font-size: 1rem;
font-weight: 700;
color: #f0f9ff;
cursor: pointer;
box-shadow: 0 4px 0 #1f3340;
transition: all 0.08s ease;
}
.shop-item .buy-btn:active {
transform: translateY(4px);
box-shadow: 0 1px 0 #1f3340;
}
.shop-item .buy-btn:hover {
background: #5c7f97;
}
.shop-item .buy-btn:disabled {
opacity: 0.4;
transform: none;
box-shadow: 0 4px 0 #1f3340;
cursor: not-allowed;
}
.shop-close-btn {
display: block;
margin: 20px auto 0 auto;
background: #4a6a7f;
border: none;
border-radius: 30px;
padding: 10px 30px;
font-size: 1rem;
font-weight: 700;
color: #f0f9ff;
cursor: pointer;
box-shadow: 0 4px 0 #1f3340;
transition: all 0.08s ease;
}
.shop-close-btn:active {
transform: translateY(4px);
box-shadow: 0 1px 0 #1f3340;
}
.shop-close-btn:hover {
background: #5c7f97;
}
.info-panel {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 18px;
padding: 0 10px;
flex-wrap: wrap;
gap: 8px;
}
.status {
color: #b8d4e8;
font-size: 1.2rem;
font-weight: 600;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
letter-spacing: 0.5px;
flex: 1 1 auto;
}
.status span {
color: #f7d44a;
font-weight: 700;
}
.stats {
display: flex;
gap: 20px;
color: #b8d4e8;
font-size: 1.0rem;
font-weight: 500;
background: #1f3340;
padding: 6px 16px;
border-radius: 40px;
box-shadow: inset 0 2px 4px rgba(0,0,0,0.3);
flex-wrap: wrap;
justify-content: center;
}
.stats span {
color: #f7d44a;
font-weight: 700;
}
.difficulty-selector {
display: flex;
gap: 6px;
background: #1f3340;
padding: 4px 10px;
border-radius: 40px;
box-shadow: inset 0 2px 4px rgba(0,0,0,0.3);
flex-wrap: wrap;
justify-content: center;
}
.difficulty-selector button {
background: transparent;
border: none;
color: #7e9fb0;
font-weight: 600;
font-size: 0.75rem;
padding: 4px 8px;
border-radius: 30px;
cursor: pointer;
transition: all 0.2s;
box-shadow: none;
text-shadow: none;
margin: 0;
white-space: nowrap;
position: relative;
}
.difficulty-selector button:hover:not(.locked) {
background: #3a5a6e;
color: #f0f9ff;
}
.difficulty-selector button.active {
background: #4a6a7f;
color: #f0f9ff;
box-shadow: 0 2px 0 #1f3340;
}
.difficulty-selector button.locked {
opacity: 0.5;
cursor: not-allowed;
color: #5a7a8a;
}
button {
background: #4a6a7f;
border: none;
border-radius: 30px;
padding: 10px 28px;
font-size: 1rem;
font-weight: 700;
color: #f0f9ff;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
box-shadow: 0 6px 0 #1f3340, 0 8px 12px rgba(0, 0, 0, 0.3);
cursor: pointer;
transition: all 0.08s ease;
letter-spacing: 0.8px;
}
button:active {
transform: translateY(5px);
box-shadow: 0 1px 0 #1f3340, 0 5px 10px rgba(0, 0, 0, 0.3);
}
button:hover {
background: #5c7f97;
box-shadow: 0 6px 0 #26404f, 0 8px 14px rgba(0, 0, 0, 0.4);
}
#nextDifficultyButton {
display: none;
background: #f7d44a;
color: #1a2a3a;
box-shadow: 0 6px 0 #b8860b, 0 8px 12px rgba(0, 0, 0, 0.3);
margin-top: 10px;
font-size: 1.1rem;
}
#nextDifficultyButton:hover {
background: #ffe066;
box-shadow: 0 6px 0 #b8860b, 0 8px 14px rgba(0, 0, 0, 0.4);
}
@media (max-width: 850px) {
.game-wrapper {
flex-direction: column;
align-items: center;
gap: 10px;
}
.left-panel {
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
min-width: unset;
max-width: unset;
width: 100%;
padding: 12px 16px;
gap: 12px;
}
.left-panel .pause-btn {
width: 48px;
height: 48px;
font-size: 24px;
margin-bottom: 0;
}
.left-panel .instructions {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 6px 16px;
width: auto;
margin-bottom: 0;
font-size: 0.8rem;
}
.left-panel .instructions p {
margin: 0;
}
.left-panel .mode-task {
font-size: 0.9rem;
padding: 6px 10px;
margin: 4px 0 8px 0;
width: auto;
flex: 1 1 auto;
min-width: 150px;
}
.left-panel .mode-task span {
font-size: 0.75rem;
}
.left-panel .creator {
border-top: none;
padding-top: 0;
margin-top: 0;
width: auto;
font-size: 0.8rem;
}
.right-panel {
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
gap: 10px;
width: 100%;
}
.controls-container {
width: 120px;
height: 120px;
padding: 10px;
}
.arrow-grid {
grid-template-columns: 40px 40px 40px;
grid-template-rows: 40px 40px 40px;
gap: 3px;
}
.arrow-btn {
width: 40px;
height: 40px;
font-size: 20px;
}
.shop-section {
width: 140px;
padding: 10px;
}
.slots-grid {
gap: 6px;
}
.slot {
min-height: 32px;
font-size: 1.2rem;
}
.slot-count {
font-size: 0.7rem;
}
.game-container {
padding: 20px 10px 10px 10px;
}
canvas {
width: 330px;
height: 330px;
}
.info-panel {
flex-direction: column;
align-items: stretch;
gap: 6px;
}
.stats {
justify-content: space-around;
}
.difficulty-selector {
justify-content: center;
}
button {
padding: 8px 20px;
font-size: 0.9rem;
}
#nextDifficultyButton {
font-size: 1rem;
padding: 8px 20px;
}
.shop-modal {
padding: 20px;
}
.shop-item {
flex-wrap: wrap;
gap: 8px;
}
.shop-item .price {
font-size: 0.9rem;
padding: 2px 10px;
}
.shop-item .buy-btn {
font-size: 0.9rem;
padding: 4px 14px;
}
}
@media (max-width: 450px) {
canvas {
width: 280px;
height: 280px;
}
.left-panel {
padding: 8px 10px;
gap: 8px;
}
.left-panel .pause-btn {
width: 40px;
height: 40px;
font-size: 20px;
}
.left-panel .instructions p {
font-size: 0.7rem;
}
.left-panel .mode-task {
font-size: 0.8rem;
padding: 4px 8px;
}
.left-panel .creator {
font-size: 0.7rem;
}
.right-panel {
flex-direction: column;
align-items: center;
gap: 8px;
}
.controls-container {
width: 100px;
height: 100px;
padding: 8px;
}
.arrow-grid {
grid-template-columns: 32px 32px 32px;
grid-template-rows: 32px 32px 32px;
gap: 2px;
}
.arrow-btn {
width: 32px;
height: 32px;
font-size: 16px;
}
.shop-section {
width: 110px;
padding: 8px;
}
.slots-grid {
gap: 4px;
}
.slot {
min-height: 28px;
font-size: 1.0rem;
}
.slot-count {
font-size: 0.65rem;
}
.shop-modal {
padding: 16px;
}
.shop-item .icon {
font-size: 1.6rem;
min-width: 36px;
}
.shop-item .info .name {
font-size: 0.95rem;
}
.shop-item .info .desc {
font-size: 0.75rem;
}
}
</style>
</head>
<body>
<div class="game-wrapper">
<!-- Левая панель -->
<div class="left-panel">
<button class="pause-btn" id="pauseButton" title="Пауза (Q/Й)">⚙️</button>
<div class="mode-task" id="modeTask">
Задание:
<span id="modeTaskText">Собери все яблоки 🍎 и уворачивайся от черепов 💀 и врагов 👾</span>
</div>
<div class="instructions">
<p>← ↑ → ↓ / WASD — ход</p>
<p>Q/Й — пауза</p>
<p>E/У — магазин</p>
<p>Shift — новая игра</p>
<p>Пробел — след. уровень (при победе)</p>
<p>1-4 — активация предметов</p>
</div>
<div class="creator">Создатель игры — riavly (Максим),<br>любит кириешки с соусом</div>
</div>
<!-- Игровое поле -->
<div class="game-container">
<canvas id="mazeCanvas" width="450" height="450"></canvas>
<div class="info-panel">
<div class="status" id="statusDisplay">🚀 Игра <span>активна</span></div>
<div class="stats">
<span>❤️ <span id="livesCounter">3</span></span>
<span>⭐ <span id="starsBalance">0</span></span>
<span>🍎 <span id="appleProgress">0/20</span></span>
<span>🚶 <span id="stepCounter">0</span></span>
<span>⏱️ <span id="timeCounter">0</span>с</span>
<span id="appleStat">🍎 <span id="itemCounter">0</span>/<span id="totalItems">0</span></span>
</div>
<div class="difficulty-selector">
<button data-diff="easy" class="active">😊 Простой</button>
<button data-diff="medium" class="locked">🤔 Средний 🔒</button>
<button data-diff="hard" class="locked">😈 Сложный 🔒</button>
<button data-diff="mega" class="locked">🗺️ Мега 🔒</button>
<button data-diff="survival" class="locked">🔥 Выживание 🔒</button>
</div>
<button id="resetButton">🔄 Новая игра</button>
</div>
<button id="nextDifficultyButton">➡️ Следующая сложность</button>
</div>
<!-- Правая панель -->
<div class="right-panel">
<div class="controls-container">
<div class="arrow-grid">
<div></div>
<button class="arrow-btn" id="arrow-up">▲</button>
<div></div>
<button class="arrow-btn" id="arrow-left">◄</button>
<div class="arrow-btn empty"></div>
<button class="arrow-btn" id="arrow-right">►</button>
<div></div>
<button class="arrow-btn" id="arrow-down">▼</button>
<div></div>
</div>
</div>
<div class="shop-section">
<button class="shop-btn" id="shopButton">🏪 Магазин</button>
<div class="slots-grid">
<div class="slot-item" data-key="lightning">
<div class="slot">⚡</div>
<span class="slot-count" id="count-lightning">0</span>
</div>
<div class="slot-item" data-key="pickaxe">
<div class="slot">⛏️</div>
<span class="slot-count" id="count-pickaxe">0</span>
</div>
<div class="slot-item" data-key="cake">
<div class="slot">🎂</div>
<span class="slot-count" id="count-cake">0</span>
</div>
<div class="slot-item" data-key="clock">
<div class="slot">⏰</div>
<span class="slot-count" id="count-clock">0</span>
</div>
</div>
</div>
</div>
</div>
<!-- Модальное окно магазина -->
<div class="shop-overlay" id="shopOverlay">
<div class="shop-modal">
<h2>🏪 Магазин</h2>
<div class="close-hint">Нажмите E / У или ✕ для закрытия</div>
<div class="shop-items" id="shopItems"></div>
<button class="shop-close-btn" id="shopCloseBtn">✕ Закрыть</button>
</div>
</div>
<script>
// ========== ЗВУКОВЫЕ ЭФФЕКТЫ (Web Audio API) ==========
let audioCtx = null;
function getAudioContext() {
if (!audioCtx) {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
return audioCtx;
}
// Шаг — короткий щелчок
function playStepSound() {
try {
const ctx = getAudioContext();
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.connect(gain);
gain.connect(ctx.destination);
osc.frequency.value = 400;
osc.type = 'sine';
gain.gain.setValueAtTime(0.03, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.05);
osc.start(ctx.currentTime);
osc.stop(ctx.currentTime + 0.05);
} catch (e) { /* тихо игнорируем ошибки */ }
}
// Яблоко — приятный звон
function playAppleSound() {
try {
const ctx = getAudioContext();
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.connect(gain);
gain.connect(ctx.destination);
osc.frequency.setValueAtTime(880, ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(1320, ctx.currentTime + 0.1);
osc.type = 'sine';
gain.gain.setValueAtTime(0.08, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.12);
osc.start(ctx.currentTime);
osc.stop(ctx.currentTime + 0.12);
} catch (e) { /* тихо игнорируем ошибки */ }
}
// Ловушка — низкий грохот
function playTrapSound() {
try {
const ctx = getAudioContext();
// Смесь шума и низкого тона
const bufferSize = ctx.sampleRate * 0.2;
const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
data[i] = (Math.random() * 2 - 1) * Math.exp(-i / bufferSize * 3);
}
const noise = ctx.createBufferSource();
noise.buffer = buffer;
const gain = ctx.createGain();
noise.connect(gain);
gain.connect(ctx.destination);
gain.gain.setValueAtTime(0.06, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.2);
noise.start(ctx.currentTime);
noise.stop(ctx.currentTime + 0.2);
// Добавим низкий удар
const osc = ctx.createOscillator();
const gain2 = ctx.createGain();
osc.connect(gain2);
gain2.connect(ctx.destination);
osc.frequency.setValueAtTime(150, ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(80, ctx.currentTime + 0.15);
osc.type = 'sawtooth';
gain2.gain.setValueAtTime(0.04, ctx.currentTime);
gain2.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.15);
osc.start(ctx.currentTime);
osc.stop(ctx.currentTime + 0.15);
} catch (e) { /* тихо игнорируем ошибки */ }
}
// --- Конфигурация сложности ---
const DIFFICULTY = {
easy: { size: 15, extraPassages: 0.30, itemCount: 4, trapCount: 3, enemyInterval: 1000, survival: false, mega: false },
medium: { size: 20, extraPassages: 0.25, itemCount: 6, trapCount: 5, enemyInterval: 750, survival: false, mega: false },
hard: { size: 25, extraPassages: 0.05, itemCount: 8, trapCount: 7, enemyInterval: 500, survival: false, mega: false },
mega: { size: 30, extraPassages: 0.05, itemCount: 0, trapCount: 0, enemyInterval: 0, survival: false, mega: true },
survival: { size: 20, extraPassages: 0.25, itemCount: 5, trapCount: 0, enemyInterval: 500, survival: true, mega: false }
};
const MAIN_CYCLE = ['easy', 'medium', 'hard', 'mega'];
const APPLES_PER_LIFE = 20;
const BOMB_FUSE_TIME = 2;
const SHOP_ITEMS = [
{ id: 'lightning', icon: '⚡', name: 'Молния', desc: 'Двойной шаг на 10 сек', price: 3, key: 'lightning' },
{ id: 'pickaxe', icon: '⛏️', name: 'Кирка', desc: 'Ходить по стенам 5 сек', price: 5, key: 'pickaxe' },
{ id: 'cake', icon: '🎂', name: 'Торт', desc: '+1 жизнь', price: 7, key: 'cake' },
{ id: 'clock', icon: '⏰', name: 'Часы', desc: 'Замедляет врагов на 5 сек', price: 10, key: 'clock' }
];
// --- DOM элементы ---
const canvas = document.getElementById('mazeCanvas');
const ctx = canvas.getContext('2d');
const statusDisplay = document.getElementById('statusDisplay');
const stepCounter = document.getElementById('stepCounter');
const timeCounter = document.getElementById('timeCounter');
const itemCounter = document.getElementById('itemCounter');
const totalItemsSpan = document.getElementById('totalItems');
const livesCounter = document.getElementById('livesCounter');
const appleProgress = document.getElementById('appleProgress');
const starsBalanceSpan = document.getElementById('starsBalance');
const diffButtons = document.querySelectorAll('.difficulty-selector button');
const nextDiffButton = document.getElementById('nextDifficultyButton');
const pauseButton = document.getElementById('pauseButton');
const modeTaskText = document.getElementById('modeTaskText');
const shopOverlay = document.getElementById('shopOverlay');
const shopItemsContainer = document.getElementById('shopItems');
const shopCloseBtn = document.getElementById('shopCloseBtn');
const shopButton = document.getElementById('shopButton');
const countCake = document.getElementById('count-cake');
const countClock = document.getElementById('count-clock');
const countLightning = document.getElementById('count-lightning');
const countPickaxe = document.getElementById('count-pickaxe');
// --- Инвентарь и валюты ---
const inventory = { cake: 0, clock: 0, lightning: 0, pickaxe: 0 };
let appleBalance = 0;
let starsBalance = 0;
function updateInventoryDisplay() {
countCake.textContent = inventory.cake;
countClock.textContent = inventory.clock;
countLightning.textContent = inventory.lightning;
countPickaxe.textContent = inventory.pickaxe;
}
function updateStarsDisplay() {
starsBalanceSpan.textContent = starsBalance;
}
// --- Предметы на поле ---
let fieldItems = [];
// --- Активные эффекты ---
let doubleStepActive = false;
let pickaxeActive = false;
let lightningTimer = null;
let pickaxeTimer = null;
// --- Кнопки-стрелки ---
const arrowUp = document.getElementById('arrow-up');
const arrowDown = document.getElementById('arrow-down');
const arrowLeft = document.getElementById('arrow-left');
const arrowRight = document.getElementById('arrow-right');
// --- Переменные состояния ---
let currentSize = 15;
let cellSize = canvas.width / currentSize;
let maze = [];
let playerX = 1;
let playerY = 1;
let exitX, exitY;
let gameOver = false;
let gameWon = false;
let items = [];
let traps = [];
let collectedItems = 0;
let totalItems = 0;
let exitVisible = false;
let isSurvival = false;
let isMega = false;
let lives = 3;
let applesSinceLastLife = 0;
let steps = 0;
let seconds = 0;
let timerInterval = null;
let timerStarted = false;
let enemies = [];
let enemyInterval = null;
let currentEnemyIntervalMs = 500;
let currentDifficulty = 'easy';
let isPaused = false;
let isShopOpen = false;
let redFlash = false;
let redFlashTimeout = null;
let yellowFlash = false;
let yellowFlashTimeout = null;
let explosionZone = null;
let shield = null;
let shieldRespawnTimeout = null;
let bossHP = 10;
const MAX_BOSS_HP = 10;
let sword = null;
let hasSword = false;
let bomb = null;
let bombSpawnTimer = 3;
let unlocked = {
easy: true,
medium: false,
hard: false,
mega: false,
survival: false
};
let secret = { pressed6: false, timestamp: 0 };
// --- Вспомогательные функции ---
function getSizeFromDifficulty(diffKey) {
return DIFFICULTY[diffKey].size;
}
function getExtraPassages(diffKey) {
return DIFFICULTY[diffKey].extraPassages;
}
function getItemCount(diffKey) {
return DIFFICULTY[diffKey].itemCount;
}
function getTrapCount(diffKey) {
return DIFFICULTY[diffKey].trapCount;
}
function getEnemyInterval(diffKey) {
return DIFFICULTY[diffKey].enemyInterval;
}
function isSurvivalMode(diffKey) {
return DIFFICULTY[diffKey].survival || false;
}
function isMegaMode(diffKey) {
return DIFFICULTY[diffKey].mega || false;
}
function updateModeInstructions() {
if (isSurvival) {
modeTaskText.textContent = 'Собери как можно больше яблок 🍎, избегая врагов 👾';
} else if (isMega) {
modeTaskText.textContent = 'Снеси все сердца босса ❤️ (10 HP)';
} else {
modeTaskText.textContent = 'Собери все яблоки 🍎 и уворачивайся от черепов 💀 и врагов 👾';
}
}
function updateDifficultyButtons() {
diffButtons.forEach(btn => {
const diff = btn.dataset.diff;
const isUnlocked = unlocked[diff] || false;
if (isUnlocked) {
btn.classList.remove('locked');
btn.textContent = btn.textContent.replace(/ 🔒$/, '');
} else {
btn.classList.add('locked');
if (!btn.textContent.includes('🔒')) {
btn.textContent += ' 🔒';
}
}
if (diff === currentDifficulty) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
});
}
function unlockNextLevel() {
const currentIndex = MAIN_CYCLE.indexOf(currentDifficulty);
if (currentIndex !== -1 && currentIndex < MAIN_CYCLE.length - 1) {
const nextDiff = MAIN_CYCLE[currentIndex + 1];
if (!unlocked[nextDiff]) {
unlocked[nextDiff] = true;
checkSurvivalUnlock();
updateDifficultyButtons();
}
} else if (currentDifficulty === 'mega') {
checkSurvivalUnlock();
updateDifficultyButtons();
}
}
function checkSurvivalUnlock() {
if (unlocked.easy && unlocked.medium && unlocked.hard && unlocked.mega) {
if (!unlocked.survival) {
unlocked.survival = true;
updateDifficultyButtons();
}
}
}
// --- Генерация лабиринта ---
function generateMaze(size, extraPercent, mega) {
maze = Array.from({ length: size }, () => Array(size).fill(true));
const startX = 1, startY = 1;
maze[startY][startX] = false;
const stack = [{ x: startX, y: startY }];
const dirs = [
{ dx: 0, dy: -2 },
{ dx: 0, dy: 2 },
{ dx: -2, dy: 0 },
{ dx: 2, dy: 0 }
];
while (stack.length > 0) {
const current = stack[stack.length - 1];
const { x, y } = current;
const neighbors = [];
for (const d of dirs) {
const nx = x + d.dx;
const ny = y + d.dy;
if (nx > 0 && nx < size - 1 && ny > 0 && ny < size - 1 && maze[ny][nx] === true) {
neighbors.push({ x: nx, y: ny, dx: d.dx / 2, dy: d.dy / 2 });
}
}
if (neighbors.length === 0) {
stack.pop();
} else {
const chosen = neighbors[Math.floor(Math.random() * neighbors.length)];
maze[y + chosen.dy][x + chosen.dx] = false;
maze[chosen.y][chosen.x] = false;
stack.push({ x: chosen.x, y: chosen.y });
}
}
exitX = size - 2;
exitY = size - 2;
maze[exitY][exitX] = false;
maze[startY][startX] = false;
addExtraPassages(size, extraPercent);
ensureExitAccessible(size);
if (mega) {
const center = Math.floor(size / 2);
const halfBlock = 2;
for (let dy = -halfBlock; dy <= halfBlock; dy++) {
for (let dx = -halfBlock; dx <= halfBlock; dx++) {
const row = center + dy;
const col = center + dx;
if (row >= 0 && row < size && col >= 0 && col < size) {
maze[row][col] = true;
}
}
}
for (let dy = -halfBlock - 1; dy <= halfBlock + 1; dy++) {
for (let dx = -halfBlock - 1; dx <= halfBlock + 1; dx++) {
const row = center + dy;
const col = center + dx;
if (row >= 0 && row < size && col >= 0 && col < size) {
if (Math.abs(dy) <= halfBlock && Math.abs(dx) <= halfBlock) continue;
maze[row][col] = false;
}
}
}
}
}
function addExtraPassages(size, extraPercent) {
const innerCells = (size - 2) * (size - 2);
const extraCount = Math.floor(innerCells * extraPercent);
let added = 0;
let attempts = 0;
const maxAttempts = extraCount * 30;
while (added < extraCount && attempts < maxAttempts) {
attempts++;
const row = 1 + Math.floor(Math.random() * (size - 2));
const col = 1 + Math.floor(Math.random() * (size - 2));
if (maze[row][col] === true) continue;
const dirs = [
{ dx: 0, dy: -2 },
{ dx: 0, dy: 2 },
{ dx: -2, dy: 0 },
{ dx: 2, dy: 0 }
];
const dir = dirs[Math.floor(Math.random() * dirs.length)];
const nx = col + dir.dx;
const ny = row + dir.dy;
if (nx <= 0 || nx >= size - 1 || ny <= 0 || ny >= size - 1) continue;
const midX = col + dir.dx / 2;
const midY = row + dir.dy / 2;
if (maze[midY][midX] === true && maze[row][col] === false && maze[ny][nx] === false) {
maze[midY][midX] = false;
added++;
}
}
}
function ensureExitAccessible(size) {
const dirs = [
{ dx: 0, dy: -1 },
{ dx: 0, dy: 1 },
{ dx: -1, dy: 0 },
{ dx: 1, dy: 0 }
];
for (const d of dirs) {
const nx = exitX + d.dx;
const ny = exitY + d.dy;
if (nx >= 0 && nx < size && ny >= 0 && ny < size) {
maze[ny][nx] = false;
}
}
}
// --- Спавн врагов ---
function spawnEnemies(size, survival, mega) {
enemies = [];
if (survival) {
const positions = [
{ x: 1, y: size - 2 },
{ x: size - 2, y: 1 },
{ x: size - 2, y: size - 2 }
];
for (const pos of positions) {
if (maze[pos.y][pos.x] === true) {
maze[pos.y][pos.x] = false;
const dirs = [
{ dx: 0, dy: -1 }, { dx: 0, dy: 1 },
{ dx: -1, dy: 0 }, { dx: 1, dy: 0 }
];
for (const d of dirs) {
const nx = pos.x + d.dx;
const ny = pos.y + d.dy;
if (nx >= 0 && nx < size && ny >= 0 && ny < size && maze[ny][nx] === true) {
maze[ny][nx] = false;
break;
}
}
}
enemies.push({ x: pos.x, y: pos.y });
}
} else if (!mega) {
let sx = size - 2;
let sy = 1;
if (maze[sy][sx] === true) maze[sy][sx] = false;
const dirs = [
{ dx: -1, dy: 0 }, { dx: 0, dy: 1 }
];
for (const d of dirs) {
const nx = sx + d.dx;
const ny = sy + d.dy;
if (nx >= 0 && nx < size && ny >= 0 && ny < size && maze[ny][nx] === true) {
maze[ny][nx] = false;
break;
}
}
enemies.push({ x: sx, y: sy });
}
}
// --- Размещение предметов и ловушек ---
function placeItemsAndTraps(size, itemCount, trapCount, survival, mega) {
items = [];
traps = [];
let freeCells = [];
for (let row = 0; row < size; row++) {
for (let col = 0; col < size; col++) {
if (maze[row][col] === false) {
if ((row === 1 && col === 1)) continue;
if (!survival && !mega && (row === exitY && col === exitX)) continue;
let occupied = false;
for (const e of enemies) {
if (e.x === col && e.y === row) { occupied = true; break; }
}
if (occupied) continue;
freeCells.push({ x: col, y: row });
}
}
}
for (let i = freeCells.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[freeCells[i], freeCells[j]] = [freeCells[j], freeCells[i]];
}
const actualItemCount = Math.min(itemCount, freeCells.length);
for (let i = 0; i < actualItemCount; i++) {
items.push(freeCells[i]);
}
if (!survival && !mega) {
const remaining = freeCells.slice(actualItemCount);
const actualTrapCount = Math.min(trapCount, remaining.length);
for (let i = 0; i < actualTrapCount; i++) {
traps.push(remaining[i]);
}
}
totalItems = items.length;
if (survival) {
totalItemsSpan.textContent = '∞';
totalItemsSpan.style.display = 'inline';
totalItems = Infinity;
} else if (mega) {
totalItemsSpan.textContent = '0';
totalItemsSpan.style.display = 'inline';
totalItems = 0;
exitVisible = false;
} else {
totalItemsSpan.style.display = 'inline';
totalItemsSpan.textContent = totalItems;
exitVisible = false;
}
collectedItems = 0;
itemCounter.textContent = '0';
appleBalance = 0;
updateInventoryDisplay();
fieldItems = [];
doubleStepActive = false;
pickaxeActive = false;
if (lightningTimer) clearTimeout(lightningTimer);
if (pickaxeTimer) clearTimeout(pickaxeTimer);
}
// --- Сбор яблок ---
function collectItem(x, y) {
const index = items.findIndex(item => item.x === x && item.y === y);
if (index !== -1) {
items.splice(index, 1);
collectedItems++;
appleBalance++;
itemCounter.textContent = collectedItems;
// Звук яблока
playAppleSound();
applesSinceLastLife++;
const progress = applesSinceLastLife % APPLES_PER_LIFE;
const progressDisplay = progress === 0 ? APPLES_PER_LIFE : progress;
appleProgress.textContent = `${progressDisplay}/${APPLES_PER_LIFE}`;
if (applesSinceLastLife >= APPLES_PER_LIFE) {
applesSinceLastLife = 0;
lives++;
livesCounter.textContent = lives;
statusDisplay.innerHTML = '❤️ +1 жизнь! (бонус за 20 яблок)';
setTimeout(() => {
if (!gameOver && !isPaused) {
if (isSurvival) {
statusDisplay.innerHTML = `🔥 Выживание — собрано ${collectedItems} 🍎, время ${seconds}с`;
} else if (isMega) {
statusDisplay.innerHTML = `🗺️ Мега — босс имеет ${bossHP} ❤️`;
} else if (!exitVisible) {
statusDisplay.innerHTML = `🍎 Соберите все предметы (${collectedItems}/${totalItems === Infinity ? '∞' : totalItems})`;
} else {
statusDisplay.innerHTML = '🔑 Выход открыт! Идите к 🚪';
}
}
}, 1500);
}
if (isSurvival) {
spawnNewItem();
} else if (!isMega) {
if (collectedItems === totalItems) {
exitVisible = true;
statusDisplay.innerHTML = '🔑 Все предметы собраны! Ищите выход 🚪';
}
}
return true;
}
return false;
}
// --- Остальные функции ---
function spawnNewItem() {
if (!isSurvival) return;
let freeCells = [];
for (let row = 0; row < currentSize; row++) {
for (let col = 0; col < currentSize; col++) {
if (maze[row][col] === false) {
if (row === playerY && col === playerX) continue;
let occupied = false;
for (const e of enemies) {
if (e.x === col && e.y === row) { occupied = true; break; }
}
if (occupied) continue;
for (const it of items) {
if (it.x === col && it.y === row) { occupied = true; break; }
}
if (occupied) continue;
if (shield && shield.x === col && shield.y === row) continue;
freeCells.push({ x: col, y: row });
}
}
}
if (freeCells.length > 0) {
const idx = Math.floor(Math.random() * freeCells.length);
items.push(freeCells[idx]);
}
}
function spawnShield() {
if (!isSurvival) return;
if (shieldRespawnTimeout) {
clearTimeout(shieldRespawnTimeout);
shieldRespawnTimeout = null;
}
let freeCells = [];
for (let row = 0; row < currentSize; row++) {
for (let col = 0; col < currentSize; col++) {
if (maze[row][col] === false) {
if (row === playerY && col === playerX) continue;
let occupied = false;
for (const e of enemies) {
if (e.x === col && e.y === row) { occupied = true; break; }
}
if (occupied) continue;
for (const it of items) {
if (it.x === col && it.y === row) { occupied = true; break; }
}
if (occupied) continue;
if (shield && shield.x === col && shield.y === row) continue;
freeCells.push({ x: col, y: row });
}
}
}
if (freeCells.length > 0) {
const idx = Math.floor(Math.random() * freeCells.length);
shield = { x: freeCells[idx].x, y: freeCells[idx].y };
} else {
shield = null;
}
render();
}
function activateShield() {
if (!isSurvival || !shield) return;
shield = null;
if (yellowFlashTimeout) clearTimeout(yellowFlashTimeout);
yellowFlash = true;
render();
yellowFlashTimeout = setTimeout(() => {
yellowFlash = false;
render();
yellowFlashTimeout = null;
}, 2000);
if (enemyInterval) {
clearInterval(enemyInterval);
enemyInterval = null;
}
let freeCells = [];
for (let row = 0; row < currentSize; row++) {
for (let col = 0; col < currentSize; col++) {
if (maze[row][col] === false) {
if (row === playerY && col === playerX) continue;
let occupied = false;
for (const e of enemies) {
if (e.x === col && e.y === row) { occupied = true; break; }
}
if (occupied) continue;
for (const it of items) {
if (it.x === col && it.y === row) { occupied = true; break; }
}
if (occupied) continue;
freeCells.push({ x: col, y: row });
}
}
}
for (let i = 0; i < enemies.length; i++) {
if (freeCells.length === 0) break;
const idx = Math.floor(Math.random() * freeCells.length);
const newPos = freeCells.splice(idx, 1)[0];
enemies[i].x = newPos.x;
enemies[i].y = newPos.y;
}
setTimeout(() => {
if (!gameOver) {
startEnemy(currentEnemyIntervalMs);
}
}, 2000);
if (shieldRespawnTimeout) clearTimeout(shieldRespawnTimeout);
shieldRespawnTimeout = setTimeout(() => {
shieldRespawnTimeout = null;
if (!gameOver && isSurvival) {
spawnShield();
}
}, 10000);
render();
}
function checkShieldPickup() {
if (!isSurvival || !shield) return false;
if (playerX === shield.x && playerY === shield.y) {
activateShield();
return true;
}
return false;
}
function removeTrap(x, y) {
traps = traps.filter(t => !(t.x === x && t.y === y));
}
function loseLife(reason, teleport = true) {
if (gameOver) return;
lives--;
livesCounter.textContent = lives;
// Звук ловушки, если причина — trap
if (reason === 'trap') {
playTrapSound();
}
if (redFlashTimeout) clearTimeout(redFlashTimeout);
redFlash = true;
render();
redFlashTimeout = setTimeout(() => {
redFlash = false;
render();
redFlashTimeout = null;
}, 500);
if (lives <= 0) {
gameOver = true;
gameWon = false;
statusDisplay.innerHTML = '💀 Игра <span>проиграна</span>!';
stopTimer();
stopEnemy();
alert('💀 У вас закончились жизни! Вы проиграли!');
render();
return;
}
if (teleport) {
playerX = 1;
playerY = 1;
}
const msg = reason === 'trap' ? '💀 Ловушка! -1 жизнь' :
reason === 'bomb' ? '💥 Взрыв! -1 жизнь' :
'👾 Враг! -1 жизнь';
statusDisplay.innerHTML = `${msg} (осталось ${lives} ❤️)`;
setTimeout(() => {
if (!gameOver) {
if (isSurvival) {
statusDisplay.innerHTML = `🔥 Выживание — собрано ${collectedItems} 🍎, время ${seconds}с`;
} else if (isMega) {
statusDisplay.innerHTML = `🗺️ Мега — босс имеет ${bossHP} ❤️`;
} else if (!exitVisible) {
statusDisplay.innerHTML = `🍎 Соберите все предметы (${collectedItems}/${totalItems === Infinity ? '∞' : totalItems})`;
} else {
statusDisplay.innerHTML = '🔑 Выход открыт! Идите к 🚪';
}
}
}, 1500);
render();
}
function moveEnemy() {
if (gameOver || isPaused || isShopOpen) return;
if (enemies.length === 0) return;
for (let i = 0; i < enemies.length; i++) {
const e = enemies[i];
if (e.x === -1 || e.y === -1) continue;
const dx = Math.sign(playerX - e.x);
const dy = Math.sign(playerY - e.y);
const candidates = [];
if (dx !== 0) {
const nx = e.x + dx;
const ny = e.y;
if (nx >= 0 && nx < currentSize && ny >= 0 && ny < currentSize) {
candidates.push({ dx: dx, dy: 0 });
}
}
if (dy !== 0) {
const nx = e.x;
const ny = e.y + dy;
if (nx >= 0 && nx < currentSize && ny >= 0 && ny < currentSize) {
candidates.push({ dx: 0, dy: dy });
}
}
let chosen = null;
if (candidates.length > 0) {
chosen = candidates[Math.floor(Math.random() * candidates.length)];
} else {
const allDirs = [
{ dx: 0, dy: -1 }, { dx: 0, dy: 1 },
{ dx: -1, dy: 0 }, { dx: 1, dy: 0 }
];
for (let j = allDirs.length - 1; j > 0; j--) {
const k = Math.floor(Math.random() * (j + 1));
[allDirs[j], allDirs[k]] = [allDirs[k], allDirs[j]];
}
for (const d of allDirs) {
const nx = e.x + d.dx;
const ny = e.y + d.dy;
if (nx >= 0 && nx < currentSize && ny >= 0 && ny < currentSize) {
chosen = d;
break;
}
}
}
if (chosen) {
e.x += chosen.dx;
e.y += chosen.dy;
if (e.x === playerX && e.y === playerY) {
loseLife('enemy');
render();
return;
}
}
}
render();
}
function isWalkable(x, y) {
if (x < 0 || x >= currentSize || y < 0 || y >= currentSize) return false;
return !maze[y][x];
}
function startEnemy(intervalMs) {
if (enemyInterval) clearInterval(enemyInterval);
currentEnemyIntervalMs = intervalMs;
if (intervalMs > 0) {
enemyInterval = setInterval(() => {
moveEnemy();
}, intervalMs);
} else {
enemyInterval = null;
}
}
function stopEnemy() {
if (enemyInterval) {
clearInterval(enemyInterval);
enemyInterval = null;
}
}
function spawnSword() {
if (!isMega) return;
let freeCells = [];
const center = Math.floor(currentSize / 2);
const halfBlock = 2;
for (let row = 0; row < currentSize; row++) {
for (let col = 0; col < currentSize; col++) {
if (maze[row][col] === false) {
if (row === 1 && col === 1) continue;
if (Math.abs(row - center) <= halfBlock && Math.abs(col - center) <= halfBlock) continue;
if (row === playerY && col === playerX) continue;
if (shield && shield.x === col && shield.y === row) continue;
if (sword && sword.x === col && sword.y === row) continue;
freeCells.push({ x: col, y: row });
}
}
}
if (freeCells.length > 0) {
const idx = Math.floor(Math.random() * freeCells.length);
sword = freeCells[idx];
} else {
sword = null;
}
render();
}
function isAdjacentToBoss(x, y) {
const center = Math.floor(currentSize / 2);
const halfBlock = 2;
for (let dy = -halfBlock; dy <= halfBlock; dy++) {
for (let dx = -halfBlock; dx <= halfBlock; dx++) {
const bx = center + dx;
const by = center + dy;
const neighbors = [
[bx-1, by], [bx+1, by], [bx, by-1], [bx, by+1]
];
for (const [nx, ny] of neighbors) {
if (nx === x && ny === y) {
return true;
}
}
}
}
return false;
}
function attackBoss() {
if (!isMega) return;
if (!hasSword) return;
if (bossHP <= 0) return;
if (!isAdjacentToBoss(playerX, playerY)) {
return;
}
bossHP--;
hasSword = false;
sword = null;
if (bossHP > 0) {
spawnSword();
}
if (bossHP <= 0) {
gameWon = true;
gameOver = true;
statusDisplay.innerHTML = '🎉 Вы <span>победили</span>! 🎉';
stopTimer();
stopEnemy();
unlockNextLevel();
updateDifficultyButtons();
nextDiffButton.style.display = 'inline-block';
const starsEarned = Math.min(lives, 3);
starsBalance += starsEarned;
updateStarsDisplay();
alert(`🎉 Поздравляем! Вы убили босса! Заработано ${starsEarned} ⭐`);
render();
return;
}
render();
}
function updateBombs() {
if (!isMega) return;
if (gameOver || isPaused || isShopOpen) return;
if (bossHP <= 0) return;
const isEnraged = (bossHP <= 5);
const interval = isEnraged ? 2 : 3;
const radius = isEnraged ? 3 : 2;
if (bomb === null) {
bombSpawnTimer--;
if (bombSpawnTimer <= 0) {
const dirs = [
[0, -1], [0, 1], [-1, 0], [1, 0]
];
for (let i = dirs.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[dirs[i], dirs[j]] = [dirs[j], dirs[i]];
}
let placed = false;
for (const [dx, dy] of dirs) {
const nx = playerX + dx;
const ny = playerY + dy;
if (isWalkable(nx, ny)) {
const center = Math.floor(currentSize / 2);
const halfBlock = 2;
if (Math.abs(nx - center) <= halfBlock && Math.abs(ny - center) <= halfBlock) continue;
if (sword && sword.x === nx && sword.y === ny) continue;
if (shield && shield.x === nx && shield.y === ny) continue;
bomb = { x: nx, y: ny, timer: BOMB_FUSE_TIME };
placed = true;
break;
}
}
if (placed) {
bombSpawnTimer = interval;
render();
} else {
bombSpawnTimer = 1;
}
}
} else {
bomb.timer--;
if (bomb.timer <= 0) {
const bx = bomb.x;
const by = bomb.y;
explosionZone = { x: bx, y: by };
if (window.explosionZoneTimeout) clearTimeout(window.explosionZoneTimeout);
window.explosionZoneTimeout = setTimeout(() => {
explosionZone = null;
render();
}, 500);
const r = radius;
if (playerX >= bx - r && playerX <= bx + r && playerY >= by - r && playerY <= by + r) {
loseLife('bomb', false);
}
bomb = null;
bombSpawnTimer = interval;
render();
}
}
}
// --- Функции для активации предметов ---
function activateItem(key) {
if (gameOver || isPaused || isShopOpen) return;
if (inventory[key] <= 0) return;
let freeCells = [];
for (let dy = -5; dy <= 5; dy++) {
for (let dx = -5; dx <= 5; dx++) {
const dist = Math.abs(dx) + Math.abs(dy);
if (dist > 5 || dist === 0) continue;
const nx = playerX + dx;
const ny = playerY + dy;
if (nx < 0 || nx >= currentSize || ny < 0 || ny >= currentSize) continue;
if (!isWalkable(nx, ny)) continue;
let occupied = false;
if (isMega) {
const center = Math.floor(currentSize / 2);
const halfBlock = 2;
if (Math.abs(nx - center) <= halfBlock && Math.abs(ny - center) <= halfBlock) occupied = true;
}
if (sword && sword.x === nx && sword.y === ny) occupied = true;
if (shield && shield.x === nx && shield.y === ny) occupied = true;
if (fieldItems.some(f => f.x === nx && f.y === ny)) occupied = true;
if (!occupied) freeCells.push({ x: nx, y: ny });
}
}
if (freeCells.length === 0) {
alert('Нет места для предмета!');
return;
}
const idx = Math.floor(Math.random() * freeCells.length);
const pos = freeCells[idx];
inventory[key]--;
updateInventoryDisplay();
fieldItems.push({ x: pos.x, y: pos.y, key: key });
render();
statusDisplay.innerHTML = `📍 Предмет появился рядом!`;
setTimeout(() => {
if (!gameOver && !isPaused && !isShopOpen) {
if (isSurvival) {
statusDisplay.innerHTML = `🔥 Выживание — собрано ${collectedItems} 🍎, время ${seconds}с`;
} else if (isMega) {
statusDisplay.innerHTML = `🗺️ Мега — босс имеет ${bossHP} ❤️`;
} else if (!exitVisible) {
statusDisplay.innerHTML = `🍎 Соберите все предметы (${collectedItems}/${totalItems === Infinity ? '∞' : totalItems})`;
} else {
statusDisplay.innerHTML = '🔑 Выход открыт! Идите к 🚪';
}
}
}, 1500);
}
function applyConsumable(key) {
switch (key) {
case 'cake':
lives++;
livesCounter.textContent = lives;
statusDisplay.innerHTML = '❤️ +1 жизнь!';
break;
case 'clock':
if (enemyInterval) {
clearInterval(enemyInterval);
currentEnemyIntervalMs = currentEnemyIntervalMs * 2;
startEnemy(currentEnemyIntervalMs);
statusDisplay.innerHTML = '⏰ Враги замедлены на 5 секунд!';
setTimeout(() => {
if (!gameOver) {
const diff = DIFFICULTY[currentDifficulty];
startEnemy(diff.enemyInterval);
statusDisplay.innerHTML = '⏰ Скорость врагов восстановлена';
}
}, 5000);
}
break;
case 'lightning':
if (pickaxeActive) {
pickaxeActive = false;
if (pickaxeTimer) {
clearTimeout(pickaxeTimer);
pickaxeTimer = null;
}
}
doubleStepActive = true;
if (lightningTimer) clearTimeout(lightningTimer);
lightningTimer = setTimeout(() => {
doubleStepActive = false;
if (!gameOver) {
statusDisplay.innerHTML = '⚡ Эффект молнии закончился';
}
}, 10000);
statusDisplay.innerHTML = '⚡ Молния! Двойной шаг на 10 секунд!';
break;
case 'pickaxe':
if (doubleStepActive) {
doubleStepActive = false;
if (lightningTimer) {
clearTimeout(lightningTimer);
lightningTimer = null;
}
}
pickaxeActive = true;
if (pickaxeTimer) clearTimeout(pickaxeTimer);
pickaxeTimer = setTimeout(() => {
pickaxeActive = false;
if (!gameOver) {
statusDisplay.innerHTML = '⛏️ Эффект кирки закончился';
}
}, 5000);
statusDisplay.innerHTML = '⛏️ Кирка! Можно ломать стены 5 секунд!';
break;
}
render();
}
function pickupItem(x, y) {
const idx = fieldItems.findIndex(f => f.x === x && f.y === y);
if (idx !== -1) {
const item = fieldItems[idx];
fieldItems.splice(idx, 1);
applyConsumable(item.key);
render();
return true;
}
return false;
}
// --- Игровая логика ---
function attemptMove(dx, dy) {
const newX = playerX + dx;
const newY = playerY + dy;
if (newX < 0 || newX >= currentSize || newY < 0 || newY >= currentSize) return false;
if (isMega) {
const center = Math.floor(currentSize / 2);
const halfBlock = 2;
if (Math.abs(newX - center) <= halfBlock && Math.abs(newY - center) <= halfBlock) return false;
}
if (pickaxeActive) {
if (maze[newY][newX] === true) {
maze[newY][newX] = false;
playerX = newX;
playerY = newY;
return true;
} else {
if (isWalkable(newX, newY)) {
playerX = newX;
playerY = newY;
return true;
}
return false;
}
} else {
if (isWalkable(newX, newY)) {
playerX = newX;
playerY = newY;
return true;
}
return false;
}
}
function isTrap(x, y) {
return traps.some(t => t.x === x && t.y === y);
}
function movePlayer(dx, dy) {
if (gameOver || isPaused || isShopOpen) return;
let stepsToTake = (doubleStepActive && !pickaxeActive) ? 2 : 1;
for (let i = 0; i < stepsToTake; i++) {
const moved = attemptMove(dx, dy);
if (!moved) break;
// Звук шага
playStepSound();
steps++;
updateStepDisplay();
if (!timerStarted) {
startTimer();
timerStarted = true;
}
if (!isMega) {
collectItem(playerX, playerY);
}
if (isTrap(playerX, playerY)) {
removeTrap(playerX, playerY);
loseLife('trap');
if (gameOver) break;
}
pickupItem(playerX, playerY);
if (isSurvival && shield) {
checkShieldPickup();
}
if (!isSurvival && !isMega && exitVisible && playerX === exitX && playerY === exitY) {
gameWon = true;
gameOver = true;
statusDisplay.innerHTML = '🎉 Вы <span>победили</span>! 🎉';
stopTimer();
stopEnemy();
unlockNextLevel();
updateDifficultyButtons();
nextDiffButton.style.display = 'inline-block';
const starsEarned = Math.min(lives, 3);
starsBalance += starsEarned;
updateStarsDisplay();
alert(`🎉 Поздравляем! Вы выиграли! Заработано ${starsEarned} ⭐`);
render();
return;
}
for (const e of enemies) {
if (e.x === playerX && e.y === playerY) {
loseLife('enemy');
if (gameOver) break;
}
}
if (gameOver) break;
if (isMega) {
if (sword && !hasSword) {
const dxS = Math.abs(playerX - sword.x);
const dyS = Math.abs(playerY - sword.y);
if ((dxS === 1 && dyS === 0) || (dxS === 0 && dyS === 1)) {
sword = null;
hasSword = true;
render();
}
}
if (hasSword && bossHP > 0) {
if (isAdjacentToBoss(playerX, playerY)) {
attackBoss();
if (gameOver) break;
}
}
}
render();
}
}
function updateStepDisplay() {
stepCounter.textContent = steps;
}
function startTimer() {
if (timerInterval) clearInterval(timerInterval);
seconds = 0;
timeCounter.textContent = seconds;
timerInterval = setInterval(() => {
if (!isPaused && !gameOver && !isShopOpen) {
seconds++;
timeCounter.textContent = seconds;
updateBombs();
}
}, 1000);
}
function stopTimer() {
if (timerInterval) {
clearInterval(timerInterval);
timerInterval = null;
}
}
function togglePause() {
if (gameOver) return;
isPaused = !isPaused;
if (isPaused) {
statusDisplay.innerHTML = '⏸️ Игра <span>на паузе</span>';
} else {
if (isSurvival) {
statusDisplay.innerHTML = `🔥 Выживание — собрано ${collectedItems} 🍎, время ${seconds}с`;
} else if (isMega) {
statusDisplay.innerHTML = `🗺️ Мега — босс имеет ${bossHP} ❤️`;
} else if (!exitVisible) {
statusDisplay.innerHTML = `🍎 Соберите все предметы (${collectedItems}/${totalItems === Infinity ? '∞' : totalItems})`;
} else {
statusDisplay.innerHTML = '🔑 Выход открыт! Идите к 🚪';
}
}
render();
}
function handleExit() {
if (gameOver) return;
if (confirm('Вы уверены, что хотите выйти из игры? (прогресс будет потерян)')) {
resetGame();
}
}
function goToNextDifficulty() {
const currentIndex = MAIN_CYCLE.indexOf(currentDifficulty);
if (currentIndex === -1) {
const nextDiff = MAIN_CYCLE[0];
if (unlocked[nextDiff]) {
nextDiffButton.style.display = 'none';
setDifficulty(nextDiff);
}
return;
}
const nextIndex = (currentIndex + 1) % MAIN_CYCLE.length;
const nextDiff = MAIN_CYCLE[nextIndex];
if (unlocked[nextDiff]) {
nextDiffButton.style.display = 'none';
setDifficulty(nextDiff);
} else {
for (let i = 0; i < MAIN_CYCLE.length; i++) {
const diff = MAIN_CYCLE[i];
if (unlocked[diff]) {
nextDiffButton.style.display = 'none';
setDifficulty(diff);
return;
}
}
nextDiffButton.style.display = 'none';
}
}
function resetGame() {
stopTimer();
stopEnemy();
timerStarted = false;
isPaused = false;
isShopOpen = false;
shopOverlay.classList.remove('active');
nextDiffButton.style.display = 'none';
if (shieldRespawnTimeout) clearTimeout(shieldRespawnTimeout);
shieldRespawnTimeout = null;
shield = null;
if (yellowFlashTimeout) clearTimeout(yellowFlashTimeout);
yellowFlash = false;
if (redFlashTimeout) clearTimeout(redFlashTimeout);
redFlash = false;
if (window.explosionZoneTimeout) clearTimeout(window.explosionZoneTimeout);
explosionZone = null;
fieldItems = [];
doubleStepActive = false;
pickaxeActive = false;
if (lightningTimer) clearTimeout(lightningTimer);
if (pickaxeTimer) clearTimeout(pickaxeTimer);
const diff = DIFFICULTY[currentDifficulty];
currentSize = diff.size;
cellSize = canvas.width / currentSize;
isSurvival = diff.survival;
isMega = diff.mega;
lives = 3;
livesCounter.textContent = lives;
applesSinceLastLife = 0;
appleProgress.textContent = `0/${APPLES_PER_LIFE}`;
inventory.cake = 0;
inventory.clock = 0;
inventory.lightning = 0;
inventory.pickaxe = 0;
updateInventoryDisplay();
generateMaze(currentSize, diff.extraPassages, isMega);
spawnEnemies(currentSize, isSurvival, isMega);
playerX = 1;
playerY = 1;
exitX = currentSize - 2;
exitY = currentSize - 2;
gameOver = false;
gameWon = false;
exitVisible = false;
steps = 0;
seconds = 0;
updateStepDisplay();
timeCounter.textContent = '0';
placeItemsAndTraps(currentSize, diff.itemCount, diff.trapCount, isSurvival, isMega);
if (isSurvival) {
spawnShield();
}
if (isMega) {
bossHP = MAX_BOSS_HP;
hasSword = false;
sword = null;
spawnSword();
bomb = null;
bombSpawnTimer = 3;
} else {
bossHP = 10;
hasSword = false;
sword = null;
bomb = null;
bombSpawnTimer = 3;
}
statusDisplay.innerHTML = isSurvival ? '🔥 Выживание — собирайте яблоки, избегайте врагов!' :
isMega ? `🗺️ Мега — убейте босса (${bossHP} ❤️)` :
'🚀 Игра <span>активна</span>';
updateModeInstructions();
render();
const interval = getEnemyInterval(currentDifficulty);
startEnemy(interval);
if (!timerStarted) {
startTimer();
timerStarted = true;
}
}
// --- Магазин ---
function openShop() {
if (gameOver) return;
isShopOpen = true;
shopOverlay.classList.add('active');
if (!isPaused) {
isPaused = true;
statusDisplay.innerHTML = '🛒 Магазин открыт';
}
renderShopItems();
render();
}
function closeShop() {
isShopOpen = false;
shopOverlay.classList.remove('active');
if (isPaused && !gameOver) {
isPaused = false;
if (isSurvival) {
statusDisplay.innerHTML = `🔥 Выживание — собрано ${collectedItems} 🍎, время ${seconds}с`;
} else if (isMega) {
statusDisplay.innerHTML = `🗺️ Мега — босс имеет ${bossHP} ❤️`;
} else if (!exitVisible) {
statusDisplay.innerHTML = `🍎 Соберите все предметы (${collectedItems}/${totalItems === Infinity ? '∞' : totalItems})`;
} else {
statusDisplay.innerHTML = '🔑 Выход открыт! Идите к 🚪';
}
}
render();
}
function renderShopItems() {
shopItemsContainer.innerHTML = '';
SHOP_ITEMS.forEach(item => {
const div = document.createElement('div');
div.className = 'shop-item';
const canAfford = starsBalance >= item.price;
div.innerHTML = `
<div class="icon">${item.icon}</div>
<div class="info">
<div class="name">${item.name}</div>
<div class="desc">${item.desc}</div>
</div>
<div class="price">⭐${item.price}</div>
<button class="buy-btn" data-id="${item.id}" data-price="${item.price}" ${!canAfford ? 'disabled' : ''}>
Купить
</button>
`;
shopItemsContainer.appendChild(div);
});
document.querySelectorAll('.shop-item .buy-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const id = btn.dataset.id;
const price = parseInt(btn.dataset.price);
buyItem(id, price);
});
});
}
function buyItem(id, price) {
if (starsBalance < price) {
alert('Недостаточно звёзд!');
return;
}
starsBalance -= price;
updateStarsDisplay();
const itemKey = SHOP_ITEMS.find(i => i.id === id).key;
inventory[itemKey]++;
updateInventoryDisplay();
renderShopItems();
const itemName = SHOP_ITEMS.find(i => i.id === id).name;
statusDisplay.innerHTML = `🛒 Куплен ${itemName}! (осталось ${starsBalance} ⭐)`;
setTimeout(() => {
if (!gameOver && !isShopOpen) {
if (isSurvival) {
statusDisplay.innerHTML = `🔥 Выживание — собрано ${collectedItems} 🍎, время ${seconds}с`;
} else if (isMega) {
statusDisplay.innerHTML = `🗺️ Мега — босс имеет ${bossHP} ❤️`;
} else if (!exitVisible) {
statusDisplay.innerHTML = `🍎 Соберите все предметы (${collectedItems}/${totalItems === Infinity ? '∞' : totalItems})`;
} else {
statusDisplay.innerHTML = '🔑 Выход открыт! Идите к 🚪';
}
}
}, 1500);
}
// --- Отрисовка ---
function render() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (let row = 0; row < currentSize; row++) {
for (let col = 0; col < currentSize; col++) {
const x = col * cellSize;
const y = row * cellSize;
if (maze[row][col]) {
if (isMega) {
const center = Math.floor(currentSize / 2);
const halfBlock = 2;
const isInBlock = (Math.abs(row - center) <= halfBlock && Math.abs(col - center) <= halfBlock);
if (isInBlock) {
ctx.fillStyle = '#b8d4e8';
ctx.fillRect(x, y, cellSize, cellSize);
ctx.fillStyle = '#9bbcd4';
ctx.fillRect(x + 1, y + 1, cellSize - 2, cellSize - 2);
continue;
}
}
ctx.fillStyle = '#2f4a5c';
ctx.fillRect(x, y, cellSize, cellSize);
ctx.fillStyle = '#3a5a6e';
ctx.fillRect(x + 2, y + 2, cellSize - 4, cellSize - 4);
ctx.fillStyle = '#1f3340';
ctx.fillRect(x + 4, y + 4, cellSize - 8, cellSize - 8);
} else {
ctx.fillStyle = '#b8d4e8';
ctx.fillRect(x, y, cellSize, cellSize);
ctx.fillStyle = '#9bbcd4';
ctx.fillRect(x + 1, y + 1, cellSize - 2, cellSize - 2);
}
}
}
if (isMega && bossHP <= 5 && bossHP > 0 && !gameOver) {
ctx.fillStyle = 'rgba(255, 0, 0, 0.15)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
if (explosionZone) {
const ex = explosionZone.x;
const ey = explosionZone.y;
const r = (bossHP <= 5 && bossHP > 0) ? 3 : 2;
const minX = Math.max(0, ex - r) * cellSize;
const maxX = Math.min(currentSize - 1, ex + r) * cellSize + cellSize;
const minY = Math.max(0, ey - r) * cellSize;
const maxY = Math.min(currentSize - 1, ey + r) * cellSize + cellSize;
ctx.fillStyle = 'rgba(255, 0, 0, 0.35)';
ctx.fillRect(minX, minY, maxX - minX, maxY - minY);
}
if (isMega) {
const center = Math.floor(currentSize / 2);
const halfBlock = 2;
const emojiX = center * cellSize + cellSize - cellSize / 2;
const emojiY = center * cellSize + cellSize;
ctx.font = `${5 * cellSize * 0.9}px sans-serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.shadowColor = 'rgba(0,0,0,0.5)';
ctx.shadowBlur = 10;
ctx.fillText('👺', emojiX, emojiY);
ctx.shadowBlur = 0;
ctx.font = `18px sans-serif`;
ctx.fillStyle = '#ff6b6b';
ctx.textAlign = 'center';
ctx.textBaseline = 'bottom';
ctx.shadowColor = 'rgba(0,0,0,0.8)';
ctx.shadowBlur = 6;
ctx.fillText(`${bossHP} ❤️`, emojiX, emojiY - cellSize * 2 - 4);
ctx.shadowBlur = 0;
}
if (sword) {
const sx = sword.x * cellSize + cellSize / 2;
const sy = sword.y * cellSize + cellSize / 2;
ctx.font = `${cellSize * 0.8}px sans-serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.shadowColor = 'rgba(255,215,0,0.5)';
ctx.shadowBlur = 10;
ctx.fillText('🗡️', sx, sy);
ctx.shadowBlur = 0;
}
for (const fi of fieldItems) {
const sx = fi.x * cellSize + cellSize / 2;
const sy = fi.y * cellSize + cellSize / 2;
const icon = SHOP_ITEMS.find(i => i.key === fi.key).icon;
ctx.font = `${cellSize * 0.7}px sans-serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.shadowColor = 'rgba(255,215,0,0.5)';
ctx.shadowBlur = 8;
ctx.fillText(icon, sx, sy);
ctx.shadowBlur = 0;
}
const fontSize = Math.max(14, Math.min(28, cellSize * 0.8));
ctx.font = `${fontSize}px "Segoe UI Emoji", "Apple Color Emoji", system-ui, sans-serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
for (const item of items) {
const x = item.x * cellSize + cellSize / 2;
const y = item.y * cellSize + cellSize / 2;
ctx.shadowColor = 'rgba(0,0,0,0.3)';
ctx.shadowBlur = 8;
ctx.fillText('🍎', x, y + 1);
ctx.shadowBlur = 0;
}
for (const trap of traps) {
const x = trap.x * cellSize + cellSize / 2;
const y = trap.y * cellSize + cellSize / 2;
ctx.shadowColor = 'rgba(0,0,0,0.4)';
ctx.shadowBlur = 8;
ctx.fillText('💀', x, y + 1);
ctx.shadowBlur = 0;
}
if (shield) {
const sx = shield.x * cellSize;
const sy = shield.y * cellSize;
ctx.shadowColor = 'rgba(0,255,255,0.5)';
ctx.shadowBlur = 15;
const shieldSize = Math.max(24, Math.min(40, cellSize * 0.9));
ctx.font = `${shieldSize}px "Segoe UI Emoji", "Apple Color Emoji", system-ui, sans-serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('🛡️', sx + cellSize / 2, sy + cellSize / 2 + 1);
ctx.shadowBlur = 0;
}
for (const e of enemies) {
if (e.x === -1 || e.y === -1) continue;
if (e.x === playerX && e.y === playerY && gameOver) continue;
const ex = e.x * cellSize;
const ey = e.y * cellSize;
ctx.shadowColor = 'rgba(255,0,0,0.5)';
ctx.shadowBlur = 15;
const enemySize = Math.max(24, Math.min(40, cellSize * 0.9));
ctx.font = `${enemySize}px "Segoe UI Emoji", "Apple Color Emoji", system-ui, sans-serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('👾', ex + cellSize / 2, ey + cellSize / 2 + 1);
ctx.shadowBlur = 0;
}
if (!isSurvival && !isMega && !(playerX === exitX && playerY === exitY)) {
const ex = exitX * cellSize;
const ey = exitY * cellSize;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
if (exitVisible) {
ctx.shadowColor = 'rgba(0,0,0,0.3)';
ctx.shadowBlur = 10;
const doorSize = Math.max(24, Math.min(40, cellSize * 1.1));
ctx.font = `${doorSize}px "Segoe UI Emoji", "Apple Color Emoji", system-ui, sans-serif`;
ctx.fillText('🚪', ex + cellSize / 2, ey + cellSize / 2 + 1);
ctx.shadowBlur = 0;
} else {
const lockSize = Math.max(20, Math.min(36, cellSize));
ctx.font = `${lockSize}px "Segoe UI Emoji", "Apple Color Emoji", system-ui, sans-serif`;
ctx.fillStyle = '#5a7a8a';
ctx.fillText('🔒', ex + cellSize / 2, ey + cellSize / 2 + 1);
}
}
const px = playerX * cellSize;
const py = playerY * cellSize;
ctx.shadowColor = 'rgba(0,0,0,0.4)';
ctx.shadowBlur = 12;
const playerSize = Math.max(24, Math.min(40, cellSize * 0.9));
ctx.font = `${playerSize}px "Segoe UI Emoji", "Apple Color Emoji", system-ui, sans-serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('🦧', px + cellSize / 2, py + cellSize / 2 + 1);
ctx.shadowBlur = 0;
if (pickaxeActive) {
const offsets = [
[0, -1], [-1, 0], [1, 0], [0, 1]
];
let pickX = playerX;
let pickY = playerY;
for (const [dx, dy] of offsets) {
const nx = playerX + dx;
const ny = playerY + dy;
if (nx >= 0 && nx < currentSize && ny >= 0 && ny < currentSize && isWalkable(nx, ny)) {
const center = Math.floor(currentSize / 2);
const halfBlock = 2;
if (Math.abs(nx - center) <= halfBlock && Math.abs(ny - center) <= halfBlock) continue;
pickX = nx;
pickY = ny;
break;
}
}
if (pickX !== playerX || pickY !== playerY) {
const sx = pickX * cellSize + cellSize / 2;
const sy = pickY * cellSize + cellSize / 2;
ctx.font = `${cellSize * 0.7}px sans-serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.shadowColor = 'rgba(255,215,0,0.5)';
ctx.shadowBlur = 8;
ctx.fillText('⛏️', sx, sy);
ctx.shadowBlur = 0;
}
}
if (bomb) {
const bx = bomb.x * cellSize + cellSize / 2;
const by = bomb.y * cellSize + cellSize / 2;
ctx.font = `${cellSize * 0.8}px sans-serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.shadowColor = 'rgba(255,0,0,0.5)';
ctx.shadowBlur = 10;
ctx.fillText('💣', bx, by);
ctx.shadowBlur = 0;
}
if (hasSword) {
const offsets = [
[0, -1], [-1, 0], [1, 0], [0, 1]
];
let swordX = playerX;
let swordY = playerY;
for (const [dx, dy] of offsets) {
const nx = playerX + dx;
const ny = playerY + dy;
if (nx >= 0 && nx < currentSize && ny >= 0 && ny < currentSize && isWalkable(nx, ny)) {
const center = Math.floor(currentSize / 2);
const halfBlock = 2;
if (Math.abs(nx - center) <= halfBlock && Math.abs(ny - center) <= halfBlock) continue;
swordX = nx;
swordY = ny;
break;
}
}
if (swordX !== playerX || swordY !== playerY) {
const sx = swordX * cellSize + cellSize / 2;
const sy = swordY * cellSize + cellSize / 2;
ctx.font = `${cellSize * 0.8}px sans-serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.shadowColor = 'rgba(255,215,0,0.5)';
ctx.shadowBlur = 10;
ctx.fillText('🗡️', sx, sy);
ctx.shadowBlur = 0;
} else {
const sx = px + cellSize / 2;
const sy = py + cellSize / 2 - cellSize * 0.2;
ctx.font = `${cellSize * 0.7}px sans-serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.shadowColor = 'rgba(255,215,0,0.5)';
ctx.shadowBlur = 8;
ctx.fillText('🗡️', sx, sy);
ctx.shadowBlur = 0;
}
}
if (redFlash) {
ctx.fillStyle = 'rgba(255, 0, 0, 0.35)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
if (yellowFlash) {
ctx.fillStyle = 'rgba(255, 255, 0, 0.3)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
if (isPaused && !gameOver && !isShopOpen) {
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.font = 'bold 48px "Segoe UI", sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle = '#f7d44a';
ctx.shadowColor = 'rgba(0,0,0,0.8)';
ctx.shadowBlur = 20;
ctx.fillText('⏸ ПАУЗА', canvas.width / 2, canvas.height / 2);
ctx.shadowBlur = 0;
}
if (gameOver) {
ctx.fillStyle = 'rgba(0,0,0,0.3)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.font = 'bold 32px "Segoe UI", sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.shadowColor = 'rgba(0,0,0,0.8)';
ctx.shadowBlur = 20;
if (gameWon) {
ctx.fillStyle = '#f7d44a';
ctx.fillText('🏆 ПОБЕДА!', canvas.width / 2, canvas.height / 2 - 30);
const starSize = 30;
const totalStars = 3;
const starSpacing = 45;
const startX = canvas.width / 2 - ((totalStars - 1) * starSpacing) / 2;
const starY = canvas.height / 2 + 30;
for (let i = 0; i < totalStars; i++) {
if (i < lives) {
ctx.fillStyle = '#f7d44a';
ctx.shadowColor = 'rgba(247, 212, 74, 0.8)';
ctx.shadowBlur = 15;
} else {
ctx.fillStyle = '#555';
ctx.shadowBlur = 0;
}
ctx.font = `${starSize}px sans-serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('★', startX + i * starSpacing, starY);
}
ctx.shadowBlur = 0;
} else {
ctx.fillStyle = '#ff6b6b';
ctx.fillText('💀 ПОРАЖЕНИЕ', canvas.width / 2, canvas.height / 2);
}
ctx.shadowBlur = 0;
}
if (!gameOver && !isPaused && !isShopOpen) {
if (isSurvival) {
statusDisplay.innerHTML = `🔥 Выживание — собрано ${collectedItems} 🍎, время ${seconds}с`;
} else if (isMega) {
statusDisplay.innerHTML = `🗺️ Мега — босс имеет ${bossHP} ❤️`;
} else {
if (!exitVisible) {
statusDisplay.innerHTML = `🍎 Соберите все предметы (${collectedItems}/${totalItems})`;
} else {
statusDisplay.innerHTML = '🔑 Выход открыт! Идите к 🚪';
}
}
}
}
// --- Обработчики для кнопок-стрелок ---
function setupArrowButtons() {
const arrows = [
{ el: arrowUp, dx: 0, dy: -1 },
{ el: arrowDown, dx: 0, dy: 1 },
{ el: arrowLeft, dx: -1, dy: 0 },
{ el: arrowRight, dx: 1, dy: 0 }
];
arrows.forEach(({ el, dx, dy }) => {
if (!el) return;
const handler = (e) => {
e.preventDefault();
movePlayer(dx, dy);
};
el.addEventListener('click', handler);
el.addEventListener('touchstart', handler, { passive: false });
});
}
// --- Обработчики для слотов ---
function setupSlotClicks() {
document.querySelectorAll('.slot-item').forEach(el => {
el.addEventListener('click', () => {
const key = el.dataset.key;
activateItem(key);
});
});
}
// --- Управление с клавиатуры ---
function handleKeydown(e) {
const key = e.key;
if (key === 'F1') {
e.preventDefault();
inventory.lightning = 999;
updateInventoryDisplay();
return;
}
if (key === 'F2') {
e.preventDefault();
inventory.pickaxe = 999;
updateInventoryDisplay();
return;
}
if (key === 'F3') {
e.preventDefault();
inventory.cake = 999;
updateInventoryDisplay();
return;
}
if (key === 'F4') {
e.preventDefault();
inventory.clock = 999;
updateInventoryDisplay();
return;
}
const controlKeys = [
'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight',
'w', 'W', 'A', 'a', 's', 'S', 'd', 'D',
'q', 'Q', 'й', 'Й',
'e', 'E', 'у', 'У',
'Shift', ' ', '1', '2', '3', '4'
];
if (key === '6' || key === '7') {
e.preventDefault();
if (key === '6') {
secret.pressed6 = true;
secret.timestamp = Date.now();
} else if (key === '7' && secret.pressed6) {
const now = Date.now();
if (now - secret.timestamp < 2000) {
unlocked.easy = true;
unlocked.medium = true;
unlocked.hard = true;
unlocked.mega = true;
unlocked.survival = true;
updateDifficultyButtons();
statusDisplay.innerHTML = '🔓 Все уровни разблокированы!';
setTimeout(() => {
if (!gameOver) {
statusDisplay.innerHTML = '🚀 Игра активна';
}
}, 2000);
}
secret.pressed6 = false;
}
return;
}
if (key === 'e' || key === 'E' || key === 'у' || key === 'У') {
e.preventDefault();
if (isShopOpen) {
closeShop();
} else {
openShop();
}
return;
}
if (key === '1') { e.preventDefault(); activateItem('lightning'); return; }
if (key === '2') { e.preventDefault(); activateItem('pickaxe'); return; }
if (key === '3') { e.preventDefault(); activateItem('cake'); return; }
if (key === '4') { e.preventDefault(); activateItem('clock'); return; }
if (controlKeys.includes(key) || key === ' ') {
e.preventDefault();
}
if (key === 'q' || key === 'Q' || key === 'й' || key === 'Й') {
togglePause();
return;
}
if (key === 'Shift') {
resetGame();
return;
}
if (key === ' ' && gameWon === true) {
goToNextDifficulty();
return;
}
if (gameOver || isPaused || isShopOpen) return;
switch (key) {
case 'ArrowUp':
case 'w':
case 'W':
movePlayer(0, -1);
break;
case 'ArrowDown':
case 's':
case 'S':
movePlayer(0, 1);
break;
case 'ArrowLeft':
case 'a':
case 'A':
movePlayer(-1, 0);
break;
case 'ArrowRight':
case 'd':
case 'D':
movePlayer(1, 0);
break;
default:
return;
}
}
function setDifficulty(diffKey) {
if (!unlocked[diffKey]) {
return;
}
if (diffKey === currentDifficulty) return;
currentDifficulty = diffKey;
diffButtons.forEach(btn => {
btn.classList.remove('active');
if (btn.dataset.diff === diffKey) btn.classList.add('active');
});
resetGame();
}
// --- Инициализация ---
window.addEventListener('load', () => {
diffButtons.forEach(btn => {
btn.addEventListener('click', () => {
const diff = btn.dataset.diff;
setDifficulty(diff);
});
});
nextDiffButton.addEventListener('click', goToNextDifficulty);
pauseButton.addEventListener('click', togglePause);
shopButton.addEventListener('click', openShop);
shopCloseBtn.addEventListener('click', closeShop);
shopOverlay.addEventListener('click', (e) => {
if (e.target === shopOverlay) {
closeShop();
}
});
setupArrowButtons();
setupSlotClicks();
updateInventoryDisplay();
updateStarsDisplay();
updateDifficultyButtons();
currentDifficulty = 'easy';
const diff = DIFFICULTY['easy'];
currentSize = diff.size;
cellSize = canvas.width / currentSize;
isSurvival = false;
isMega = false;
lives = 3;
livesCounter.textContent = lives;
applesSinceLastLife = 0;
appleProgress.textContent = `0/${APPLES_PER_LIFE}`;
appleBalance = 0;
generateMaze(currentSize, diff.extraPassages, false);
spawnEnemies(currentSize, false, false);
playerX = 1;
playerY = 1;
exitX = currentSize - 2;
exitY = currentSize - 2;
gameOver = false;
gameWon = false;
exitVisible = false;
steps = 0;
seconds = 0;
timerStarted = false;
updateStepDisplay();
timeCounter.textContent = '0';
placeItemsAndTraps(currentSize, diff.itemCount, diff.trapCount, false, false);
statusDisplay.innerHTML = '🚀 Игра <span>активна</span>';
updateModeInstructions();
render();
startEnemy(getEnemyInterval(currentDifficulty));
window.addEventListener('keydown', handleKeydown);
document.getElementById('resetButton').addEventListener('click', resetGame);
});
window.addEventListener('click', () => {});
</script>
</body>
</html>Game Source: Лабиринт играть онлайн
Creator: WildFalcon36
Libraries: none
Complexity: complex (2931 lines, 111.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-wildfalcon36" to link back to the original. Then publish at arcadelab.ai/publish.