Cosmo Runner 3D
by BlazePanther14495 lines20.9 KB🛠️ Three.js (3D graphics)
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Cosmo Runner 3D</title>
<style>
* { margin:0; padding:0; box-sizing:border-box; -webkit-tap-highlight-color:transparent; }
html, body { height:100%; overflow:hidden; background:#87ceeb; font-family:-apple-system,system-ui,sans-serif; touch-action:none; user-select:none; }
#canvas3d { position:absolute; inset:0; display:block; }
#overlay { position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center; text-align:center; color:#fff; background:rgba(3,4,15,0.86); padding:24px; z-index:10; }
.screen { display:flex; flex-direction:column; align-items:center; }
.screen h1 { font-size:30px; margin-bottom:6px; letter-spacing:1px; color:#ffd23f; text-shadow:0 0 12px rgba(255,210,63,0.6); }
.screen p { font-size:14px; opacity:0.85; margin-bottom:14px; max-width:300px; line-height:1.5; }
.score { font-size:18px; margin-bottom:16px; color:#5ce1ff; }
button { font-size:17px; font-weight:700; padding:13px 32px; border:none; border-radius:30px; background:#ffd23f; color:#3a2a00; box-shadow:0 6px 0 #c99f1e; cursor:pointer; margin:6px 0; }
button:active { transform:translateY(4px); box-shadow:0 2px 0 #c99f1e; }
button.secondary { background:transparent; color:#5ce1ff; box-shadow:none; border:2px solid #5ce1ff; padding:10px 26px; font-size:14px; }
#leaderboardList { list-style:none; width:280px; margin-bottom:16px; max-height:260px; overflow-y:auto; }
#leaderboardList li { display:flex; justify-content:space-between; padding:7px 10px; background:rgba(255,255,255,0.06); border-radius:8px; margin-bottom:5px; font-size:14px; }
#leaderboardList li .rank { color:#ffd23f; font-weight:700; width:24px; }
#leaderboardList li .name { flex:1; text-align:left; padding-left:6px; }
#leaderboardList .empty { opacity:0.6; font-size:13px; padding:14px; }
#nameEntry { display:flex; gap:8px; margin-bottom:8px; }
#nameInput { font-size:16px; padding:10px 12px; border-radius:20px; border:2px solid #5ce1ff; background:rgba(255,255,255,0.08); color:#fff; width:150px; text-align:center; }
#hud { position:absolute; top:14px; left:0; right:0; text-align:center; color:#fff; font-weight:700; font-size:26px; text-shadow:0 2px 6px rgba(0,0,0,0.5); z-index:5; pointer-events:none; }
#muteBtn { position:absolute; top:12px; right:12px; z-index:6; width:40px; height:40px; border-radius:50%; padding:0; margin:0; background:rgba(255,255,255,0.18); color:#fff; box-shadow:none; font-size:18px; display:flex; align-items:center; justify-content:center; }
#hint { position:absolute; bottom:6px; left:0; right:0; text-align:center; color:rgba(255,255,255,0.75); font-size:11px; z-index:5; text-shadow:0 1px 3px rgba(0,0,0,0.5); }
</style>
</head>
<body>
<canvas id="canvas3d"></canvas>
<div id="hud">0</div>
<button id="muteBtn">🔊</button>
<div id="hint">Свайп влево/вправо — смена полосы • вверх — прыжок • вниз — подкат</div>
<div id="overlay">
<div id="startScreen" class="screen">
<h1>COSMO RUNNER</h1>
<p>Беги по трём полосам, перепрыгивай ящики, подкатывайся под балки и уворачивайся от стен. Свайпы на телефоне, стрелки/WASD на компьютере.</p>
<div class="score" id="hi">Рекорд: 0</div>
<button id="startBtn">Играть</button>
<button id="showLeaderboardBtn" class="secondary">Таблица лидеров</button>
</div>
<div id="gameOverScreen" class="screen" style="display:none">
<h1>СТОЛКНОВЕНИЕ!</h1>
<p id="goStats">Счёт: 0</p>
<div id="nameEntry" style="display:none">
<input id="nameInput" maxlength="12" placeholder="Твоё имя">
</div>
<button id="saveScoreBtn" style="display:none">Сохранить результат</button>
<button id="restartBtn">Заново</button>
<button id="goLeaderboardBtn" class="secondary">Таблица лидеров</button>
</div>
<div id="leaderboardScreen" class="screen" style="display:none">
<h1>ТОП-10</h1>
<ol id="leaderboardList"></ol>
<button id="backBtn">Назад</button>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
(function(){
// ---------- UI refs ----------
const overlay = document.getElementById('overlay');
const startScreen = document.getElementById('startScreen');
const gameOverScreen = document.getElementById('gameOverScreen');
const leaderboardScreen = document.getElementById('leaderboardScreen');
const startBtn = document.getElementById('startBtn');
const restartBtn = document.getElementById('restartBtn');
const showLeaderboardBtn = document.getElementById('showLeaderboardBtn');
const goLeaderboardBtn = document.getElementById('goLeaderboardBtn');
const backBtn = document.getElementById('backBtn');
const saveScoreBtn = document.getElementById('saveScoreBtn');
const nameEntry = document.getElementById('nameEntry');
const nameInput = document.getElementById('nameInput');
const hiEl = document.getElementById('hi');
const goStats = document.getElementById('goStats');
const leaderboardList = document.getElementById('leaderboardList');
const muteBtn = document.getElementById('muteBtn');
const hud = document.getElementById('hud');
// ---------- sound ----------
let audioCtx = null, muted = false;
try { muted = localStorage.getItem('runner_muted') === '1'; } catch(e){}
function updateMuteBtn(){ muteBtn.textContent = muted ? '🔇' : '🔊'; }
updateMuteBtn();
function ensureAudio(){
if (!audioCtx){ try { audioCtx = new (window.AudioContext||window.webkitAudioContext)(); } catch(e){ return; } }
if (audioCtx.state === 'suspended') audioCtx.resume();
}
muteBtn.addEventListener('click', ()=>{
muted = !muted;
try { localStorage.setItem('runner_muted', muted?'1':'0'); } catch(e){}
updateMuteBtn();
});
function tone(freq,dur,type,vol,glideTo){
if (muted || !audioCtx) return;
const t0=audioCtx.currentTime;
const osc=audioCtx.createOscillator(), gain=audioCtx.createGain();
osc.type=type||'square'; osc.frequency.setValueAtTime(freq,t0);
if (glideTo) osc.frequency.exponentialRampToValueAtTime(glideTo,t0+dur);
gain.gain.setValueAtTime(vol||0.12,t0);
gain.gain.exponentialRampToValueAtTime(0.001,t0+dur);
osc.connect(gain).connect(audioCtx.destination);
osc.start(t0); osc.stop(t0+dur);
}
function noiseBurst(dur,vol){
if (muted || !audioCtx) return;
const size=Math.floor(audioCtx.sampleRate*dur);
const buffer=audioCtx.createBuffer(1,size,audioCtx.sampleRate);
const data=buffer.getChannelData(0);
for (let i=0;i<size;i++){ data[i]=(Math.random()*2-1)*(1-i/size); }
const src=audioCtx.createBufferSource(); src.buffer=buffer;
const gain=audioCtx.createGain();
gain.gain.setValueAtTime(vol||0.2,audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001,audioCtx.currentTime+dur);
src.connect(gain).connect(audioCtx.destination); src.start();
}
const sfx = {
jump: ()=> tone(440,0.1,'square',0.08,660),
slide: ()=> tone(220,0.1,'square',0.07,150),
coin: ()=> { tone(880,0.06,'square',0.09,1200); setTimeout(()=>tone(1200,0.06,'square',0.08),50); },
crash: ()=> { noiseBurst(0.4,0.3); tone(90,0.5,'sawtooth',0.2,40); },
laneswitch: ()=> tone(330,0.05,'square',0.05)
};
// ---------- leaderboard ----------
function loadLB(){ try { return JSON.parse(localStorage.getItem('runner_leaderboard')||'[]'); } catch(e){ return []; } }
function saveLB(list){ try { localStorage.setItem('runner_leaderboard', JSON.stringify(list)); } catch(e){} }
function qualifies(score){ const l=loadLB(); return l.length<10 || score>l[l.length-1].score; }
function addScore(name,score){
const list=loadLB();
list.push({ name:(name||'Игрок').slice(0,12), score });
list.sort((a,b)=>b.score-a.score);
const trimmed=list.slice(0,10);
saveLB(trimmed);
return trimmed;
}
function escapeHtml(s){ return String(s).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
function renderLB(){
const list=loadLB();
leaderboardList.innerHTML='';
if (!list.length){ leaderboardList.innerHTML='<div class="empty">Пока нет результатов — беги первым!</div>'; return; }
list.forEach((e,i)=>{
const li=document.createElement('li');
li.innerHTML='<span class="rank">'+(i+1)+'</span><span class="name">'+escapeHtml(e.name)+'</span><span>'+e.score+'</span>';
leaderboardList.appendChild(li);
});
}
function loadBest(){ try { return parseInt(localStorage.getItem('runner_best')||'0',10); } catch(e){ return 0; } }
function saveBest(v){ try { localStorage.setItem('runner_best', String(v)); } catch(e){} }
let best = loadBest();
hiEl.textContent = 'Рекорд: ' + best;
function showScreen(name){
startScreen.style.display = name==='start' ? 'flex' : 'none';
gameOverScreen.style.display = name==='gameover' ? 'flex' : 'none';
leaderboardScreen.style.display = name==='leaderboard' ? 'flex' : 'none';
overlay.style.display = 'flex';
}
let returnToGameOver = false;
showLeaderboardBtn.addEventListener('click', ()=>{ returnToGameOver=false; renderLB(); showScreen('leaderboard'); });
goLeaderboardBtn.addEventListener('click', ()=>{ returnToGameOver=true; renderLB(); showScreen('leaderboard'); });
backBtn.addEventListener('click', ()=> showScreen(returnToGameOver?'gameover':'start'));
// ---------- three.js setup ----------
const canvas = document.getElementById('canvas3d');
const renderer = new THREE.WebGLRenderer({ canvas, antialias:true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio||1, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x8fd3ff);
scene.fog = new THREE.Fog(0x8fd3ff, 22, 78);
const camera = new THREE.PerspectiveCamera(62, window.innerWidth/window.innerHeight, 0.1, 200);
camera.position.set(0, 4.6, 7);
const hemi = new THREE.HemisphereLight(0xffffff, 0x445566, 0.9);
scene.add(hemi);
const sun = new THREE.DirectionalLight(0xffffff, 0.8);
sun.position.set(5, 10, 5);
scene.add(sun);
window.addEventListener('resize', ()=>{
camera.aspect = window.innerWidth/window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// ground with scrolling stripe texture
const stripeCanvas = document.createElement('canvas');
stripeCanvas.width = 64; stripeCanvas.height = 256;
const sctx = stripeCanvas.getContext('2d');
sctx.fillStyle = '#5a6270'; sctx.fillRect(0,0,64,256);
sctx.fillStyle = '#767f8f';
for (let y=0;y<256;y+=32){ sctx.fillRect(0,y,64,16); }
sctx.fillStyle = '#e8d24a';
sctx.fillRect(0,0,4,256); sctx.fillRect(60,0,4,256);
const groundTex = new THREE.CanvasTexture(stripeCanvas);
groundTex.wrapS = THREE.RepeatWrapping; groundTex.wrapT = THREE.RepeatWrapping;
groundTex.repeat.set(1, 60);
const groundMat = new THREE.MeshLambertMaterial({ map: groundTex });
const groundGeo = new THREE.PlaneGeometry(7, 320);
const ground = new THREE.Mesh(groundGeo, groundMat);
ground.rotation.x = -Math.PI/2;
ground.position.set(0, 0, -100);
scene.add(ground);
// side rails
const railMat = new THREE.MeshLambertMaterial({ color: 0x334155 });
[-3.7, 3.7].forEach(x=>{
const rail = new THREE.Mesh(new THREE.BoxGeometry(0.3, 0.6, 320), railMat);
rail.position.set(x, 0.3, -100);
scene.add(rail);
});
// ---------- player ----------
const LANE_X = [-1.8, 0, 1.8];
const player = {
lane: 1, x: 0, y: 0, vy: 0,
isJumping: false, isSliding: false, slideTime: 0,
alive: true
};
const GRAVITY = -32, JUMP_VELOCITY = 11.5, SLIDE_DURATION = 0.55;
const playerGroup = new THREE.Group();
const bodyMat = new THREE.MeshLambertMaterial({ color: 0xff6a3d });
const limbMat = new THREE.MeshLambertMaterial({ color: 0x3d5aff });
const headMat = new THREE.MeshLambertMaterial({ color: 0xffd7a8 });
const torso = new THREE.Mesh(new THREE.BoxGeometry(0.62, 0.8, 0.36), bodyMat);
torso.position.y = 0.95;
playerGroup.add(torso);
const head = new THREE.Mesh(new THREE.SphereGeometry(0.28, 12, 12), headMat);
head.position.y = 1.62;
playerGroup.add(head);
const legL = new THREE.Mesh(new THREE.BoxGeometry(0.24, 0.62, 0.24), limbMat);
legL.position.set(-0.16, 0.31, 0);
playerGroup.add(legL);
const legR = new THREE.Mesh(new THREE.BoxGeometry(0.24, 0.62, 0.24), limbMat);
legR.position.set(0.16, 0.31, 0);
playerGroup.add(legR);
const armL = new THREE.Mesh(new THREE.BoxGeometry(0.18, 0.6, 0.18), bodyMat);
armL.position.set(-0.44, 0.95, 0);
playerGroup.add(armL);
const armR = new THREE.Mesh(new THREE.BoxGeometry(0.18, 0.6, 0.18), bodyMat);
armR.position.set(0.44, 0.95, 0);
playerGroup.add(armR);
scene.add(playerGroup);
// ---------- obstacles & coins ----------
const obstacles = [];
const coins = [];
const SPAWN_Z = -75;
const REMOVE_Z = 6;
const crateGeo = new THREE.BoxGeometry(1.3, 1.05, 1.3);
const crateMat = new THREE.MeshLambertMaterial({ color: 0x8b5a2b });
const beamGeo = new THREE.BoxGeometry(1.55, 0.4, 0.6);
const beamMat = new THREE.MeshLambertMaterial({ color: 0x2e3a59 });
const wallGeo = new THREE.BoxGeometry(1.6, 2.3, 0.5);
const wallMat = new THREE.MeshLambertMaterial({ color: 0xd23a3a });
const coinGeo = new THREE.TorusGeometry(0.26, 0.09, 8, 16);
const coinMat = new THREE.MeshLambertMaterial({ color: 0xffd700 });
function makeObstacleMesh(type){
if (type === 'jump') { const m = new THREE.Mesh(crateGeo, crateMat); m.position.y = 0.52; return m; }
if (type === 'duck') { const m = new THREE.Mesh(beamGeo, beamMat); m.position.y = 1.75; return m; }
const m = new THREE.Mesh(wallGeo, wallMat); m.position.y = 1.15; return m;
}
function spawnPattern(){
const laneIdx = [0,1,2];
for (let i=laneIdx.length-1;i>0;i--){ const j=Math.floor(Math.random()*(i+1)); [laneIdx[i],laneIdx[j]]=[laneIdx[j],laneIdx[i]]; }
const numBlocked = Math.random() < 0.6 ? 1 : 2;
const blocked = laneIdx.slice(0, numBlocked);
const open = laneIdx.slice(numBlocked);
const type = ['jump','duck','wall'][Math.floor(Math.random()*3)];
for (const lane of blocked){
const mesh = makeObstacleMesh(type);
mesh.position.x = LANE_X[lane];
mesh.position.z = SPAWN_Z;
scene.add(mesh);
obstacles.push({ mesh, lane, type, resolved:false });
}
if (Math.random() < 0.75){
for (const lane of open){
for (let k=0;k<4;k++){
const c = new THREE.Mesh(coinGeo, coinMat);
c.rotation.x = Math.PI/2;
c.position.set(LANE_X[lane], 1.05, SPAWN_Z + k*1.7);
scene.add(c);
coins.push({ mesh:c, lane, collected:false });
}
}
}
}
// ---------- game state ----------
let running = false;
let distance = 0, score = 0, speed = 15;
const MAX_SPEED = 32;
let spawnAcc = 0, nextSpawnAt = 18;
function resetGame(){
for (const o of obstacles) scene.remove(o.mesh);
for (const c of coins) scene.remove(c.mesh);
obstacles.length = 0; coins.length = 0;
player.lane = 1; player.x = 0; player.y = 0; player.vy = 0;
player.isJumping = false; player.isSliding = false; player.slideTime = 0;
playerGroup.position.set(0,0,0);
playerGroup.scale.set(1,1,1);
distance = 0; score = 0; speed = 15;
spawnAcc = 0; nextSpawnAt = 18;
running = true;
hud.style.display = 'block';
hud.textContent = '0';
}
function tryJump(){
if (!running) return;
if (!player.isJumping && !player.isSliding){
player.isJumping = true; player.vy = JUMP_VELOCITY; sfx.jump();
}
}
function trySlide(){
if (!running) return;
if (!player.isJumping && !player.isSliding){
player.isSliding = true; player.slideTime = SLIDE_DURATION; sfx.slide();
}
}
function moveLane(dir){
if (!running) return;
const newLane = player.lane + dir;
if (newLane >= 0 && newLane <= 2){ player.lane = newLane; sfx.laneswitch(); }
}
// ---------- input ----------
let touchStartX=0, touchStartY=0, touchActive=false;
canvas.addEventListener('touchstart', (e)=>{
ensureAudio();
const t = e.changedTouches[0];
touchStartX = t.clientX; touchStartY = t.clientY; touchActive = true;
}, {passive:true});
canvas.addEventListener('touchend', (e)=>{
if (!touchActive) return;
touchActive = false;
const t = e.changedTouches[0];
const dx = t.clientX - touchStartX, dy = t.clientY - touchStartY;
if (Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > 32){
moveLane(dx > 0 ? 1 : -1);
} else if (dy < -32){
tryJump();
} else if (dy > 32){
trySlide();
}
}, {passive:true});
window.addEventListener('keydown', (e)=>{
ensureAudio();
if (e.code==='ArrowLeft'||e.code==='KeyA') moveLane(-1);
else if (e.code==='ArrowRight'||e.code==='KeyD') moveLane(1);
else if (e.code==='ArrowUp'||e.code==='KeyW'||e.code==='Space'){ e.preventDefault(); tryJump(); }
else if (e.code==='ArrowDown'||e.code==='KeyS') trySlide();
});
// ---------- update ----------
const clock = new THREE.Clock();
let runTime = 0;
function update(dt){
if (!running) return;
runTime += dt;
// difficulty
speed = Math.min(MAX_SPEED, 15 + distance*0.012);
distance += speed * dt;
score = Math.floor(distance);
hud.textContent = score;
// lane movement (smooth)
const targetX = LANE_X[player.lane];
player.x += (targetX - player.x) * Math.min(1, dt*10);
// jump physics
if (player.isJumping){
player.vy += GRAVITY * dt;
player.y += player.vy * dt;
if (player.y <= 0){ player.y = 0; player.isJumping = false; player.vy = 0; }
}
// slide timer
if (player.isSliding){
player.slideTime -= dt;
if (player.slideTime <= 0){ player.isSliding = false; }
}
playerGroup.position.x = player.x;
playerGroup.position.y = player.y;
playerGroup.scale.y = player.isSliding ? 0.55 : 1;
// running limb animation
const swing = Math.sin(runTime * 14) * 0.5;
legL.rotation.x = swing; legR.rotation.x = -swing;
armL.rotation.x = -swing; armR.rotation.x = swing;
// ground scroll
groundTex.offset.y -= speed * dt * 0.05;
// spawn
spawnAcc += speed * dt;
if (spawnAcc >= nextSpawnAt){
spawnAcc = 0; nextSpawnAt = 15 + Math.random()*8;
spawnPattern();
}
// move obstacles
for (const o of obstacles){
o.mesh.position.z += speed * dt;
if (!o.resolved && o.mesh.position.z > -1.0 && o.mesh.position.z < 1.0 && o.lane === player.lane){
let hit = false;
if (o.type === 'jump') hit = !(player.isJumping && player.y > 0.55);
else if (o.type === 'duck') hit = !player.isSliding;
else hit = true;
if (hit){ o.resolved = true; crash(); }
else o.resolved = true;
}
}
for (let i=obstacles.length-1;i>=0;i--){
if (obstacles[i].mesh.position.z > REMOVE_Z){ scene.remove(obstacles[i].mesh); obstacles.splice(i,1); }
}
// coins
for (const c of coins){
c.mesh.position.z += speed * dt;
c.mesh.rotation.z += dt * 5;
if (!c.collected && c.lane === player.lane && Math.abs(c.mesh.position.z) < 1.0){
c.collected = true;
score += 5;
scene.remove(c.mesh);
sfx.coin();
}
}
for (let i=coins.length-1;i>=0;i--){
if (coins[i].collected || coins[i].mesh.position.z > REMOVE_Z){
if (coins[i].mesh.parent) scene.remove(coins[i].mesh);
coins.splice(i,1);
}
}
// camera follow
camera.position.x += (player.x*0.5 - camera.position.x) * Math.min(1, dt*4);
camera.lookAt(player.x*0.3, 1.3, -8);
}
function crash(){
running = false;
sfx.crash();
if (score > best){ best = score; saveBest(best); }
hiEl.textContent = 'Рекорд: ' + best;
goStats.textContent = 'Счёт: ' + score;
hud.style.display = 'none';
if (qualifies(score)){
nameEntry.style.display = 'flex'; saveScoreBtn.style.display = 'inline-block'; nameInput.value='';
} else {
nameEntry.style.display = 'none'; saveScoreBtn.style.display = 'none';
}
showScreen('gameover');
}
saveScoreBtn.addEventListener('click', ()=>{
addScore(nameInput.value.trim(), score);
nameEntry.style.display='none'; saveScoreBtn.style.display='none';
renderLB(); returnToGameOver = true; showScreen('leaderboard');
});
startBtn.addEventListener('click', ()=>{ ensureAudio(); overlay.style.display='none'; resetGame(); });
restartBtn.addEventListener('click', ()=>{ ensureAudio(); overlay.style.display='none'; resetGame(); });
function animate(){
const dt = Math.min(0.05, clock.getDelta());
update(dt);
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
hud.style.display = 'none';
animate();
})();
</script>
</body>
</html>Game Source: Cosmo Runner 3D
Creator: BlazePanther14
Libraries: three
Complexity: complex (495 lines, 20.9 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: cosmo-runner-3d-blazepanther14" to link back to the original. Then publish at arcadelab.ai/publish.