<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>2D PARKOUR</title>
<style>
*{box-sizing:border-box}
body{margin:0;padding:15px;background:#101426;color:white;font-family:Arial,sans-serif}
.app{max-width:950px;margin:auto}
header{text-align:center;padding:12px}
h1{color:#70f5bd;font-size:36px;margin:8px}
header p{color:#aab5d0}
.panel{background:#1c2440;border:1px solid #303d60;border-radius:18px;padding:17px;margin-bottom:15px}
.stats{display:flex;gap:10px}
.stat{flex:1;background:#111a30;padding:12px;border-radius:12px;text-align:center}
.stat b{display:block;color:#70f5bd;font-size:23px;margin-top:5px}
.levels{display:grid;grid-template-columns:repeat(5,1fr);gap:10px}
button{font-family:inherit;cursor:pointer;touch-action:manipulation}
.level{border:0;border-radius:12px;padding:15px 5px;font-size:20px;font-weight:bold;min-height:65px;background:#303b5b;color:white}
.level.open{background:#187b57}
.level.done{background:#b17a24}
.level:disabled{opacity:.5;cursor:not-allowed}
#gameScreen{display:none}
.topbar{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:10px}
.action{background:#303e60;color:white;border:0;padding:12px 15px;border-radius:10px;font-weight:bold}
canvas{display:block;width:100%;height:auto;background:#8ed8ef;border-radius:14px;border:3px solid #43577b;touch-action:none}
.hint{text-align:center;color:#aab5d0;font-size:13px;margin:10px}
.controls{display:flex;justify-content:center;gap:15px;margin-top:12px}
.controls button{width:80px;height:60px;border:0;border-radius:14px;background:#34466d;color:white;font-size:27px;user-select:none;-webkit-user-select:none;touch-action:none}
.controls button:active{background:#20a875}
.overlay{position:fixed;inset:0;background:#080d20dd;display:none;align-items:center;justify-content:center;padding:20px;z-index:5}
.modal{background:#202c49;padding:28px;border-radius:20px;text-align:center;max-width:360px;width:100%;border:1px solid #50648d}
.modal h2{color:#70f5bd}
.primary{background:#20a875;color:white;border:0;padding:13px 22px;border-radius:12px;font-size:17px;font-weight:bold;margin:5px}
@media(max-width:500px){
.levels{grid-template-columns:repeat(4,1fr)}
h1{font-size:29px}
body{padding:9px}
}
</style>
</head>
<body>
<div class="app">
<header>
<h1>⚡ 2D PARKOUR</h1>
<p>20 уровней. Сможешь пройти все?</p>
</header>
<div id="menu">
<div class="panel">
<div class="stats">
<div class="stat">🏁 Открыто<b id="opened">1/20</b></div>
<div class="stat">🏆 Пройдено<b id="completed">0/20</b></div>
</div>
</div>
<div class="panel">
<h2>🗺️ Выбор уровня</h2>
<div class="levels" id="levels"></div>
</div>
</div>
<div id="gameScreen">
<div class="topbar">
<button class="action" id="menuBtn">← Карта</button>
<b id="levelTitle">Уровень 1</b>
<button class="action" id="restartBtn">↻ Заново</button>
</div>
<div class="panel" style="padding:8px">
<canvas id="canvas" width="900" height="500"></canvas>
<div class="hint">← → движение · ПРОБЕЛ — прыжок · R — заново</div>
<div class="controls">
<button id="left">◀</button>
<button id="jump">⬆</button>
<button id="right">▶</button>
</div>
</div>
</div>
</div>
<div class="overlay" id="win">
<div class="modal">
<h2>🏆 УРОВЕНЬ ПРОЙДЕН!</h2>
<p id="winText"></p>
<button class="primary" id="nextBtn">Следующий уровень →</button>
<button class="action" id="backBtn">К карте</button>
</div>
</div>
<script>
"use strict";
const $=id=>document.getElementById(id);
const canvas=$("canvas"),ctx=canvas.getContext("2d");
let unlocked=1,completed=[];
try{
unlocked=Math.max(1,Math.min(20,Number(localStorage.getItem("parkourUnlocked"))||1));
completed=JSON.parse(localStorage.getItem("parkourDone")||"[]");
if(!Array.isArray(completed))completed=[];
}catch(e){}
let level=1,platforms=[],spikes=[],world={width:2300,goal:2100};
let camera=0,playing=false,won=false,jumpQueued=false;
let keys={left:false,right:false};
let player={
x:45,y:382,w:34,h:48,
vx:0,vy:0,ground:false,
direction:1,anim:0
};
const themes=[
{sky1:"#74c8f0",sky2:"#d4f4fa",ground:"#26976d",top:"#76e5a3",spike:"#e74458",name:"🌳 Зелёный лес"},
{sky1:"#ed955b",sky2:"#ffd1a0",ground:"#9b472d",top:"#ffb05c",spike:"#fff04b",name:"🌋 Мир лавы"},
{sky1:"#80c9ef",sky2:"#edfaff",ground:"#5b9ec7",top:"#e1f8ff",spike:"#3883e8",name:"❄️ Ледяной мир"},
{sky1:"#32235e",sky2:"#7656a4",ground:"#63478d",top:"#c6a4ff",spike:"#ff65a5",name:"🌌 Космос"}
];
let theme=themes[0];
function save(){
try{
localStorage.setItem("parkourUnlocked",String(unlocked));
localStorage.setItem("parkourDone",JSON.stringify(completed));
}catch(e){}
}
function renderLevels(){
$("opened").textContent=unlocked+"/20";
$("completed").textContent=completed.length+"/20";
const box=$("levels");
box.innerHTML="";
for(let i=1;i<=20;i++){
const b=document.createElement("button");
b.className="level";
if(i<=unlocked)b.classList.add("open");
if(completed.includes(i))b.classList.add("done");
b.disabled=i>unlocked;
b.textContent=i>unlocked?"🔒":completed.includes(i)?"🏆 "+i:i;
b.onclick=()=>startLevel(i);
box.appendChild(b);
}
}
const layouts=[
[[400,65],[800,75],[1250,80]],
[[300,90],[650,65],[1050,100],[1500,70]],
[[450,70],[700,90],[1150,75],[1600,100]],
[[320,65],[600,65],[950,110],[1400,90],[1750,65]],
[[400,110],[750,70],[1100,100],[1500,110]],
[[300,80],[550,100],[900,100],[1250,120],[1650,100]],
[[400,100],[700,120],[1050,90],[1400,130],[1750,90]],
[[280,80],[550,130],[900,130],[1300,100],[1650,130]],
[[350,130],[700,120],[1100,140],[1500,130],[1850,100]],
[[280,120],[600,140],[950,140],[1350,150],[1750,130]],
[[350,110],[650,140],[1050,130],[1450,160],[1850,120]],
[[280,130],[600,150],[950,150],[1300,170],[1700,150]],
[[350,150],[700,160],[1100,150],[1500,170],[1850,140]],
[[280,160],[620,150],[980,170],[1370,160],[1750,180]],
[[350,170],[700,180],[1100,170],[1500,180],[1850,160]],
[[280,160],[600,180],[950,180],[1300,190],[1700,180]],
[[350,180],[700,190],[1050,190],[1400,200],[1800,180]],
[[280,190],[620,200],[980,190],[1370,200],[1750,190]],
[[350,200],[700,200],[1050,210],[1450,200],[1850,200]],
[[280,200],[600,210],[950,210],[1300,210],[1700,210]]
];
function makeLevel(n){
platforms=[];
spikes=[];
theme=themes[Math.floor((n-1)/5)];
world={width:2300,goal:2170};
const gaps=layouts[n-1];
let cursor=0;
gaps.forEach(g=>{
let x=g[0],w=g[1];
platforms.push({x:cursor,y:430,w:x-cursor,h:70});
if(n%4===0){
platforms.push({x:x+10,y:350,w:w-20,h:15});
}else if(n%4===1){
platforms.push({x:x+15,y:385,w:w-30,h:15});
}else if(n%4===2){
platforms.push({x:x+10,y:330,w:w-20,h:15});
}else{
platforms.push({x:x+5,y:365,w:w-10,h:15});
}
cursor=x+w;
});
platforms.push({x:cursor,y:430,w:2300-cursor,h:70});
let amount=2+Math.floor(n/2);
for(let i=0;i<amount;i++){
let x=250+i*(155+((n*7)%30));
let y=365-((i+n)%4)*40;
if(x<2050){
platforms.push({x,y,w:65+(n%3)*12,h:15});
}
}
let count=2+Math.floor(n/2);
for(let i=0;i<count;i++){
let x=220+i*(145-(Math.min(n,10)*3));
if(x>2100)break;
if(gaps.some(g=>x>g[0]-45&&x<g[0]+g[1]+35))continue;
spikes.push({x,y:402,w:38+(n%3)*5,h:28});
}
if(n>=6){
for(let i=0;i<Math.floor(n/3);i++){
let x=500+i*330;
if(x<2050)spikes.push({x,y:402,w:48,h:28});
}
}
}
function startLevel(n){
if(n>unlocked)return;
level=n;
$("menu").style.display="none";
$("gameScreen").style.display="block";
$("win").style.display="none";
makeLevel(n);
$("levelTitle").textContent="Уровень "+n+" · "+theme.name;
restart();
}
function restart(){
player={
x:45,y:382,w:34,h:48,
vx:0,vy:0,ground:false,
direction:1,anim:0
};
camera=0;
playing=true;
won=false;
jumpQueued=false;
keys.left=false;
keys.right=false;
$("win").style.display="none";
}
function showMenu(){
playing=false;
keys.left=false;
keys.right=false;
$("gameScreen").style.display="none";
$("menu").style.display="block";
$("win").style.display="none";
renderLevels();
}
function winLevel(){
if(won)return;
won=true;
playing=false;
if(!completed.includes(level))completed.push(level);
unlocked=Math.max(unlocked,Math.min(20,level+1));
save();
renderLevels();
$("winText").textContent=level===20?
"НЕВЕРОЯТНО! ТЫ ПРОШЁЛ ВСЕ 20 ПАРКУРОВ!":
"Ты открыл уровень "+(level+1)+"!";
$("nextBtn").style.display=level===20?"none":"inline-block";
$("win").style.display="flex";
}
function nextLevel(){
if(level<20)startLevel(level+1);
else showMenu();
}
function collide(a,b){
return a.x<b.x+b.w&&a.x+a.w>b.x&&a.y<b.y+b.h&&a.y+a.h>b.y;
}
function update(){
if(!playing)return;
player.vx=0;
if(keys.left)player.vx=-4.5;
if(keys.right)player.vx=4.5;
if(player.vx!==0){
player.direction=player.vx>0?1:-1;
player.anim+=0.18;
}
if(jumpQueued&&player.ground){
player.vy=-12.5;
player.ground=false;
}
jumpQueued=false;
player.x+=player.vx;
player.x=Math.max(0,Math.min(world.width-player.w,player.x));
let oldBottom=player.y+player.h;
player.vy=Math.min(player.vy+0.55,14);
player.y+=player.vy;
player.ground=false;
for(const p of platforms){
if(player.vy>=0&&oldBottom<=p.y+2&&player.y+player.h>=p.y&&player.x+player.w>p.x&&player.x<p.x+p.w){
player.y=p.y-player.h;
player.vy=0;
player.ground=true;
}
}
for(const s of spikes){
if(collide(player,s)){restart();return;}
}
if(player.y>550){restart();return;}
if(player.x+player.w>=world.goal){winLevel();return;}
camera=Math.max(0,Math.min(world.width-900,player.x-350));
}
// Рисуем мультяшного персонажа
function drawPlayer(){
let x=player.x;
let y=player.y;
let moving=Math.abs(player.vx)>0;
let running=moving&&player.ground;
let jump=!player.ground;
let t=player.anim;
let dir=player.direction;
ctx.save();
ctx.translate(x+player.w/2,y);
ctx.scale(dir,1);
// Тень
ctx.fillStyle="#14213a55";
ctx.beginPath();
ctx.ellipse(0,player.h+1,17,4,0,0,Math.PI*2);
ctx.fill();
// Анимация ног
let legMove=running?Math.sin(t)*5:0;
let armMove=running?Math.sin(t+Math.PI)*5:0;
// Ноги
ctx.lineCap="round";
ctx.lineJoin="round";
// Дальняя нога
ctx.strokeStyle="#24334f";
ctx.lineWidth=8;
ctx.beginPath();
ctx.moveTo(-5,31);
ctx.lineTo(-7-legMove,40);
ctx.lineTo(-9-legMove,43);
ctx.stroke();
// Ближняя нога
ctx.beginPath();
ctx.moveTo(5,31);
ctx.lineTo(7+legMove,40);
ctx.lineTo(9+legMove,43);
ctx.stroke();
// Кроссовки
ctx.fillStyle="#ffffff";
ctx.beginPath();
ctx.roundRect(-16-legMove,39,15,9,4);
ctx.fill();
ctx.beginPath();
ctx.roundRect(2+legMove,39,15,9,4);
ctx.fill();
// Подошвы
ctx.fillStyle="#a9c8df";
ctx.fillRect(-16-legMove,46,14,2);
ctx.fillRect(2+legMove,46,14,2);
// Руки
ctx.strokeStyle="#f1b28a";
ctx.lineWidth=7;
ctx.beginPath();
ctx.moveTo(-10,13);
ctx.lineTo(-14-armMove,23);
ctx.lineTo(-12-armMove,27);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(10,13);
ctx.lineTo(14+armMove,23);
ctx.lineTo(12+armMove,27);
ctx.stroke();
// Кисти
ctx.fillStyle="#f1b28a";
ctx.beginPath();
ctx.arc(-12-armMove,28,4,0,Math.PI*2);
ctx.fill();
ctx.beginPath();
ctx.arc(12+armMove,28,4,0,Math.PI*2);
ctx.fill();
// Тело и кофта
ctx.fillStyle="#267de0";
ctx.beginPath();
ctx.roundRect(-12,9,24,24,7);
ctx.fill();
// Светлая часть кофты
ctx.fillStyle="#4aa5ff";
ctx.beginPath();
ctx.roundRect(-9,11,18,8,4);
ctx.fill();
// Молния
ctx.fillStyle="#d7efff";
ctx.fillRect(-1,13,2,16);
// Карманы
ctx.fillStyle="#185bb0";
ctx.beginPath();
ctx.roundRect(-9,24,7,5,2);
ctx.fill();
ctx.beginPath();
ctx.roundRect(2,24,7,5,2);
ctx.fill();
// Шея
ctx.fillStyle="#e9a77e";
ctx.fillRect(-4,5,8,7);
// Голова
ctx.fillStyle="#f4bc94";
ctx.beginPath();
ctx.roundRect(-12,-15,24,24,8);
ctx.fill();
// Уши
ctx.beginPath();
ctx.arc(-12,-5,4,0,Math.PI*2);
ctx.fill();
ctx.beginPath();
ctx.arc(12,-5,4,0,Math.PI*2);
ctx.fill();
// Волосы
ctx.fillStyle="#603b28";
ctx.beginPath();
ctx.roundRect(-13,-19,26,12,7);
ctx.fill();
// Чёлка
ctx.beginPath();
ctx.moveTo(-12,-11);
ctx.quadraticCurveTo(-9,-18,-4,-12);
ctx.quadraticCurveTo(0,-20,4,-12);
ctx.quadraticCurveTo(9,-18,13,-10);
ctx.lineTo(11,-5);
ctx.lineTo(-12,-5);
ctx.closePath();
ctx.fill();
// Глаза
ctx.fillStyle="#ffffff";
ctx.beginPath();
ctx.ellipse(-5,-5,3.3,4.2,0,0,Math.PI*2);
ctx.fill();
ctx.beginPath();
ctx.ellipse(5,-5,3.3,4.2,0,0,Math.PI*2);
ctx.fill();
ctx.fillStyle="#26344d";
ctx.beginPath();
ctx.arc(-4,-4,1.8,0,Math.PI*2);
ctx.fill();
ctx.beginPath();
ctx.arc(6,-4,1.8,0,Math.PI*2);
ctx.fill();
// Улыбка
ctx.strokeStyle="#a65c4d";
ctx.lineWidth=1.5;
ctx.beginPath();
ctx.arc(0,0,4,0.2,Math.PI-0.2);
ctx.stroke();
// Брови
ctx.strokeStyle="#603b28";
ctx.lineWidth=2;
ctx.beginPath();
ctx.moveTo(-8,-11);
ctx.lineTo(-3,-12);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(3,-12);
ctx.lineTo(8,-11);
ctx.stroke();
ctx.restore();
}
function draw(){
ctx.clearRect(0,0,900,500);
let sky=ctx.createLinearGradient(0,0,0,500);
sky.addColorStop(0,theme.sky1);
sky.addColorStop(1,theme.sky2);
ctx.fillStyle=sky;
ctx.fillRect(0,0,900,500);
ctx.save();
ctx.translate(-camera,0);
// Облака и звёзды
if(level<=5||level>=11&&level<=15){
ctx.fillStyle="#ffffff88";
for(let i=0;i<10;i++){
let x=i*250+40;
ctx.beginPath();
ctx.arc(x,75,20,0,Math.PI*2);
ctx.arc(x+25,65,25,0,Math.PI*2);
ctx.arc(x+50,78,18,0,Math.PI*2);
ctx.fill();
}
}else{
ctx.fillStyle="#ffffff";
for(let i=0;i<35;i++){
ctx.fillRect((i*173)%2200,(i*83)%260,3,3);
}
}
// Платформы
for(const p of platforms){
ctx.fillStyle=theme.ground;
ctx.fillRect(p.x,p.y,p.w,p.h);
ctx.fillStyle=theme.top;
ctx.fillRect(p.x,p.y,p.w,7);
ctx.fillStyle="#172b45";
for(let x=p.x+10;x<p.x+p.w;x+=35){
ctx.fillRect(x,p.y+20,8,8);
}
}
// Шипы
for(const s of spikes){
ctx.fillStyle=theme.spike;
ctx.beginPath();
ctx.moveTo(s.x,s.y+s.h);
ctx.lineTo(s.x+s.w/2,s.y);
ctx.lineTo(s.x+s.w,s.y+s.h);
ctx.closePath();
ctx.fill();
}
// Финиш
ctx.fillStyle="#27344c";
ctx.fillRect(world.goal,330,7,100);
ctx.fillStyle="#ffdc55";
ctx.fillRect(world.goal+7,330,65,36);
ctx.fillStyle="#26344c";
ctx.font="bold 15px Arial";
ctx.fillText("FINISH",world.goal+10,353);
// Персонаж
drawPlayer();
ctx.restore();
ctx.fillStyle="#203b58";
ctx.font="bold 17px Arial";
ctx.fillText("УРОВЕНЬ "+level,20,28);
}
function loop(){
update();
draw();
requestAnimationFrame(loop);
}
document.addEventListener("keydown",e=>{
const k=e.key.toLowerCase();
if(["arrowleft","arrowright","arrowup"," "].includes(k))e.preventDefault();
if(k==="arrowleft"||k==="a")keys.left=true;
if(k==="arrowright"||k==="d")keys.right=true;
if(k===" "||k==="arrowup"||k==="w"){
if(!e.repeat)jumpQueued=true;
}
if(k==="r"&&playing)restart();
});
document.addEventListener("keyup",e=>{
const k=e.key.toLowerCase();
if(k==="arrowleft"||k==="a")keys.left=false;
if(k==="arrowright"||k==="d")keys.right=false;
});
function bindButton(id,key){
const b=$(id);
b.addEventListener("pointerdown",e=>{
e.preventDefault();
if(key==="jump")jumpQueued=true;
else keys[key]=true;
});
function release(e){
e.preventDefault();
if(key!=="jump")keys[key]=false;
}
b.addEventListener("pointerup",release);
b.addEventListener("pointercancel",release);
b.addEventListener("lostpointercapture",release);
}
bindButton("left","left");
bindButton("right","right");
bindButton("jump","jump");
$("menuBtn").onclick=showMenu;
$("restartBtn").onclick=restart;
$("nextBtn").onclick=nextLevel;
$("backBtn").onclick=showMenu;
renderLevels();
makeLevel(1);
restart();
showMenu();
requestAnimationFrame(loop);
</script>
</body>
</html>