Загрузка данных


<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NikLjoh — паркур платформер</title>
<style>
  html,body{margin:0;height:100%;background:#000;display:flex;flex-direction:column;align-items:center;justify-content:center;font-family:'Courier New',monospace;overflow:hidden;}
  canvas{width:min(96vw,960px);height:auto;aspect-ratio:960/540;image-rendering:pixelated;border:3px solid #22c55e;box-shadow:0 0 40px rgba(34,197,94,.45);cursor:pointer;}
  #hint{color:#94a3b8;font-size:12px;margin-top:8px;text-align:center;}
</style>
</head>
<body>
<canvas id="game" width="960" height="540"></canvas>
<div id="hint">WASD/Стрелки — движение · Пробел — прыжок · ЛКМ у таблички — читать · Enter — диалог</div>
<script>
const canvas=document.getElementById('game');
const ctx=canvas.getContext('2d');
const W=960,H=540;

/* ================= INPUT ================= */
const keys={};
window.addEventListener('keydown',e=>{keys[e.code]=true; if(e.code==='Enter') onEnter();});
window.addEventListener('keyup',e=>keys[e.code]=false);
const mouse={x:0,y:0,down:false};
canvas.addEventListener('mousemove',e=>{
  const r=canvas.getBoundingClientRect();
  mouse.x=(e.clientX-r.left)*(W/r.width);
  mouse.y=(e.clientY-r.top)*(H/r.height);
  if(mouse.down && state==='SETTINGS') handleSettingsClick();
});
canvas.addEventListener('mousedown',e=>{mouse.down=true; onClick();});
canvas.addEventListener('mouseup',e=>{mouse.down=false;});

/* ================= AUDIO ================= */
let actx=null;
function initAudio(){ if(!actx){ try{actx=new (window.AudioContext||window.webkitAudioContext)();}catch(e){} } }
function beep(freq,dur,vol=0.2,type='square'){
  if(!actx) return;
  const o=actx.createOscillator(), g=actx.createGain();
  o.type=type;o.frequency.value=freq;
  g.gain.value=vol;
  o.connect(g);g.connect(actx.destination);
  o.start(); o.stop(actx.currentTime+dur);
}
let musicPlaying=false, musicTimer=null, melodyIndex=0;
const menuMelody=[[523,0.35],[523,0.35],[587,0.35],[659,0.18],[659,0.18],[523,0.6],[0,0.4]];
function playMenuMusic(){
  musicPlaying=true;
  (function step(){
     if(!musicPlaying) return;
     const [freq,dur]=menuMelody[melodyIndex%menuMelody.length];
     if(freq>0) beep(freq,dur*0.9,0.18*settings.music,'triangle');
     melodyIndex++;
     musicTimer=setTimeout(step,dur*1000);
  })();
}
function stopMenuMusic(){musicPlaying=false; clearTimeout(musicTimer);}

/* ================= SETTINGS ================= */
let settings=JSON.parse(localStorage.getItem('nikljoh_settings')||'null')||{music:0.6,sfx:0.7,graphics:'high'};
function saveSettings(){localStorage.setItem('nikljoh_settings',JSON.stringify(settings));}

/* ================= OUTFITS / CAPS ================= */
const outfits=[
 {name:'Стандарт',shirt:'#3b82f6',pants:'#1f2937',shoes:'#111827'},
 {name:'Красный',shirt:'#ef4444',pants:'#111827',shoes:'#000'},
 {name:'Зелёный',shirt:'#22c55e',pants:'#14532d',shoes:'#052e14'},
 {name:'Жёлтый',shirt:'#eab308',pants:'#3f3f46',shoes:'#1c1917'},
 {name:'Фиолет',shirt:'#a855f7',pants:'#312e81',shoes:'#1e1b4b'},
 {name:'Оранж',shirt:'#f97316',pants:'#7c2d12',shoes:'#292524'},
 {name:'Розовый',shirt:'#ec4899',pants:'#4a044e',shoes:'#000'},
 {name:'Голубой',shirt:'#06b6d4',pants:'#164e63',shoes:'#083344'},
 {name:'Чёрный',shirt:'#27272a',pants:'#000',shoes:'#000'},
 {name:'Белый',shirt:'#f4f4f5',pants:'#a1a1aa',shoes:'#52525b'}
];
const caps=[
 {name:'Без кепки',color:null},
 {name:'Чёрная',color:'#111827'},
 {name:'Красная',color:'#dc2626'},
 {name:'Синяя',color:'#2563eb'},
 {name:'Зелёная',color:'#16a34a'},
 {name:'Жёлтая',color:'#facc15'}
];
let selOutfit=parseInt(localStorage.getItem('nikljoh_outfit')||'0');
let selCap=parseInt(localStorage.getItem('nikljoh_cap')||'0');

/* ================= PLAYER SPRITE ================= */
function drawPlayer(c,x,y,facing,outfitIdx,capIdx,walkFrame,scale=3){
  const o=outfits[outfitIdx], cap=caps[capIdx], u=scale;
  c.save();
  c.translate(x,y);
  if(facing<0) c.scale(-1,1);
  const legOff=Math.sin(walkFrame)*u;
  c.fillStyle=o.pants;
  c.fillRect(-5*u,8*u+legOff,4*u,8*u);
  c.fillRect(1*u,8*u-legOff,4*u,8*u);
  c.fillStyle=o.shoes;
  c.fillRect(-5*u,15*u+legOff,4*u,2*u);
  c.fillRect(1*u,15*u-legOff,4*u,2*u);
  c.fillStyle=o.shirt;
  c.fillRect(-5*u,0,10*u,8*u);
  c.fillRect(-7*u,1*u,2*u,6*u);
  c.fillRect(5*u,1*u,2*u,6*u);
  c.fillStyle='#f1c27d';
  c.fillRect(-4*u,-8*u,8*u,8*u);
  c.fillStyle='#fff';
  c.fillRect(-2*u,-5*u,2*u,2*u);
  c.fillRect(1*u,-5*u,2*u,2*u);
  c.fillStyle='#000';
  c.fillRect(-1*u,-5*u,1*u,2*u);
  c.fillRect(2*u,-5*u,1*u,2*u);
  c.fillStyle='#0a0a0a';
  c.fillRect(-5*u,-10*u,10*u,4*u);
  c.fillRect(-5*u,-6*u,2*u,3*u);
  c.fillRect(3*u,-6*u,2*u,3*u);
  if(cap.color){
     c.fillStyle=cap.color;
     c.fillRect(-5*u,-11*u,10*u,3*u);
     c.fillRect(-1*u,-9*u,7*u,2*u);
  }
  c.restore();
}
function drawNPC(x,y){
  ctx.save(); ctx.translate(x,y);
  ctx.fillStyle='#111827'; ctx.fillRect(-15,-40,30,40);
  ctx.fillStyle='#f1c27d'; ctx.fillRect(-12,-64,24,24);
  ctx.fillStyle='#78350f'; ctx.fillRect(-12,-64,24,8);
  ctx.fillStyle='#000'; ctx.fillRect(-6,-52,4,4); ctx.fillRect(2,-52,4,4);
  ctx.fillRect(-16,-40,6,40); ctx.fillRect(10,-40,6,40);
  ctx.restore();
}

/* ================= PARTICLES ================= */
let particles=[];
function spawnFirework(){
  const x=Math.random()*W, y=Math.random()*H*0.5;
  const color=`hsl(${Math.random()*360},80%,60%)`;
  for(let i=0;i<26;i++){
    const a=Math.random()*Math.PI*2, sp=Math.random()*3+1;
    particles.push({x,y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp,life:1,color,size:2,type:'fw'});
  }
}
function spawnBlood(x,y){
  for(let i=0;i<24;i++){
    particles.push({x,y,vx:(Math.random()-0.5)*7,vy:(Math.random()-1)*7,life:1,color:'#b91c1c',size:3,type:'blood'});
  }
}
function updateParticles(){
  for(const p of particles){
    p.x+=p.vx;p.y+=p.vy;
    if(p.type==='blood') p.vy+=0.35;
    p.life-=0.02;
  }
  particles=particles.filter(p=>p.life>0);
}
function drawParticles(){
  for(const p of particles){
    ctx.globalAlpha=Math.max(p.life,0);
    ctx.fillStyle=p.color;
    ctx.fillRect(p.x,p.y,p.size,p.size);
  }
  ctx.globalAlpha=1;
}
let fireworkTimer=0;

/* ================= STATE ================= */
let state='MENU';
let loadProgress=0;
let walkFrame=0;

/* ---- cutscene ---- */
let cutscene={playerX:-50,npcX:W+50,phase:'run'};
function updateCutscene(){
  if(cutscene.phase==='run'){
    cutscene.playerX+=2.5;
    if(cutscene.playerX>380) cutscene.phase='npcApproach';
  } else if(cutscene.phase==='npcApproach'){
    cutscene.npcX-=3;
    if(cutscene.npcX<480){ cutscene.phase='talk'; startDialogue(mainDialogueLines,null); }
  }
}
function drawCutsceneBG(){
  const g=ctx.createLinearGradient(0,0,0,H*0.7);
  g.addColorStop(0,'#7dd3fc'); g.addColorStop(1,'#bae6fd');
  ctx.fillStyle=g; ctx.fillRect(0,0,W,H*0.7);
  ctx.fillStyle='#52525b'; ctx.fillRect(0,H*0.7,W,H*0.3);
  ctx.fillStyle='#fde68a';
  for(let i=0;i<W;i+=60) ctx.fillRect(i,H*0.7+40,30,8);
  ctx.fillStyle='#334155';
  ctx.fillRect(30,H*0.4,120,H*0.3);
  ctx.fillRect(700,H*0.35,150,H*0.35);
}
function drawCutscene(){
  drawCutsceneBG();
  drawPlayer(ctx,cutscene.playerX,H*0.7,1,selOutfit,selCap,cutscene.phase==='run'?walkFrame:0);
  if(cutscene.phase!=='run') drawNPC(cutscene.npcX,H*0.7);
  if(dialogue.active) drawDialogue();
}

/* ---- dialogue ---- */
const mainDialogueLines=[
 {speaker:'Незнакомец',text:'Привет.'},
 {speaker:'Незнакомец',text:'Хочешь попасть в наш эксперимент?',choices:['Да','Нет','Не знаю']}
];
let dialogue={active:false,lines:[],idx:0,charIdx:0,timer:0,onDone:null,choiceButtons:null};
function startDialogue(lines,onDone){
  dialogue.active=true; dialogue.lines=lines; dialogue.idx=0; dialogue.charIdx=0; dialogue.timer=0; dialogue.onDone=onDone; dialogue.choiceButtons=null;
}
function onEnter(){
  if(!dialogue.active) return;
  const line=dialogue.lines[dialogue.idx];
  if(!line) return;
  if(dialogue.charIdx<line.text.length){ dialogue.charIdx=line.text.length; return; }
  if(line.choices) return;
  dialogue.idx++; dialogue.charIdx=0;
  if(dialogue.idx>=dialogue.lines.length){
     dialogue.active=false;
     const cb=dialogue.onDone; dialogue.onDone=null;
     if(cb) cb();
  }
}
function updateDialogueTyping(dt){
  if(!dialogue.active) return;
  const line=dialogue.lines[dialogue.idx]; if(!line) return;
  dialogue.timer+=dt;
  if(dialogue.timer>30 && dialogue.charIdx<line.text.length){
     dialogue.charIdx++; dialogue.timer=0;
     if(dialogue.charIdx%2===0) beep(600,0.02,0.05*settings.sfx,'square');
  }
}
function drawDialogue(){
  const line=dialogue.lines[dialogue.idx]; if(!line) return;
  ctx.fillStyle='rgba(0,0,0,0.85)'; ctx.fillRect(60,30,W-120,120);
  ctx.strokeStyle='#22c55e'; ctx.lineWidth=3; ctx.strokeRect(60,30,W-120,120);
  ctx.fillStyle='#22c55e'; ctx.font='14px monospace'; ctx.textAlign='left';
  ctx.fillText(line.speaker+':',80,55);
  ctx.fillStyle='#fff'; ctx.font='16px monospace';
  ctx.fillText(line.text.substring(0,dialogue.charIdx),80,85);
  if(dialogue.charIdx>=line.text.length){
    if(line.choices){
      dialogue.choiceButtons=[]; let cx=90;
      for(const ch of line.choices){
        const bw=100;
        dialogue.choiceButtons.push({x:cx,y:110,w:bw,h:30,text:ch});
        ctx.fillStyle='#1f2937'; ctx.fillRect(cx,110,bw,30);
        ctx.strokeStyle='#22c55e'; ctx.strokeRect(cx,110,bw,30);
        ctx.fillStyle='#fff'; ctx.textAlign='center'; ctx.fillText(ch,cx+bw/2,130);
        cx+=bw+10;
      }
      ctx.textAlign='left';
    } else {
      ctx.fillStyle='#9ca3af'; ctx.font='12px monospace';
      ctx.fillText('[Enter] продолжить',80,140);
    }
  } else dialogue.choiceButtons=null;
}
function handleChoice(choice){
  beep(440,0.1,0.2*settings.sfx);
  if(choice==='Да'){
    dialogue.lines=[{speaker:'Незнакомец',text:'Вот, держи визитку.'},{speaker:'Незнакомец',text:'Позвони потом сюда.'}];
    dialogue.idx=0; dialogue.charIdx=0;
    dialogue.onDone=()=>startTransition('gift');
  } else {
    const resp = choice==='Нет' ? 'Ну и ладно.' : 'Не знаешь? Значит - да!';
    dialogue.lines=[{speaker:'Незнакомец',text:resp},{speaker:'Незнакомец',text:'Тогда я решу за тебя!'}];
    dialogue.idx=0; dialogue.charIdx=0;
    dialogue.onDone=()=>startTransition('portal');
  }
}

/* ---- transition (gift / portal) ---- */
let transitionAnim={type:null,timer:0};
function startTransition(type){
  dialogue.active=false; transitionAnim={type,timer:0}; state='TRANSITION';
}
function updateTransition(dt){
  transitionAnim.timer+=dt;
  if(transitionAnim.timer>2500){ makeEntranceRoom(); state='PLAY'; }
}
function drawTransition(){
  drawCutsceneBG();
  const t=transitionAnim.timer/2500;
  if(transitionAnim.type==='gift'){
    drawPlayer(ctx,cutscene.playerX,H*0.7,1,selOutfit,selCap,0);
    drawNPC(cutscene.npcX,H*0.7);
    if(t>0.25){
      ctx.fillStyle='#fff'; ctx.fillRect(cutscene.playerX-20,H*0.7-90-t*40,60,36);
      ctx.fillStyle='#000'; ctx.font='8px monospace';
      ctx.fillText('ЭКСПЕРИМЕНТ',cutscene.playerX-18,H*0.7-75-t*40);
      ctx.fillText('ЗВОНИТЕ',cutscene.playerX-5,H*0.7-65-t*40);
    }
  } else {
    ctx.save(); ctx.translate(cutscene.playerX,H*0.6); ctx.rotate(t*10);
    ctx.strokeStyle='#a855f7';
    for(let i=0;i<5;i++){ ctx.beginPath(); ctx.arc(0,0,20+i*15+t*30,0,Math.PI*2); ctx.stroke(); }
    ctx.restore();
    drawPlayer(ctx,cutscene.playerX,H*0.6+t*100,1,selOutfit,selCap,0,3*(1-t)+0.1);
  }
  ctx.fillStyle=`rgba(0,0,0,${Math.min(t*1.2,1)})`; ctx.fillRect(0,0,W,H);
}

/* ---- levels ---- */
let level={num:0,platforms:[],hazards:[],sign:null,door:null,goal:null,spawn:{x:0,y:0}};
let player={x:0,y:0,vx:0,vy:0,w:24,h:44,onGround:false,facing:1,dead:false};
let cameraY=0;
let signReadProgress=0, signRead=false, signTextVisible=false;

function makeEntranceRoom(){
  level={num:0,
    platforms:[{x:0,y:480,w:960,h:60,color:'#374151'}],
    hazards:[],
    sign:{x:420,y:420,w:24,h:60,text:'Поздравляю, ты попал в наш эксперимент :)'},
    door:{x:860,y:300,w:80,h:180,openAmt:0,opening:false},
    goal:null,
    spawn:{x:100,y:420}
  };
  player.x=level.spawn.x; player.y=level.spawn.y; player.vx=0; player.vy=0;
  signRead=false; signReadProgress=0; cameraY=0;
}
function seededRandom(seed){ let s=seed; return function(){ s=(s*9301+49297)%233280; return s/233280; }; }
function makeLevel(n){
  const rand=seededRandom(n*1000+42);
  let curY=H-100, curX=W/2;
  const platforms=[{x:curX-80,y:curY,w:160,h:20,color:'#4b5563',curX:curX-80,curY:curY}];
  const spawn={x:curX,y:curY-60};
  const hazards=[];
  const steps=12+n*2;
  for(let i=0;i<steps;i++){
    curY-=60+rand()*40;
    curX+=(rand()*320-160);
    curX=Math.max(50,Math.min(W-50-120,curX));
    const w=Math.max(70,150-n*4-rand()*30);
    platforms.push({x:curX,y:curY,w:w,h:20,color:'#4b5563',curX:curX,curY:curY});
    if(n>=2 && rand()<Math.min(0.5,0.15+n*0.03)){
      hazards.push({type:'saw',baseX:curX+w/2,baseY:curY-40,r:16,range:50+rand()*30,speed:1.5+n*0.15,axis:rand()>0.5?'x':'y',phase:rand()*10});
    }
    if(n>=2 && rand()<Math.min(0.45,0.12+n*0.025)){
      hazards.push({type:'laser',x:curX-10,y:curY-16,w:w+20,h:6,cycle:90-n*2,offset:Math.floor(rand()*90)});
    }
    if(n>=4 && rand()<0.15){
      const by=curY-25;
      platforms.push({x:curX,y:by,w:60,h:14,color:'#6b7280',moving:true,axis:'x',range:60+rand()*40,speed:1+rand(),phase:rand()*10,baseX:curX,baseY:by,curX:curX,curY:by});
    }
  }
  level={num:n,platforms,hazards,sign:null,door:null,goal:{x:curX,y:curY-70,w:70,h:50},spawn};
  player.x=spawn.x; player.y=spawn.y; player.vx=0; player.vy=0; cameraY=0;
}

const GRAVITY=0.7, JUMP=-14, MOVESPEED=1.4, FRICTION=0.82;
function updatePlayer(dt){
  if(player.dead) return;
  let moveX=0;
  if(keys['ArrowLeft']||keys['KeyA']) moveX=-1;
  if(keys['ArrowRight']||keys['KeyD']) moveX=1;
  player.vx+=moveX*MOVESPEED; player.vx*=FRICTION;
  if(Math.abs(player.vx)<0.1) player.vx=0;
  if(moveX!==0) player.facing=moveX;
  if((keys['Space']||keys['ArrowUp']||keys['KeyW']) && player.onGround){
    player.vy=JUMP; player.onGround=false; beep(300,0.1,0.3*settings.sfx,'square');
  }
  player.vy+=GRAVITY; if(player.vy>18) player.vy=18;
  player.x+=player.vx; player.y+=player.vy;
  player.onGround=false;
  for(const p of level.platforms){
    let px=p.x, py=p.y;
    if(p.moving){
      if(p.axis==='x'){ px=p.baseX+Math.sin(Date.now()*0.001*p.speed+p.phase)*p.range; py=p.baseY; }
      else { py=p.baseY+Math.sin(Date.now()*0.001*p.speed+p.phase)*p.range; px=p.baseX; }
    }
    p.curX=px; p.curY=py;
    const withinX=player.x+player.w>px && player.x<px+p.w;
    const wasAbove=(player.y+player.h-player.vy)<=py+1;
    if(withinX && player.vy>=0 && wasAbove && player.y+player.h>=py && player.y+player.h<=py+p.h+player.vy+1){
      player.y=py-player.h; player.vy=0; player.onGround=true;
    }
  }
  if(player.x<0) player.x=0;
  if(player.x+player.w>W) player.x=W-player.w;

  for(const hz of level.hazards){
    if(hz.type==='saw'){
      let hx=hz.baseX, hy=hz.baseY;
      if(hz.axis==='x') hx=hz.baseX+Math.sin(Date.now()*0.001*hz.speed+hz.phase)*hz.range;
      else hy=hz.baseY+Math.sin(Date.now()*0.001*hz.speed+hz.phase)*hz.range;
      hz.curX=hx; hz.curY=hy;
      const dx=(player.x+player.w/2)-hx, dy=(player.y+player.h/2)-hy;
      if(Math.sqrt(dx*dx+dy*dy)<hz.r+12) killPlayer();
    } else if(hz.type==='laser'){
      const t=(Date.now()/16.6+hz.offset)%(hz.cycle*2);
      hz.active=t<hz.cycle;
      if(hz.active && player.x+player.w>hz.x && player.x<hz.x+hz.w && player.y+player.h>hz.y && player.y<hz.y+hz.h) killPlayer();
    }
  }
  if(level.num>0 && player.y>level.spawn.y+420) killPlayer();

  if(level.goal){
    const g=level.goal;
    if(player.x+player.w>g.x && player.x<g.x+g.w && player.y+player.h>g.y && player.y<g.y+g.h) completeLevel();
  }

  if(level.num===0){
    const s=level.sign;
    if(mouse.down){
      const dist=Math.abs((player.x+player.w/2)-(s.x+s.w/2));
      if(dist<80){
        signReadProgress+=dt; signTextVisible=true;
        if(signReadProgress>1200 && !signRead){ signRead=true; level.door.opening=true; beep(200,0.3,0.4*settings.sfx,'sawtooth'); }
      }
    } else signTextVisible=false;
    if(level.door.opening && level.door.openAmt<1) level.door.openAmt+=0.01;
    if(level.door.openAmt>0.8 && player.x>level.door.x+40) makeLevel(1);
  }
  cameraY=player.y-H*0.6;
  if(cameraY>-50) cameraY=-50;
}
function killPlayer(){
  if(player.dead) return;
  player.dead=true;
  spawnBlood(player.x+player.w/2,player.y+player.h/2);
  beep(100,0.4,0.5*settings.sfx,'sawtooth');
  setTimeout(()=>{
    player.dead=false;
    player.x=level.spawn.x; player.y=level.spawn.y; player.vx=0; player.vy=0;
  },700);
}
let levelCompleteTimer=null;
function completeLevel(){
  if(level.num>=10){ state='ENDING'; return; }
  state='LEVELCOMPLETE';
  const nextNum=level.num+1;
  clearTimeout(levelCompleteTimer);
  levelCompleteTimer=setTimeout(()=>{ makeLevel(nextNum); state='PLAY'; },1800);
}

function drawLevel(){
  const g=ctx.createLinearGradient(0,0,0,H);
  if(level.num===0){ g.addColorStop(0,'#334155'); g.addColorStop(1,'#1e293b'); }
  else { g.addColorStop(0,'#0c1445'); g.addColorStop(1,'#3b0764'); }
  ctx.fillStyle=g; ctx.fillRect(0,0,W,H);

  ctx.save(); ctx.translate(0,-cameraY);

  if(level.num>0){
    const lavaY=level.spawn.y+420;
    ctx.fillStyle='#dc2626'; ctx.fillRect(0,lavaY,W,60);
    ctx.fillStyle='#f97316';
    for(let i=0;i<W;i+=20){ const wave=Math.sin(Date.now()/200+i)*4; ctx.fillRect(i,lavaY+wave,10,6); }
  }

  for(const p of level.platforms){
    const px=p.moving?p.curX:p.x, py=p.moving?p.curY:p.y;
    ctx.fillStyle=p.color||'#4b5563'; ctx.fillRect(px,py,p.w,p.h);
    ctx.fillStyle='rgba(255,255,255,0.12)'; ctx.fillRect(px,py,p.w,4);
  }

  for(const hz of level.hazards){
    if(hz.type==='saw'){
      ctx.save(); ctx.translate(hz.curX??hz.baseX,hz.curY??hz.baseY); ctx.rotate(Date.now()/100);
      ctx.fillStyle='#d1d5db'; ctx.beginPath();
      for(let i=0;i<8;i++){ const a=i/8*Math.PI*2; ctx.lineTo(Math.cos(a)*hz.r,Math.sin(a)*hz.r); ctx.lineTo(Math.cos(a+0.2)*(hz.r+6),Math.sin(a+0.2)*(hz.r+6)); }
      ctx.closePath(); ctx.fill();
      ctx.fillStyle='#9ca3af'; ctx.beginPath(); ctx.arc(0,0,hz.r*0.5,0,7); ctx.fill();
      ctx.restore();
    } else if(hz.type==='laser'){
      ctx.fillStyle=hz.active?'#ef4444':'rgba(239,68,68,0.2)';
      ctx.fillRect(hz.x,hz.y,hz.w,hz.h);
      if(hz.active){ ctx.shadowColor='#ef4444'; ctx.shadowBlur=10; ctx.fillRect(hz.x,hz.y,hz.w,hz.h); ctx.shadowBlur=0; }
    }
  }

  if(level.goal){
    ctx.fillStyle='#facc15'; ctx.fillRect(level.goal.x,level.goal.y,level.goal.w,level.goal.h);
    ctx.fillStyle='#000'; ctx.font='12px monospace'; ctx.textAlign='center';
    ctx.fillText('ФИНИШ',level.goal.x+level.goal.w/2,level.goal.y+level.goal.h/2);
  }

  if(level.num===0){
    ctx.fillStyle='#a16207'; ctx.fillRect(level.sign.x,level.sign.y,level.sign.w,level.sign.h);
    ctx.fillStyle='#fde68a'; ctx.fillRect(level.sign.x-8,level.sign.y-20,level.sign.w+16,24);
    const d=level.door;
    ctx.save(); ctx.beginPath(); ctx.rect(d.x,d.y,d.w,d.h); ctx.clip();
    ctx.fillStyle='#71717a'; ctx.fillRect(d.x,d.y-d.h*d.openAmt,d.w,d.h);
    ctx.strokeStyle='#3f3f46'; ctx.lineWidth=4; ctx.strokeRect(d.x,d.y-d.h*d.openAmt,d.w,d.h);
    ctx.restore();
  }

  if(!player.dead) drawPlayer(ctx,player.x+player.w/2,player.y+player.h,player.facing,selOutfit,selCap,player.onGround?(Math.abs(player.vx)>0.5?walkFrame:0):0);

  drawParticles();
  ctx.restore();

  ctx.fillStyle='#fff'; ctx.font='14px monospace'; ctx.textAlign='left';
  ctx.fillText(level.num===0?'Комната':'Уровень '+level.num+' / 10',10,20);

  if(signTextVisible){
    ctx.fillStyle='rgba(0,0,0,0.8)'; ctx.fillRect(180,60,600,50);
    ctx.fillStyle='#22c55e'; ctx.font='16px monospace'; ctx.textAlign='center';
    ctx.fillText(level.sign.text,W/2,90);
  }
}
function drawLevelComplete(){
  drawLevel();
  ctx.fillStyle='rgba(0,0,0,0.6)'; ctx.fillRect(0,0,W,H);
  ctx.fillStyle='#facc15'; ctx.font='28px monospace'; ctx.textAlign='center';
  ctx.fillText('УРОВЕНЬ '+level.num+' ПРОЙДЕН!',W/2,H/2);
}
function drawEnding(){
  ctx.fillStyle='#000'; ctx.fillRect(0,0,W,H);
  ctx.fillStyle='#22c55e'; ctx.font='28px monospace'; ctx.textAlign='center';
  ctx.fillText('Ты прошёл эксперимент.',W/2,180);
  ctx.font='16px monospace'; ctx.fillStyle='#fff';
  ctx.fillText('Оказывается, всё это было проверкой системы NikLjoh.',W/2,230);
  ctx.fillText('Незнакомец в фартуке был лишь наблюдателем.',W/2,255);
  ctx.fillText('Ты доказал, что человеческая воля сильнее лабиринта.',W/2,280);
  ctx.font='14px monospace'; ctx.fillStyle='#94a3b8'; ctx.fillText('СПАСИБО ЗА ИГРУ',W/2,340);
  drawButton(menuButtons.backToMenu);
}

/* ================= MENU / SETTINGS / WARDROBE ================= */
const menuButtons={
  play:{x:W/2-100,y:190,w:200,h:44,text:'ИГРАТЬ'},
  settings:{x:W/2-100,y:245,w:200,h:44,text:'НАСТРОЙКИ'},
  wardrobe:{x:W/2-100,y:300,w:200,h:44,text:'ШКАФ'},
  back:{x:20,y:20,w:110,h:36,text:'НАЗАД'},
  backToMenu:{x:W/2-100,y:400,w:200,h:40,text:'В МЕНЮ'}
};
function hit(b){ return mouse.x>=b.x&&mouse.x<=b.x+b.w&&mouse.y>=b.y&&mouse.y<=b.y+b.h; }
function drawButton(b){
  const h=hit(b);
  ctx.fillStyle=h?'#16a34a':'#166534'; ctx.fillRect(b.x,b.y,b.w,b.h);
  ctx.strokeStyle='#22c55e'; ctx.lineWidth=2; ctx.strokeRect(b.x,b.y,b.w,b.h);
  ctx.fillStyle='#fff'; ctx.font='16px monospace'; ctx.textAlign='center';
  ctx.fillText(b.text,b.x+b.w/2,b.y+b.h/2+6);
}
function drawMenu(){
  const g=ctx.createLinearGradient(0,0,0,H);
  g.addColorStop(0,'#0f172a'); g.addColorStop(1,'#1e293b');
  ctx.fillStyle=g; ctx.fillRect(0,0,W,H);
  drawParticles();
  ctx.fillStyle='#22c55e'; ctx.font='44px monospace'; ctx.textAlign='center';
  ctx.fillText('NikLjoh',W/2,110);
  ctx.font='14px monospace'; ctx.fillStyle='#94a3b8';
  ctx.fillText('паркур-платформер',W/2,140);
  drawButton(menuButtons.play); drawButton(menuButtons.settings); drawButton(menuButtons.wardrobe);
  drawPlayer(ctx,W/2,400,1,selOutfit,selCap,0);
}
function handleMenuClick(){
  if(hit(menuButtons.play)){ beep(440,0.1,0.2*settings.sfx); startLoading(); }
  else if(hit(menuButtons.settings)){ beep(440,0.1,0.2*settings.sfx); state='SETTINGS'; }
  else if(hit(menuButtons.wardrobe)){ beep(440,0.1,0.2*settings.sfx); state='WARDROBE'; }
}

function startLoading(){ state='LOADING'; loadProgress=0; }
function updateLoading(dt){
  loadProgress+=dt/2500;
  if(loadProgress>=1){
    loadProgress=1; stopMenuMusic();
    cutscene={playerX:-50,npcX:W+50,phase:'run'};
    state='CUTSCENE';
  }
}
function drawLoading(){
  ctx.fillStyle='#0f172a'; ctx.fillRect(0,0,W,H);
  ctx.fillStyle='#22c55e'; ctx.font='20px monospace'; ctx.textAlign='center';
  ctx.fillText('ЗАГРУЗКА...',W/2,H/2-40);
  const barW=500, barX=W/2-barW/2, barY=H/2;
  ctx.strokeStyle='#22c55e'; ctx.strokeRect(barX,barY,barW,24);
  ctx.fillStyle='#22c55e'; ctx.fillRect(barX,barY,barW*loadProgress,24);
  drawPlayer(ctx,barX+barW*loadProgress,barY-10,1,selOutfit,selCap,walkFrame);
}

let sliderRects={};
function drawSlider(x,y,w,val,key){
  ctx.strokeStyle='#22c55e'; ctx.strokeRect(x,y,w,10);
  ctx.fillStyle='#22c55e'; ctx.fillRect(x,y,w*val,10);
  sliderRects[key]={x,y,w,h:10};
}
function drawSettings(){
  ctx.fillStyle='#0f172a'; ctx.fillRect(0,0,W,H);
  ctx.fillStyle='#22c55e'; ctx.font='24px monospace'; ctx.textAlign='center';
  ctx.fillText('НАСТРОЙКИ',W/2,80);
  ctx.font='16px monospace'; ctx.fillStyle='#fff'; ctx.textAlign='left';
  ctx.fillText('Музыка',W/2-150,150); drawSlider(W/2-150,165,300,settings.music,'music');
  ctx.fillText('Звук',W/2-150,220); drawSlider(W/2-150,235,300,settings.sfx,'sfx');
  ctx.fillText('Графика: '+(settings.graphics==='high'?'Высокая':'Низкая'),W/2-150,290);
  drawButton({x:W/2-150,y:300,w:300,h:36,text:'ПЕРЕКЛЮЧИТЬ'});
  drawButton(menuButtons.back);
}
function handleSettingsClick(){
  if(hit(menuButtons.back)){ state='MENU'; return; }
  const toggleBtn={x:W/2-150,y:300,w:300,h:36};
  if(hit(toggleBtn)){ settings.graphics=settings.graphics==='high'?'low':'high'; saveSettings(); return; }
  for(const key in sliderRects){
    const r=sliderRects[key];
    if(mouse.x>=r.x&&mouse.x<=r.x+r.w&&mouse.y>=r.y-8&&mouse.y<=r.y+r.h+8){
      settings[key]=Math.max(0,Math.min(1,(mouse.x-r.x)/r.w)); saveSettings();
    }
  }
}

let wardrobeTab='outfit', wardrobeItemRects=[], wardrobePreviewFacing=1;
function drawWardrobe(){
  ctx.fillStyle='#0f172a'; ctx.fillRect(0,0,W,H);
  ctx.fillStyle='#22c55e'; ctx.font='24px monospace'; ctx.textAlign='center';
  ctx.fillText('ШКАФ',W/2,50);
  drawButton({x:W/2-210,y:70,w:200,h:32,text:'ОДЕЖДА'});
  drawButton({x:W/2+10,y:70,w:200,h:32,text:'КЕПКИ'});
  const items=wardrobeTab==='outfit'?outfits:caps;
  const cols=5, cellW=140, cellH=110, startX=W/2-(cols*cellW)/2, startY=120;
  wardrobeItemRects=[];
  items.forEach((it,i)=>{
    const cx=startX+(i%cols)*cellW, cy=startY+Math.floor(i/cols)*cellH;
    const selected=wardrobeTab==='outfit'?selOutfit===i:selCap===i;
    ctx.fillStyle=selected?'#166534':'#1e293b'; ctx.fillRect(cx,cy,cellW-10,cellH-10);
    ctx.strokeStyle=selected?'#22c55e':'#475569'; ctx.strokeRect(cx,cy,cellW-10,cellH-10);
    if(wardrobeTab==='outfit') drawPlayer(ctx,cx+cellW/2-5,cy+80,1,i,0,0,2);
    else { ctx.fillStyle=it.color||'#666'; ctx.fillRect(cx+cellW/2-30,cy+30,60,20); }
    ctx.fillStyle='#fff'; ctx.font='11px monospace'; ctx.textAlign='center';
    ctx.fillText(it.name,cx+cellW/2-5,cy+cellH-20);
    wardrobeItemRects.push({x:cx,y:cy,w:cellW-10,h:cellH-10,idx:i});
  });
  drawPlayer(ctx,W-100,420,wardrobePreviewFacing,selOutfit,selCap,0,5);
  drawButton(menuButtons.back);
}
function handleWardrobeClick(){
  if(hit(menuButtons.back)){ state='MENU'; return; }
  if(hit({x:W/2-210,y:70,w:200,h:32})){ wardrobeTab='outfit'; return; }
  if(hit({x:W/2+10,y:70,w:200,h:32})){ wardrobeTab='cap'; return; }
  for(const r of wardrobeItemRects){
    if(mouse.x>=r.x&&mouse.x<=r.x+r.w&&mouse.y>=r.y&&mouse.y<=r.y+r.h){
      if(wardrobeTab==='outfit'){ selOutfit=r.idx; localStorage.setItem('nikljoh_outfit',selOutfit); }
      else { selCap=r.idx; localStorage.setItem('nikljoh_cap',selCap); }
      beep(500,0.08,0.2*settings.sfx);
      wardrobePreviewFacing*=-1;
      setTimeout(()=>wardrobePreviewFacing*=-1,300);
    }
  }
}

/* ================= GLOBAL CLICK ================= */
function onClick(){
  initAudio();
  if(state==='MENU' && !musicPlaying) playMenuMusic();
  if(dialogue.active && dialogue.choiceButtons){
    for(const b of dialogue.choiceButtons){
      if(mouse.x>=b.x&&mouse.x<=b.x+b.w&&mouse.y>=b.y&&mouse.y<=b.y+b.h){
        handleChoice(b.text); dialogue.choiceButtons=null; return;
      }
    }
    return;
  }
  if(state==='MENU') handleMenuClick();
  else if(state==='SETTINGS') handleSettingsClick();
  else if(state==='WARDROBE') handleWardrobeClick();
  else if(state==='ENDING'){ if(hit(menuButtons.backToMenu)) state='MENU'; }
}

/* ================= LOOP ================= */
let lastTime=performance.now();
function update(dt){
  updateParticles();
  if(state==='MENU'){
    fireworkTimer+=dt;
    if(fireworkTimer>800){ fireworkTimer=0; if(settings.graphics==='high') spawnFirework(); }
    if(actx && !musicPlaying) playMenuMusic();
  }
  if(state==='CUTSCENE'){ updateCutscene(); if(dialogue.active) updateDialogueTyping(dt); }
  if(state==='LOADING') updateLoading(dt);
  if(state==='TRANSITION') updateTransition(dt);
  if(state==='PLAY') updatePlayer(dt);
}
function draw(){
  switch(state){
    case 'MENU': drawMenu(); break;
    case 'SETTINGS': drawSettings(); break;
    case 'WARDROBE': drawWardrobe(); break;
    case 'LOADING': drawLoading(); break;
    case 'CUTSCENE': drawCutscene(); break;
    case 'TRANSITION': drawTransition(); break;
    case 'PLAY': drawLevel(); break;
    case 'LEVELCOMPLETE': drawLevelComplete(); break;
    case 'ENDING': drawEnding(); break;
  }
}
function loop(now){
  const dt=now-lastTime; lastTime=now;
  walkFrame+=dt*0.01;
  update(dt); draw();
  requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
</script>
</body>
</html>