Загрузка данных
<!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>