diff --git a/index.html b/index.html new file mode 100644 index 0000000..480d363 --- /dev/null +++ b/index.html @@ -0,0 +1,27 @@ + + + + + + Space Jump + + + +
+
0
+
Нажми ПРОБЕЛ, чтобы начать
+ +
+
+ +
+

Игра окончена

+

Счёт: 0

+

Рекорд: 0

+

Пробел — заново

+
+
+ + + + diff --git a/script.js b/script.js new file mode 100644 index 0000000..3e9894a --- /dev/null +++ b/script.js @@ -0,0 +1,134 @@ +const game = document.getElementById("game"); +const player = document.getElementById("player"); +const scoreEl = document.getElementById("score"); +const hintEl = document.getElementById("hint"); +const gameOverEl = document.getElementById("gameOver"); +const finalScoreEl = document.getElementById("finalScore"); +const bestScoreEl = document.getElementById("bestScore"); + +const GROUND = 50; // высота земли +const GRAVITY = 0.55; +const JUMP_FORCE = 12; +const START_SPEED = 5; + +let running = false; +let gameOver = false; +let playerY = 0; // высота над землёй +let velocity = 0; +let speed = START_SPEED; +let score = 0; +let best = Number(localStorage.getItem("spaceJumpBest")) || 0; +let obstacles = []; +let spawnTimer = 0; +let lastTime = 0; + +function startGame() { + obstacles.forEach((o) => o.el.remove()); + obstacles = []; + playerY = 0; + velocity = 0; + speed = START_SPEED; + score = 0; + spawnTimer = 0; + running = true; + gameOver = false; + hintEl.style.display = "none"; + gameOverEl.classList.remove("visible"); + scoreEl.textContent = "0"; + lastTime = performance.now(); + requestAnimationFrame(loop); +} + +function jump() { + if (playerY === 0) { + velocity = JUMP_FORCE; + } +} + +function spawnObstacle() { + const el = document.createElement("div"); + el.className = "obstacle"; + const height = 30 + Math.random() * 35; + el.style.height = height + "px"; + game.appendChild(el); + obstacles.push({ el, x: game.clientWidth, height }); +} + +function endGame() { + running = false; + gameOver = true; + best = Math.max(best, score); + localStorage.setItem("spaceJumpBest", best); + finalScoreEl.textContent = score; + bestScoreEl.textContent = best; + gameOverEl.classList.add("visible"); +} + +function loop(now) { + if (!running) return; + const dt = Math.min((now - lastTime) / 16.67, 3); // нормализация к 60 FPS + lastTime = now; + + // физика игрока + velocity -= GRAVITY * dt; + playerY += velocity * dt; + if (playerY <= 0) { + playerY = 0; + velocity = 0; + } + player.style.bottom = GROUND + playerY + "px"; + + // спавн препятствий + spawnTimer -= dt; + if (spawnTimer <= 0) { + spawnObstacle(); + spawnTimer = 60 + Math.random() * 70; + } + + // движение препятствий и столкновения + const playerLeft = 80; + const playerRight = playerLeft + 36; + + for (let i = obstacles.length - 1; i >= 0; i--) { + const o = obstacles[i]; + o.x -= speed * dt; + o.el.style.left = o.x + "px"; + + if (o.x + 22 < 0) { + o.el.remove(); + obstacles.splice(i, 1); + score++; + scoreEl.textContent = score; + speed += 0.15; // чуть быстрее с каждым очком + continue; + } + + const hitX = o.x < playerRight - 6 && o.x + 22 > playerLeft + 6; + const hitY = playerY < o.height - 4; + if (hitX && hitY) { + endGame(); + return; + } + } + + requestAnimationFrame(loop); +} + +document.addEventListener("keydown", (e) => { + if (e.code !== "Space") return; + e.preventDefault(); + if (!running) { + startGame(); + } else { + jump(); + } +}); + +// поддержка тапа/клика для мобильных +game.addEventListener("pointerdown", () => { + if (!running) { + startGame(); + } else { + jump(); + } +}); diff --git a/style.css b/style.css new file mode 100644 index 0000000..2327a47 --- /dev/null +++ b/style.css @@ -0,0 +1,130 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + background: #12121c; + font-family: "Segoe UI", system-ui, sans-serif; +} + +.game { + position: relative; + width: 800px; + max-width: 95vw; + height: 300px; + overflow: hidden; + border-radius: 12px; + background: linear-gradient(#1b2340, #2c3e66 70%, #3a5080); + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5); +} + +.score { + position: absolute; + top: 14px; + right: 20px; + font-size: 28px; + font-weight: bold; + color: #fff; + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.4); + z-index: 5; +} + +.hint { + position: absolute; + top: 40%; + width: 100%; + text-align: center; + color: rgba(255, 255, 255, 0.85); + font-size: 20px; + letter-spacing: 1px; + z-index: 5; + animation: pulse 1.2s ease-in-out infinite; +} + +@keyframes pulse { + 0%, 100% { opacity: 0.5; } + 50% { opacity: 1; } +} + +.ground { + position: absolute; + bottom: 0; + width: 100%; + height: 50px; + background: #263238; + border-top: 4px solid #4caf7d; +} + +.player { + position: absolute; + left: 80px; + bottom: 50px; + width: 36px; + height: 36px; + border-radius: 8px; + background: #ffca3a; + box-shadow: 0 0 12px rgba(255, 202, 58, 0.6); + z-index: 3; +} + +.player::before, +.player::after { + content: ""; + position: absolute; + top: 9px; + width: 6px; + height: 6px; + border-radius: 50%; + background: #12121c; +} + +.player::before { left: 7px; } +.player::after { right: 7px; } + +.obstacle { + position: absolute; + bottom: 50px; + width: 22px; + border-radius: 4px 4px 0 0; + background: #e5484d; + box-shadow: 0 0 10px rgba(229, 72, 77, 0.5); + z-index: 2; +} + +.game-over { + position: absolute; + inset: 0; + display: none; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 8px; + background: rgba(10, 10, 20, 0.75); + color: #fff; + z-index: 10; +} + +.game-over.visible { + display: flex; +} + +.game-over h1 { + font-size: 34px; + margin-bottom: 6px; +} + +.game-over p { + font-size: 18px; + color: rgba(255, 255, 255, 0.9); +} + +.restart-hint { + margin-top: 10px; + animation: pulse 1.2s ease-in-out infinite; +}