Загрузка данных
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Космический Полёт</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource/fredoka@5.0.5/index.min.css">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
user-select: none;
-webkit-user-select: none;
touch-action: none;
}
body {
background-color: #050510;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
font-family: 'Fredoka', sans-serif;
overflow: hidden;
}
#game-container {
position: relative;
width: 100%;
max-width: 800px;
aspect-ratio: 4 / 3;
background: #000;
border-radius: 24px;
overflow: hidden;
box-shadow: 0 0 60px rgba(59, 130, 246, 0.3), 0 0 120px rgba(239, 68, 68, 0.1);
}
canvas {
display: block;
width: 100%;
height: 100%;
}
.ui-layer {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background: rgba(15, 12, 41, 0.85);
backdrop-filter: blur(8px);
z-index: 10;
transition: opacity 0.3s ease;
}
.ui-layer.hidden {
opacity: 0;
pointer-events: none;
}
h1 {
font-size: clamp(2.5rem, 8vw, 4.5rem);
font-weight: 900;
background: linear-gradient(135deg, #ef4444 0%, #3b82f6 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 1rem;
text-align: center;
text-shadow: 0 4px 20px rgba(239, 68, 68, 0.3);
letter-spacing: 2px;
}
p.subtitle {
color: #cbd5e1;
font-size: clamp(1rem, 3vw, 1.25rem);
margin-bottom: 2.5rem;
text-align: center;
max-width: 80%;
line-height: 1.5;
}
.btn {
background: linear-gradient(135deg, #ef4444 0%, #3b82f6 100%);
color: white;
font-family: 'Fredoka', sans-serif;
font-size: clamp(1.2rem, 4vw, 1.5rem);
font-weight: 700;
padding: 16px 48px;
border: none;
border-radius: 50px;
cursor: pointer;
box-shadow: 0 8px 25px rgba(239, 68, 68, 0.4), 0 4px 10px rgba(59, 130, 246, 0.3);
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
text-transform: uppercase;
letter-spacing: 1px;
}
.btn:hover {
transform: translateY(-3px) scale(1.05);
box-shadow: 0 12px 35px rgba(239, 68, 68, 0.5), 0 6px 15px rgba(59, 130, 246, 0.4);
}
.btn:active {
transform: translateY(1px) scale(0.98);
}
#countdown-display {
font-size: clamp(5rem, 15vw, 8rem);
font-weight: 900;
color: #fbbf24;
text-shadow: 0 0 40px rgba(251, 191, 36, 0.6);
animation: popIn 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
}
@keyframes popIn {
0% { transform: scale(0.5); opacity: 0; }
100% { transform: scale(1); opacity: 1; }
}
#score-hud {
position: absolute;
top: 24px;
left: 50%;
transform: translateX(-50%);
font-size: clamp(2rem, 6vw, 3.5rem);
font-weight: 900;
color: white;
text-shadow: 0 4px 10px rgba(0, 0, 0, 0.5);
z-index: 5;
pointer-events: none;
}
.score-label {
font-size: 1rem;
color: #94a3b8;
text-align: center;
margin-bottom: 0.5rem;
text-transform: uppercase;
letter-spacing: 2px;
}
.final-score {
font-size: clamp(3rem, 10vw, 5rem);
font-weight: 900;
color: #fbbf24;
margin-bottom: 2rem;
text-shadow: 0 0 30px rgba(251, 191, 36, 0.4);
}
</style>
</head>
<body>
<div id="game-container">
<canvas id="gameCanvas" width="800" height="600"></canvas>
<div id="score-hud" class="hidden">0</div>
<div id="start-screen" class="ui-layer">
<h1>КОСМИЧЕСКИЙ<br>ПОЛЁТ</h1>
<p class="subtitle">Нажимай ПРОБЕЛ или кликай по экрану,<br>чтобы управлять кораблём и избегать колонн!</p>
<button class="btn" id="start-btn">Начать игру</button>
</div>
<div id="countdown-screen" class="ui-layer hidden">
<div id="countdown-display">3</div>
</div>
<div id="gameover-screen" class="ui-layer hidden">
<h1>ИГРА ОКОНЧЕНА</h1>
<div class="score-label">Твой счёт</div>
<div class="final-score" id="final-score">0</div>
<button class="btn" id="restart-btn">Начать заново</button>
</div>
</div>
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const startScreen = document.getElementById('start-screen');
const countdownScreen = document.getElementById('countdown-screen');
const countdownDisplay = document.getElementById('countdown-display');
const gameoverScreen = document.getElementById('gameover-screen');
const scoreHud = document.getElementById('score-hud');
const finalScoreDisplay = document.getElementById('final-score');
const startBtn = document.getElementById('start-btn');
const restartBtn = document.getElementById('restart-btn');
let gameState = 'MENU'; // MENU, COUNTDOWN, PLAYING, GAMEOVER
let score = 0;
let frames = 0;
let animationId;
// Game Constants
const GRAVITY = 0.25;
const JUMP_STRENGTH = -6.5;
const OBSTACLE_SPEED = 3.5;
const OBSTACLE_WIDTH = 70;
const OBSTACLE_GAP = 170;
const OBSTACLE_SPAWN_RATE = 110;
class Bird {
constructor() {
this.x = 150;
this.y = canvas.height / 2;
this.velocity = 0;
this.radius = 22;
this.angle = 0;
this.wingOffset = 0;
}
update() {
this.velocity += GRAVITY;
this.y += this.velocity;
// Rotation based on velocity
this.angle = Math.min(Math.PI / 4, Math.max(-Math.PI / 4, (this.velocity * 0.08)));
// Wing animation
this.wingOffset = Math.sin(frames * 0.3) * 5;
}
jump() {
this.velocity = JUMP_STRENGTH;
}
draw(ctx) {
ctx.save();
ctx.translate(this.x, this.y);
ctx.rotate(this.angle);
// Glow effect
ctx.shadowColor = 'rgba(59, 130, 246, 0.6)';
ctx.shadowBlur = 20;
// Body (Red-Blue gradient)
const grad = ctx.createLinearGradient(-this.radius, -this.radius, this.radius, this.radius);
grad.addColorStop(0, '#ef4444');
grad.addColorStop(0.5, '#a855f7');
grad.addColorStop(1, '#3b82f6');
ctx.beginPath();
ctx.arc(0, 0, this.radius, 0, Math.PI * 2);
ctx.fillStyle = grad;
ctx.fill();
ctx.shadowBlur = 0; // Reset shadow for details
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 2.5;
ctx.stroke();
// Wing
ctx.beginPath();
ctx.ellipse(-8, 6 + this.wingOffset, 14, 9, -0.3, 0, Math.PI * 2);
ctx.fillStyle = '#fbbf24';
ctx.fill();
ctx.strokeStyle = '#f59e0b';
ctx.lineWidth = 2;
ctx.stroke();
// Eyes (White)
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.arc(8, -7, 8, 0, Math.PI * 2);
ctx.arc(8, 7, 8, 0, Math.PI * 2);
ctx.fill();
// Pupils (Black)
ctx.fillStyle = '#0f172a';
ctx.beginPath();
ctx.arc(10, -7, 3.5, 0, Math.PI * 2);
ctx.arc(10, 7, 3.5, 0, Math.PI * 2);
ctx.fill();
// Beak
ctx.beginPath();
ctx.moveTo(16, -2);
ctx.lineTo(28, 2);
ctx.lineTo(16, 6);
ctx.closePath();
ctx.fillStyle = '#f97316';
ctx.fill();
ctx.strokeStyle = '#c2410c';
ctx.lineWidth = 1.5;
ctx.stroke();
ctx.restore();
}
}
class Obstacle {
constructor(x) {
this.x = x;
this.width = OBSTACLE_WIDTH;
this.gap = OBSTACLE_GAP;
this.minHeight = 60;
this.maxHeight = canvas.height - this.gap - this.minHeight;
this.topHeight = Math.random() * (this.maxHeight - this.minHeight) + this.minHeight;
this.bottomY = this.topHeight + this.gap;
this.passed = false;
}
update() {
this.x -= OBSTACLE_SPEED;
}
draw(ctx) {
const drawPillar = (x, y, w, h, isTop) => {
// Main stone body
ctx.fillStyle = '#6b7280';
ctx.fillRect(x, y, w, h);
// 3D Highlight (left)
ctx.fillStyle = '#9ca3af';
ctx.fillRect(x, y, 12, h);
// 3D Shadow (right)
ctx.fillStyle = '#4b5563';
ctx.fillRect(x + w - 12, y, 12, h);
// Cap
const capHeight = 24;
const capY = isTop ? y + h - capHeight : y;
ctx.fillStyle = '#6b7280';
ctx.fillRect(x - 6, capY, w + 12, capHeight);
// Cap highlight/shadow
ctx.fillStyle = '#9ca3af';
ctx.fillRect(x - 6, capY, 12, capHeight);
ctx.fillStyle = '#4b5563';
ctx.fillRect(x + w - 6, capY, 12, capHeight);
// Stone details (cracks/lines)
ctx.strokeStyle = '#374151';
ctx.lineWidth = 2;
ctx.beginPath();
if (isTop) {
ctx.moveTo(x + 20, y + 30);
ctx.lineTo(x + 40, y + 50);
ctx.moveTo(x + 25, y + 80);
ctx.lineTo(x + 50, y + 90);
} else {
ctx.moveTo(x + 30, y + 40);
ctx.lineTo(x + 45, y + 60);
ctx.moveTo(x + 20, y + 100);
ctx.lineTo(x + 35, y + 110);
}
ctx.stroke();
};
drawPillar(this.x, 0, this.width, this.topHeight, true);
drawPillar(this.x, this.bottomY, this.width, canvas.height - this.bottomY, false);
}
}
class Star {
constructor() {
this.x = Math.random() * canvas.width;
this.y = Math.random() * canvas.height;
this.size = Math.random() * 2.5 + 0.5;
this.speed = Math.random() * 0.8 + 0.2;
this.opacity = Math.random() * 0.8 + 0.2;
this.twinkleSpeed = Math.random() * 0.05 + 0.01;
this.twinkleDir = 1;
}
update() {
this.x -= this.speed;
if (this.x < 0) {
this.x = canvas.width;
this.y = Math.random() * canvas.height;
}
this.opacity += this.twinkleSpeed * this.twinkleDir;
if (this.opacity >= 1 || this.opacity <= 0.2) {
this.twinkleDir *= -1;
}
}
draw(ctx) {
ctx.fillStyle = `rgba(255, 255, 255, ${this.opacity})`;
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fill();
}
}
let bird;
let obstacles = [];
let stars = [];
function init() {
bird = new Bird();
obstacles = [];
stars = Array.from({ length: 120 }, () => new Star());
score = 0;
frames = 0;
scoreHud.textContent = '0';
}
function checkCollision(circle, rectX, rectY, rectW, rectH) {
let testX = circle.x;
let testY = circle.y;
if (circle.x < rectX) testX = rectX;
else if (circle.x > rectX + rectW) testX = rectX + rectW;
if (circle.y < rectY) testY = rectY;
else if (circle.y > rectY + rectH) testY = rectY + rectH;
const distX = circle.x - testX;
const distY = circle.y - testY;
const distance = Math.sqrt((distX * distX) + (distY * distY));
return distance <= circle.radius - 2; // -2 for slight forgiveness
}
function gameOver() {
gameState = 'GAMEOVER';
finalScoreDisplay.textContent = score;
scoreHud.classList.add('hidden');
gameoverScreen.classList.remove('hidden');
}
function startCountdown() {
gameState = 'COUNTDOWN';
startScreen.classList.add('hidden');
countdownScreen.classList.remove('hidden');
let count = 3;
countdownDisplay.textContent = count;
// Reset animation
countdownDisplay.style.animation = 'none';
countdownDisplay.offsetHeight; // Trigger reflow
countdownDisplay.style.animation = 'popIn 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards';
const timer = setInterval(() => {
count--;
if (count > 0) {
countdownDisplay.textContent = count;
countdownDisplay.style.animation = 'none';
countdownDisplay.offsetHeight;
countdownDisplay.style.animation = 'popIn 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards';
} else if (count === 0) {
countdownDisplay.textContent = 'ВПЕРЁД!';
countdownDisplay.style.color = '#22c55e';
countdownDisplay.style.animation = 'none';
countdownDisplay.offsetHeight;
countdownDisplay.style.animation = 'popIn 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards';
} else {
clearInterval(timer);
countdownScreen.classList.add('hidden');
countdownDisplay.style.color = '#fbbf24'; // Reset color
gameState = 'PLAYING';
scoreHud.classList.remove('hidden');
bird.jump(); // Initial jump
}
}, 800);
}
function drawBackground() {
// Deep space gradient
const grad = ctx.createLinearGradient(0, 0, 0, canvas.height);
grad.addColorStop(0, '#0f0c29');
grad.addColorStop(0.5, '#302b63');
grad.addColorStop(1, '#24243e');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Distant nebula effect
const nebula = ctx.createRadialGradient(canvas.width * 0.8, canvas.height * 0.2, 0, canvas.width * 0.8, canvas.height * 0.2, 300);
nebula.addColorStop(0, 'rgba(239, 68, 68, 0.15)');
nebula.addColorStop(1, 'rgba(0, 0, 0, 0)');
ctx.fillStyle = nebula;
ctx.fillRect(0, 0, canvas.width, canvas.height);
const nebula2 = ctx.createRadialGradient(canvas.width * 0.2, canvas.height * 0.8, 0, canvas.width * 0.2, canvas.height * 0.8, 250);
nebula2.addColorStop(0, 'rgba(59, 130, 246, 0.15)');
nebula2.addColorStop(1, 'rgba(0, 0, 0, 0)');
ctx.fillStyle = nebula2;
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
function gameLoop() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawBackground();
// Update and draw stars (always animate for visual appeal)
stars.forEach(star => {
star.update();
star.draw(ctx);
});
if (gameState === 'PLAYING') {
bird.update();
// Spawn obstacles
if (frames % OBSTACLE_SPAWN_RATE === 0) {
obstacles.push(new Obstacle(canvas.width));
}
// Update obstacles
for (let i = obstacles.length - 1; i >= 0; i--) {
const obs = obstacles[i];
obs.update();
// Score counting
if (!obs.passed && obs.x + obs.width < bird.x) {
obs.passed = true;
score++;
scoreHud.textContent = score;
}
// Remove off-screen obstacles
if (obs.x + obs.width < 0) {
obstacles.splice(i, 1);
continue;
}
// Collision detection
if (checkCollision(bird, obs.x, 0, obs.width, obs.topHeight) ||
checkCollision(bird, obs.x, obs.bottomY, obs.width, canvas.height - obs.bottomY)) {
gameOver();
}
}
// Floor/Ceiling collision
if (bird.y + bird.radius >= canvas.height || bird.y - bird.radius <= 0) {
gameOver();
}
frames++;
} else if (gameState === 'MENU' || gameState === 'GAMEOVER') {
// Idle animation for bird in menu
bird.y = (canvas.height / 2) + Math.sin(Date.now() * 0.003) * 15;
bird.wingOffset = Math.sin(Date.now() * 0.01) * 5;
}
// Draw obstacles
obstacles.forEach(obs => obs.draw(ctx));
// Draw bird
bird.draw(ctx);
animationId = requestAnimationFrame(gameLoop);
}
function handleInput(e) {
if (e.type === 'keydown' && e.code !== 'Space') return;
if (e.type === 'keydown') e.preventDefault(); // Prevent scrolling
if (gameState === 'MENU') {
startCountdown();
} else if (gameState === 'PLAYING') {
bird.jump();
} else if (gameState === 'GAMEOVER') {
init();
gameState = 'MENU';
gameoverScreen.classList.add('hidden');
startScreen.classList.remove('hidden');
}
}
// Event Listeners
window.addEventListener('keydown', handleInput);
canvas.addEventListener('mousedown', handleInput);
canvas.addEventListener('touchstart', (e) => {
e.preventDefault();
handleInput(e);
}, { passive: false });
startBtn.addEventListener('click', (e) => {
e.stopPropagation();
startCountdown();
});
restartBtn.addEventListener('click', (e) => {
e.stopPropagation();
init();
gameState = 'MENU';
gameoverScreen.classList.add('hidden');
startScreen.classList.remove('hidden');
});
// Initialize and start loop
init();
gameLoop();
</script>
</body>
</html>