CUBE 31 | Бесконечный раннер
by MegaCaptain34512 lines20.3 KB
<!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>CUBE 31 | Бесконечный раннер</title>
<style>
* {
user-select: none;
-webkit-tap-highlight-color: transparent;
}
body {
margin: 0;
min-height: 100vh;
background: linear-gradient(145deg, #0a0f1e 0%, #0c1222 100%);
display: flex;
justify-content: center;
align-items: center;
font-family: 'Segoe UI', 'Courier New', 'Monaco', monospace;
touch-action: manipulation;
}
.game-container {
background: #03060c;
padding: 20px 20px 24px;
border-radius: 64px 64px 48px 48px;
box-shadow: 0 25px 40px rgba(0,0,0,0.5), inset 0 1px 0 rgba(255,255,255,0.08);
}
canvas {
display: block;
margin: 0 auto;
border-radius: 28px;
box-shadow: 0 10px 25px rgba(0,0,0,0.5), inset 0 0 0 2px rgba(255,255,255,0.05);
cursor: pointer;
background-color: #101624;
}
.info-panel {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-top: 18px;
margin-bottom: 12px;
padding: 0 20px;
}
.score-box {
background: #010101aa;
backdrop-filter: blur(4px);
padding: 8px 18px;
border-radius: 60px;
font-weight: bold;
font-size: 1.7rem;
color: #f5e56b;
text-shadow: 0 0 5px #ffb347;
letter-spacing: 1px;
font-family: monospace;
}
.best-box {
background: #1e1f2caa;
backdrop-filter: blur(4px);
padding: 8px 18px;
border-radius: 60px;
font-weight: bold;
font-size: 1.2rem;
color: #b9c6db;
}
.controls {
display: flex;
justify-content: center;
gap: 35px;
margin-top: 20px;
}
button {
background: #1e293b;
border: none;
font-size: 2rem;
font-weight: bold;
padding: 10px 28px;
border-radius: 60px;
color: white;
font-family: monospace;
cursor: pointer;
box-shadow: 0 5px 0 #0f111a;
transition: 0.07s linear;
touch-action: manipulation;
}
button:active {
transform: translateY(3px);
box-shadow: 0 2px 0 #0f111a;
}
.restart-btn {
background: #4a2f2f;
font-size: 1.2rem;
padding: 8px 24px;
letter-spacing: 1px;
}
.status {
background: #00000066;
border-radius: 40px;
padding: 6px 18px;
font-size: 0.9rem;
font-weight: bold;
text-align: center;
color: #ffd966;
}
@media (max-width: 650px) {
.game-container {
padding: 12px 12px 18px;
}
button {
padding: 6px 22px;
font-size: 1.6rem;
}
.score-box {
font-size: 1.3rem;
padding: 4px 14px;
}
}
</style>
</head>
<body>
<div>
<div class="game-container">
<canvas id="gameCanvas" width="800" height="500"></canvas>
<div class="info-panel">
<div class="score-box">СЧЕТ: <span id="scoreValue">0</span></div>
<div class="best-box">🏆 РЕКОРД: <span id="bestValue">0</span></div>
<div class="status" id="gameStatusText">⚡ В ПУТИ</div>
</div>
<div class="controls">
<button id="leftBtn">◀ ЛЕВО</button>
<button id="rightBtn">ПРАВО ▶</button>
</div>
<div style="text-align: center; margin-top: 15px;">
<button id="restartButton" class="restart-btn">🔄 НОВАЯ ИГРА</button>
</div>
<div style="text-align: center; margin-top: 12px; font-size: 12px; color: #6c7a91;">
← → или A/D / пальцем по кнопкам | Уклоняйся от красных, бери ЖЕЛТЫЕ кубы
</div>
</div>
</div>
<script>
(function(){
// ---------- CANVAS ----------
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
// ---------- ИГРОВЫЕ ПАРАМЕТРЫ ----------
const LANE_COUNT = 5;
const LANE_WIDTH = canvas.width / LANE_COUNT; // 800/5 = 160
const PLAYER_SIZE = 48; // стильный куб 48x48
// игрок (изначально центральная полоса 2 индекс)
let playerLane = 2; // 0..4
let gameRunning = true;
let score = 0;
let bestScore = 0;
// загружаем рекорд из localStorage
try {
const saved = localStorage.getItem('cube31_best');
if(saved && !isNaN(parseInt(saved))) bestScore = parseInt(saved);
} catch(e) { /* тишина */ }
document.getElementById('bestValue').innerText = bestScore;
// ---------- ОБЪЕКТЫ МИРА (препятствия + монеты) ----------
let obstacles = []; // красные враги: { x, y, width, height, lane }
let coins = []; // жёлтые монеты: { x, y, width, height, lane }
// Базовые настройки генерации
let frameCounter = 0;
let baseSpeed = 3.8; // пикселей за кадр
let speed = baseSpeed;
let generationGap = 38; // через сколько фреймов спавнить новый объект
let lastGenFrame = 0;
// параметры сложности (скорость растёт со счётом)
let speedMultiplier = 1.0;
// размер препятствий/монет
const OBST_SIZE = 44;
const COIN_SIZE = 36;
// ---------- HELPERS ----------
function updateUI(){
document.getElementById('scoreValue').innerText = Math.floor(score);
if(score > bestScore){
bestScore = Math.floor(score);
document.getElementById('bestValue').innerText = bestScore;
try { localStorage.setItem('cube31_best', bestScore); } catch(e) {}
}
}
// спавн нового объекта (либо монета либо препятствие)
function spawnObject(){
if(!gameRunning) return;
// выбираем случайную полосу 0..4
const lane = Math.floor(Math.random() * LANE_COUNT);
const objType = Math.random();
// чем выше счёт, тем больше препятствий (но сохраняем баланс ~ 45% монет)
let obstacleChance = 0.48 + (Math.min(score, 400) / 1500);
if(obstacleChance > 0.72) obstacleChance = 0.72;
const isObstacle = objType < obstacleChance;
const x = lane * LANE_WIDTH + (LANE_WIDTH/2) - (isObstacle ? OBST_SIZE/2 : COIN_SIZE/2);
const y = - (isObstacle ? OBST_SIZE : COIN_SIZE);
if(isObstacle){
obstacles.push({
x: x,
y: y,
width: OBST_SIZE,
height: OBST_SIZE,
lane: lane
});
} else {
coins.push({
x: x,
y: y,
width: COIN_SIZE,
height: COIN_SIZE,
lane: lane
});
}
}
// обновление позиций и коллизий
function updateGame(){
if(!gameRunning) return;
// динамическая скорость (каждые 50 очков +5% скорости)
speedMultiplier = 1.0 + (Math.floor(score / 55) * 0.025);
if(speedMultiplier > 1.85) speedMultiplier = 1.85;
let currentSpeed = baseSpeed * speedMultiplier;
// ----- перемещаем препятствия -----
for(let i=0; i<obstacles.length; i++){
obstacles[i].y += currentSpeed;
}
// ----- перемещаем монеты -----
for(let i=0; i<coins.length; i++){
coins[i].y += currentSpeed;
}
// удаляем ушедшие за нижнюю границу объекты
obstacles = obstacles.filter(obs => obs.y < canvas.height + 100);
coins = coins.filter(coin => coin.y < canvas.height + 100);
// ----- СПАВН объектов (динамический интервал) -----
// чем выше скорость, тем чаще спавн, но не чаще чем 22 кадра
let dynamicGap = Math.max(24, Math.floor(generationGap - (Math.min(score,350)/45)));
if(dynamicGap < 20) dynamicGap = 20;
if(frameCounter - lastGenFrame >= dynamicGap){
spawnObject();
// иногда двойной спавн для насыщенности (эффект "куб в движении")
if(score > 80 && Math.random() < 0.33 && gameRunning){
spawnObject();
}
lastGenFrame = frameCounter;
}
// ----- КОЛЛИЗИЯ ИГРОКА (прямоугольник куба)-----
const playerX = playerLane * LANE_WIDTH + (LANE_WIDTH/2) - PLAYER_SIZE/2;
const playerY = canvas.height - 70; // низкая позиция, классика раннера
const playerRect = {
x: playerX,
y: playerY,
w: PLAYER_SIZE,
h: PLAYER_SIZE
};
// проверка препятствий (столкновение = смерть)
for(let i=0; i<obstacles.length; i++){
const obs = obstacles[i];
const obsRect = { x: obs.x, y: obs.y, w: obs.width, h: obs.height };
if(playerRect.x < obsRect.x + obsRect.w &&
playerRect.x + playerRect.w > obsRect.x &&
playerRect.y < obsRect.y + obsRect.h &&
playerRect.y + playerRect.h > obsRect.y){
gameRunning = false;
document.getElementById('gameStatusText').innerHTML = '💀 ГЕЙМ ОВЕР 💀';
document.getElementById('gameStatusText').style.color = "#ff8866";
return;
}
}
// сбор монет (увеличение счета)
for(let i=0; i<coins.length; i++){
const coin = coins[i];
const coinRect = { x: coin.x, y: coin.y, w: coin.width, h: coin.height };
if(playerRect.x < coinRect.x + coinRect.w &&
playerRect.x + playerRect.w > coinRect.x &&
playerRect.y < coinRect.y + coinRect.h &&
playerRect.y + playerRect.h > coinRect.y){
// собрали монету
coins.splice(i,1);
score += 10;
updateUI();
i--; // скорректировать индекс
}
}
// обновляем UI счета каждые кадры
updateUI();
// добавим пассивный прирост за выживание (каждый 70 кадров +1)
if(frameCounter % 70 === 0 && gameRunning){
score += 1;
updateUI();
}
}
// ---------- ОТРИСОВКА СТИЛЬНОГО КУБА И МИРА ----------
function draw(){
ctx.clearRect(0, 0, canvas.width, canvas.height);
// ---- СЕТКА дорожек (кибер-стиль) ----
for(let i=0; i<=LANE_COUNT; i++){
ctx.beginPath();
ctx.strokeStyle = "#2a3850";
ctx.lineWidth = 2.5;
ctx.moveTo(i*LANE_WIDTH, 0);
ctx.lineTo(i*LANE_WIDTH, canvas.height);
ctx.stroke();
}
// ---- рисуем препятствия (КРАСНЫЙ АГРЕССИВНЫЙ КУБ) ----
for(let obs of obstacles){
// градиент красный
const grad = ctx.createLinearGradient(obs.x, obs.y, obs.x+obs.width, obs.y+obs.height);
grad.addColorStop(0, '#e34d4d');
grad.addColorStop(1, '#b91c1c');
ctx.fillStyle = grad;
ctx.shadowBlur = 12;
ctx.shadowColor = "#ff0000aa";
ctx.fillRect(obs.x, obs.y, obs.width, obs.height);
ctx.fillStyle = "#ffffff";
ctx.font = `bold ${Math.floor(obs.width*0.5)}px monospace`;
ctx.shadowBlur = 2;
ctx.fillStyle = "black";
ctx.fillText("⚠️", obs.x+obs.width*0.25, obs.y+obs.height*0.7);
}
// ---- рисуем монеты (ЗОЛОТОЙ КУБ 31 стиль)----
for(let coin of coins){
ctx.shadowBlur = 12;
ctx.shadowColor = "#ffcc44";
ctx.fillStyle = "#f5b642";
ctx.fillRect(coin.x, coin.y, coin.width, coin.height);
ctx.fillStyle = "#ffea80";
ctx.fillRect(coin.x+6, coin.y+6, coin.width-12, coin.height-12);
ctx.fillStyle = "#d48f2b";
ctx.font = `bold ${Math.floor(coin.width*0.6)}px monospace`;
ctx.fillStyle = "#ffd966";
ctx.shadowBlur = 3;
ctx.fillText("◆", coin.x+coin.width*0.25, coin.y+coin.height*0.75);
}
// ---- ИГРОК: сияющий КУБ, который катится (эффект движения)----
const playerX = playerLane * LANE_WIDTH + (LANE_WIDTH/2) - PLAYER_SIZE/2;
const playerY = canvas.height - 70;
// куб с неоновой обводкой
ctx.shadowBlur = 14;
ctx.shadowColor = "#3b82f6";
const gradPlayer = ctx.createLinearGradient(playerX, playerY, playerX+PLAYER_SIZE, playerY+PLAYER_SIZE);
gradPlayer.addColorStop(0, "#4e9eff");
gradPlayer.addColorStop(1, "#1e40af");
ctx.fillStyle = gradPlayer;
ctx.fillRect(playerX, playerY, PLAYER_SIZE, PLAYER_SIZE);
ctx.strokeStyle = "#caf0ff";
ctx.lineWidth = 3;
ctx.strokeRect(playerX+2, playerY+2, PLAYER_SIZE-4, PLAYER_SIZE-4);
// лицо куба (дерзкие глаза)
ctx.fillStyle = "white";
ctx.fillRect(playerX+12, playerY+14, 8, 8);
ctx.fillRect(playerX+28, playerY+14, 8, 8);
ctx.fillStyle = "#020617";
ctx.fillRect(playerX+14, playerY+16, 5, 5);
ctx.fillRect(playerX+30, playerY+16, 5, 5);
ctx.beginPath();
ctx.arc(playerX+24, playerY+32, 8, 0, Math.PI, false);
ctx.strokeStyle = "#ffffff";
ctx.lineWidth = 2;
ctx.stroke();
// ---- динамический текст скорости/счета на поле----
ctx.font = "bold 15monospace";
ctx.fillStyle = "#b9d0ff";
ctx.shadowBlur = 0;
ctx.fillText(`⚡ SPEED: ${(speedMultiplier*100).toFixed(0)}%`, 20, 45);
ctx.font = "bold 18monospace";
ctx.fillStyle = "#fcda5b";
ctx.fillText(`SCORE: ${Math.floor(score)}`, canvas.width-130, 45);
// если игра окончена, затемнение
if(!gameRunning){
ctx.fillStyle = "rgba(0,0,0,0.75)";
ctx.fillRect(0,0,canvas.width,canvas.height);
ctx.font = "900 36monospace";
ctx.fillStyle = "#ffaa77";
ctx.shadowBlur = 6;
ctx.fillText("☠️ GAME OVER ☠️", canvas.width/2-150, canvas.height/2-30);
ctx.font = "18monospace";
ctx.fillStyle = "#ccccdd";
ctx.fillText("НАЖМИ → НОВАЯ ИГРА", canvas.width/2-110, canvas.height/2+40);
}
ctx.shadowBlur = 0;
}
// ---------- УПРАВЛЕНИЕ (клавиши + кнопки) ----------
function moveLeft(){
if(!gameRunning) return;
if(playerLane > 0){
playerLane--;
}
}
function moveRight(){
if(!gameRunning) return;
if(playerLane < LANE_COUNT-1){
playerLane++;
}
}
// рестарт игры
function restartGame(){
gameRunning = true;
score = 0;
speedMultiplier = 1.0;
obstacles = [];
coins = [];
playerLane = 2;
frameCounter = 0;
lastGenFrame = 0;
updateUI();
document.getElementById('gameStatusText').innerHTML = '⚡ В ПУТИ';
document.getElementById('gameStatusText').style.color = "#ffd966";
// начальный спавн нескольких монет для динамики
for(let i=0;i<2;i++) spawnObject();
// небольшой тайминг, чтобы не было мгновенной смерти
}
// ---------- АНИМАЦИОННЫЙ ЦИКЛ ----------
let animationId = null;
function gameLoop(){
if(gameRunning){
updateGame();
}
draw();
frameCounter++;
animationId = requestAnimationFrame(gameLoop);
}
// ----- ПОДКЛЮЧЕНИЕ СОБЫТИЙ -----
function initControls(){
const leftBtn = document.getElementById('leftBtn');
const rightBtn = document.getElementById('rightBtn');
const restartBtn = document.getElementById('restartButton');
leftBtn.addEventListener('click', (e) => {
e.preventDefault();
moveLeft();
});
rightBtn.addEventListener('click', (e) => {
e.preventDefault();
moveRight();
});
restartBtn.addEventListener('click', (e) => {
restartGame();
});
window.addEventListener('keydown', (e) => {
const key = e.key;
if(key === 'ArrowLeft' || key === 'a' || key === 'A'){
e.preventDefault();
moveLeft();
} else if(key === 'ArrowRight' || key === 'd' || key === 'D'){
e.preventDefault();
moveRight();
} else if(key === ' ' || key === 'Space' || key === 'r' || key === 'R'){
if(!gameRunning){
e.preventDefault();
restartGame();
}
}
});
// для мобильных тач-событий на кнопках - уже работает
}
// Инициализация игры + спавн стартовых монет
function startGame(){
restartGame(); // начальное состояние живое, с обнулением
initControls();
gameLoop();
}
startGame();
})();
</script>
</body>
</html>Game Source: CUBE 31 | Бесконечный раннер
Creator: MegaCaptain34
Libraries: none
Complexity: complex (512 lines, 20.3 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: cube-31-megacaptain34-mpk7s54d" to link back to the original. Then publish at arcadelab.ai/publish.