🎮ArcadeLab

Фабрика ожившего детства

by BlazingBunny60
186 lines26.5 KB
▶ Play
<!DOCTYPE html><html lang="ru"><head><meta charset="utf-8"><title>Фабрика ожившего детства</title>
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover">
<style>
:root{color-scheme:dark;--r:#d3202c;--t:#efe6e2}
*{box-sizing:border-box;-webkit-user-select:none;user-select:none;touch-action:none}
html,body{margin:0;height:100%;overflow:hidden;background:#050203;color:var(--t);font-family:"Trebuchet MS",Verdana,sans-serif}
#c{position:fixed;inset:0;width:100%;height:100%;image-rendering:pixelated}
#vg{position:fixed;inset:0;pointer-events:none;z-index:1;background:radial-gradient(ellipse at center,transparent 35%,#000 105%)}
#rd{position:fixed;inset:0;pointer-events:none;z-index:1;background:radial-gradient(ellipse at center,transparent 40%,#a00 130%);opacity:0}
.ov{position:fixed;inset:0;z-index:6;display:flex;align-items:center;justify-content:center;padding:16px;background:rgba(4,0,2,.72)}
.pn{width:100%;max-width:420px;max-height:94%;overflow:auto;background:#12070bee;border:1px solid var(--r);border-radius:14px;padding:20px;text-align:center;box-shadow:0 0 30px #50000a}
h1{margin:0 0 6px;font-family:Georgia,serif;font-size:clamp(26px,8vw,38px);color:var(--r);letter-spacing:1px;text-shadow:0 0 14px #800}
p{margin:6px 0 12px;opacity:.85;font-size:15px;line-height:1.35}
button{font:inherit;font-size:17px;color:var(--t);background:#2a0910;border:1px solid var(--r);border-radius:10px;padding:13px;margin:5px 0;width:100%}
button:active{background:#5c0f1a}
.hide{display:none!important}
#top{position:fixed;left:12px;right:12px;top:calc(8px + env(safe-area-inset-top));z-index:3;pointer-events:none;font-size:15px;text-shadow:0 0 4px #000;display:flex;justify-content:space-between;gap:8px}
#guide{position:fixed;left:10px;right:10px;top:calc(46px + env(safe-area-inset-top));z-index:4;display:flex;gap:10px;align-items:center;background:#0a0306ee;border-left:4px solid #7fe3ff;padding:9px 12px;border-radius:8px;font-size:15px;line-height:1.3;pointer-events:none}
#gcv{width:56px;height:56px;flex:none;image-rendering:pixelated;background:#000;border:1px solid #633}
#skip{position:fixed;right:12px;bottom:calc(20px + env(safe-area-inset-bottom));z-index:7;width:auto;padding:10px 18px}
#ctl{position:fixed;inset:auto 0 0 0;padding:0 14px calc(20px + env(safe-area-inset-bottom));z-index:3;pointer-events:none;display:flex;justify-content:space-between;align-items:flex-end}
#jz{width:130px;height:130px;border-radius:50%;border:2px solid #fff5;background:#ffffff10;pointer-events:auto}
#jk{width:50px;height:50px;margin:38px;border-radius:50%;background:#fff6}
#bt{display:grid;grid-template-columns:repeat(2,68px);gap:10px;pointer-events:auto}
.tb{height:68px;border-radius:50%;border:2px solid #fff7;background:#000a;font-size:12px;display:flex;flex-direction:column;align-items:center;justify-content:center;line-height:1.1}
.tb b{font-size:22px;font-weight:400}.on{background:#6a5a10c0}
.bc{display:flex;gap:12px;align-items:center;text-align:left;border:1px solid #5a1a22;border-radius:10px;padding:8px;margin:8px 0;font-size:14px}
.bc canvas{width:64px;height:64px;flex:none;image-rendering:pixelated;background:#000}
#rot{position:fixed;inset:0;z-index:9;background:#000;display:none;align-items:center;justify-content:center;font-size:20px;text-align:center;padding:20px}
@media (orientation:landscape) and (max-height:480px){#rot{display:flex}}
</style></head><body>
<canvas id="c"></canvas><div id="vg"></div><div id="rd"></div>
<div id="rot">Поверни телефон вертикально ↻</div>
<div id="top" class="hide"><span id="t1"></span><span id="t2"></span></div>
<div id="guide" class="hide"><canvas id="gcv" width="64" height="64" class="hide"></canvas><div><b>Проводник:</b> <span id="gt"></span></div></div>
<button id="skip" class="hide">ПРОПУСТИТЬ</button>
<div id="ctl" class="hide"><div id="jz"><div id="jk"></div></div>
<div id="bt"><div class="tb" id="bL"><b>🔦</b>свет</div><div class="tb" id="bB"><b>🎵</b>шкатулка</div><div class="tb" id="bF"><b>🤫</b>замри</div><div class="tb" id="bR"><b>⚡</b>бег</div><div class="tb" id="bT" style="grid-column:span 2;width:68px;justify-self:end"><b>⟲</b>180°</div></div></div>

<div id="menu" class="ov"><div class="pn"><h1>ФАБРИКА ОЖИВШЕГО ДЕТСТВА</h1><p>Заброшенная фабрика игрушек. Игрушки проснулись и ищут тех, кто шумит. Найди три шестерёнки и выберись.</p>
<button id="mP">ИГРАТЬ (с обучением)</button><button id="mQ">ИГРАТЬ БЕЗ ОБУЧЕНИЯ</button><button id="mB">БЕСТИАРИЙ</button><button id="mS">ЗВУК И ГОЛОС: ВКЛ</button></div></div>
<div id="bes" class="ov hide"><div class="pn"><h1 style="font-size:26px">БЕСТИАРИЙ</h1><div id="bl"></div><button id="bK">НАЗАД</button></div></div>
<div id="end" class="ov hide"><div class="pn"><h1 id="eT"></h1><p id="eD"></p><button id="eA">ЕЩЁ РАЗ</button><button id="eM">В МЕНЮ</button></div></div>

<script>
const $=i=>document.getElementById(i),R=Math.random,TAU=Math.PI*2,N=23,D4=[[1,0],[-1,0],[0,1],[0,-1]];
const touch='ontouchstart' in window||navigator.maxTouchPoints>0;
const cv=$('c'),g=cv.getContext('2d');let W=240,H=480,cg,fg,zb=[];
function rs(){const k=Math.min(1,(innerWidth<innerHeight?240:420)/innerWidth);W=Math.round(innerWidth*k);H=Math.round(innerHeight*k);cv.width=W;cv.height=H;
cg=g.createLinearGradient(0,0,0,H/2);cg.addColorStop(0,'#030102');cg.addColorStop(1,'#0e0709');fg=g.createLinearGradient(0,H/2,0,H);fg.addColorStop(0,'#0a0607');fg.addColorStop(1,'#1c1112')}
addEventListener('resize',rs);rs();
/* ---------- СПРАЙТЫ ---------- */
const mk=f=>{const c=document.createElement('canvas');c.width=c.height=128;f(c.getContext('2d'));return c};
const ci=(x,X,Y,r,c)=>{x.fillStyle=c;x.beginPath();x.arc(X,Y,r,0,TAU);x.fill()},el=(x,X,Y,a,b,c)=>{x.fillStyle=c;x.beginPath();x.ellipse(X,Y,a,b,0,0,TAU);x.fill()};
const BEAR=mk(x=>{el(x,64,104,34,24,'#4a2812');for(const X of[32,96]){ci(x,X,26,13,'#5a3418');ci(x,X,26,6,'#1a0808')}ci(x,64,56,36,'#5e3719');el(x,64,68,17,13,'#9a7a52');ci(x,64,62,5,'#000');
ci(x,48,48,9,'#ddd');ci(x,80,48,9,'#ddd');ci(x,48,49,4,'#c00');ci(x,80,49,4,'#c00');x.strokeStyle='#000';x.lineWidth=2;x.beginPath();x.moveTo(40,38);x.lineTo(57,44);x.stroke();
x.fillStyle='#100';x.beginPath();x.moveTo(46,76);x.quadraticCurveTo(64,98,82,76);x.quadraticCurveTo(64,84,46,76);x.fill();x.fillStyle='#fff';for(let i=0;i<5;i++){x.beginPath();x.moveTo(50+i*7,79);x.lineTo(54+i*7,79);x.lineTo(52+i*7,88);x.fill()}});
const DOLL=mk(x=>{x.fillStyle='#6a1a2a';x.beginPath();x.moveTo(36,128);x.lineTo(46,86);x.lineTo(82,86);x.lineTo(92,128);x.fill();ci(x,64,50,36,'#111');ci(x,64,52,29,'#e6dccd');
x.fillStyle='#111';x.beginPath();x.moveTo(30,42);x.quadraticCurveTo(64,0,98,42);x.quadraticCurveTo(64,26,30,42);x.fill();el(x,52,54,7,9,'#000');el(x,76,54,7,9,'#000');ci(x,52,52,1.5,'#fff');ci(x,76,52,1.5,'#fff');
x.strokeStyle='#a00';x.lineWidth=3;x.beginPath();x.moveTo(46,72);x.quadraticCurveTo(64,88,84,70);x.stroke();x.strokeStyle='#555';x.lineWidth=1;x.beginPath();x.moveTo(64,24);x.lineTo(60,38);x.lineTo(66,48);x.stroke();ci(x,36,26,7,'#c00');ci(x,50,23,7,'#c00')});
const CLOWN=mk(x=>{x.fillStyle='#3a2a6a';x.fillRect(38,88,52,40);for(let i=0;i<6;i++)ci(x,46+i*7,100+(i%2)*12,3,'#f4c');['#e33','#3b3','#36e','#fc3'].forEach((c,i)=>ci(x,26+i*25,34+(i%2)*-8,14,c));ci(x,64,54,30,'#f2eee6');ci(x,64,58,8,'#d00');
x.fillStyle='#fc0';for(const X of[50,78]){x.beginPath();x.moveTo(X-8,48);x.lineTo(X,44);x.lineTo(X+8,48);x.lineTo(X,52);x.fill();ci(x,X,48,2.5,'#900')}
x.fillStyle='#b00';x.beginPath();x.moveTo(36,68);x.quadraticCurveTo(64,110,92,68);x.quadraticCurveTo(64,84,36,68);x.fill();x.fillStyle='#fff';for(let i=0;i<7;i++){x.beginPath();x.moveTo(42+i*7,71);x.lineTo(46+i*7,72);x.lineTo(44+i*7,80);x.fill()}});
const GEAR=mk(x=>{x.fillStyle='#e8b923';for(let i=0;i<8;i++){x.save();x.translate(64,64);x.rotate(i*TAU/8);x.fillRect(-9,-58,18,22);x.restore()}ci(x,64,64,44,'#e8b923');ci(x,64,64,16,'#2a1a05')});
const BOX=mk(x=>{x.fillStyle='#8a5a2a';x.fillRect(30,66,68,54);x.fillStyle='#f5d27a';x.fillRect(40,76,48,8);ci(x,64,50,8,'#f5d27a')});
const SP=[BEAR,DOLL,CLOWN];
const MD=[{n:'Мишка Топтыжка',a:'Слеп, но слышит бег и тяжёлые шаги. Идёт медленно, зато неутомимо.',w:'Слабость: музыкальная шкатулка 🎵. Брось её, и он пойдёт на мелодию.',h:'Мишка слышит бег! Иди шагом или брось шкатулку, чтобы увести его.',d:'Топтыжка услышал твой бег. Идти шагом и бросать шкатулку 🎵 безопаснее.'},
{n:'Кукла Мари',a:'Очень быстрая, но только когда ты на неё не смотришь.',w:'Слабость: взгляд. Пока она в твоём поле зрения, она замирает. Фонарик 🔦 помогает увидеть её в темноте.',h:'Мари позади тебя! Развернись и смотри на неё, она замрёт.',d:'Мари подошла, пока ты не смотрел. Держи её в поле зрения и включай фонарик.'},
{n:'Клоун Хихи',a:'Видит только движение. Любой шаг на виду, и он бросается в погоню.',w:'Слабость: неподвижность. Удерживай «Замри» 🤫, и он потеряет тебя.',h:'Слышишь смех? Хихи рядом. Замри, и он не заметит.',d:'Хихи заметил твоё движение. Замирай 🤫, когда он рядом.'}];
/* ---------- ЗВУК ---------- */
let AC,mg,eb,audioOn=true,thr=99;
function au(){if(AC){if(AC.state==='suspended')AC.resume();return}AC=new(window.AudioContext||window.webkitAudioContext)();mg=AC.createGain();mg.gain.value=audioOn?1:0;mg.connect(AC.destination);
const dl=AC.createDelay(1),fb=AC.createGain(),wet=AC.createGain();dl.delayTime.value=.45;fb.gain.value=.55;wet.gain.value=.7;dl.connect(fb);fb.connect(dl);dl.connect(wet);wet.connect(mg);eb=dl;music()}
function tn(f,d,ty='sine',v=.1,sw,echo){if(!AC)return;const t=AC.currentTime,o=AC.createOscillator(),gn=AC.createGain();o.type=ty;o.frequency.setValueAtTime(f,t);if(sw)o.frequency.exponentialRampToValueAtTime(sw,t+d);gn.gain.setValueAtTime(v,t);gn.gain.exponentialRampToValueAtTime(.001,t+d);o.connect(gn);gn.connect(mg);if(echo)gn.connect(eb);o.start(t);o.stop(t+d)}
function nz(d,v,f=1200){if(!AC)return;const n=AC.sampleRate*d|0,b=AC.createBuffer(1,n,AC.sampleRate),a=b.getChannelData(0);for(let i=0;i<n;i++)a[i]=(R()*2-1)*(1-i/n);const s=AC.createBufferSource(),fl=AC.createBiquadFilter(),gn=AC.createGain();s.buffer=b;fl.type='lowpass';fl.frequency.value=f;gn.gain.value=v;s.connect(fl);fl.connect(gn);gn.connect(mg);s.start()}
const scream=()=>{nz(1.3,.5,3500);tn(600,1.3,'sawtooth',.22,2000);tn(930,1.3,'sawtooth',.18,2600);tn(55,1.3,'sine',.5,30)};
function music(){[55,77.8,82.4,116.5].forEach((f,i)=>{const o=AC.createOscillator(),fl=AC.createBiquadFilter(),gn=AC.createGain(),l=AC.createOscillator(),lg=AC.createGain();o.type='sawtooth';o.frequency.value=f;fl.type='lowpass';fl.frequency.value=230;gn.gain.value=.04;l.frequency.value=.06+i*.03;lg.gain.value=110;l.connect(lg);lg.connect(fl.frequency);o.connect(fl);fl.connect(gn);gn.connect(mg);o.start();l.start()});
const nt=[329.6,311.1,246.9,233.1,196,246.9,174.6,185];let i=0;setInterval(()=>{if(R()<.8)tn(nt[i%8]*(R()<.15?1.414:1),1.6,'triangle',.045,0,1);i++},950);
(function hb(){if(S==='play'){tn(55,.22,'sine',.5,30);setTimeout(()=>tn(46,.2,'sine',.4,28),250)}setTimeout(hb,S==='play'?Math.min(1300,350+thr*90):1300)})();
setInterval(()=>{if(R()<.6)tn(900+R()*900,1.8,'sawtooth',.018,300+R()*200,1);else tn(140,2.2,'sawtooth',.05,55,1)},5500)}
/* ---------- ПРОВОДНИК ---------- */
let skipR=null,tm,skipAll=0;
function card(c){const e=$('gcv');if(c==null){e.classList.add('hide');return}e.classList.remove('hide');const x=e.getContext('2d');x.imageSmoothingEnabled=false;x.clearRect(0,0,64,64);x.drawImage(SP[c],0,0,128,128,0,0,64,64)}
function say(t,c){return new Promise(r=>{clearTimeout(tm);$('gt').textContent=t;card(c);$('guide').classList.remove('hide');
if(audioOn&&window.speechSynthesis){try{speechSynthesis.cancel();const u=new SpeechSynthesisUtterance(t);u.lang='ru-RU';u.pitch=.5+R()*.1;u.rate=.8;speechSynthesis.speak(u)}catch(e){}}
const ms=Math.max(3500,t.length*88);skipR=()=>{clearTimeout(tm);r()};tm=setTimeout(()=>{if(S!=='intro')$('guide').classList.add('hide');r()},ms)})}
/* ---------- МИР ---------- */
let S='menu',map,sty,P,mons=[],gears=[],decoy={t:0,x:0,y:0,s:0},boxes=2,light=0,got=0,fl=1,fakeT=50,fk=0,fm=0,jump=null,exOpen=false,seenTut=false,sens=1;
const PAL=[[118,44,36],[52,72,112],[124,104,52],[62,102,78]];
function bfs(cx,cy){const d=new Int16Array(N*N).fill(999),q=[cx+cy*N];d[q[0]]=0;for(let i=0;i<q.length;i++){const c=q[i],x=c%N,y=c/N|0;for(const[a,b]of D4){const nx=x+a,ny=y+b;if(nx<0||ny<0||nx>=N||ny>=N||map[ny][nx])continue;const k=nx+ny*N;if(d[k]>d[c]+1){d[k]=d[c]+1;q.push(k)}}}return d}
function los(x1,y1,x2,y2){const n=Math.ceil(Math.hypot(x2-x1,y2-y1)/.25);for(let i=1;i<n;i++){if(map[(y1+(y2-y1)*i/n)|0][(x1+(x2-x1)*i/n)|0])return false}return true}
const angd=(a,b)=>((a-b+Math.PI)%TAU+TAU)%TAU-Math.PI;
function newMap(){map=Array.from({length:N},()=>Array(N).fill(1));const st=[[1,1]];map[1][1]=0;
while(st.length){const[x,y]=st[st.length-1];const o=D4.map(([a,b])=>[a*2,b*2]).filter(([a,b])=>{const nx=x+a,ny=y+b;return nx>0&&ny>0&&nx<N-1&&ny<N-1&&map[ny][nx]===1}).sort(()=>R()-.5);if(!o.length){st.pop();continue}const[a,b]=o[0];map[y+b/2][x+a/2]=0;map[y+b][x+a]=0;st.push([x+a,y+b])}
for(let i=0;i<45;i++){const x=1+(R()*(N-2)|0),y=1+(R()*(N-2)|0);if(map[y][x]===1&&((x%2===0)!==(y%2===0)))map[y][x]=0}
map[N-1][N-2]=3;sty=map.map(r=>r.map(()=>R()*4|0));exOpen=false;
P={x:1.5,y:1.5,a:Math.PI/4,moving:0,still:0,run:0,st:0};
const ds=bfs(1,1),cells=[];for(let y=1;y<N-1;y++)for(let x=1;x<N-1;x++)if(!map[y][x])cells.push([x,y,ds[x+y*N]]);
const pick=(min,avoid)=>{for(let i=0;i<300;i++){const c=cells[R()*cells.length|0];if(c[2]>=min&&avoid.every(a=>Math.hypot(a.x-c[0]-.5,a.y-c[1]-.5)>6))return{x:c[0]+.5,y:c[1]+.5}}return{x:N-2.5,y:N-2.5}};
gears=[];for(let i=0;i<3;i++)gears.push(pick(10,gears));
mons=[0,1,2].map(t=>{const p=pick(t===1?20:14,[]);return{t,x:p.x,y:p.y,fd:null,ft:0,fk:-1,al:0,wt:0,wp:null,tg:null,sc:2,seen:0,tip:0}});
decoy.t=0;boxes=2;light=0;got=0;thr=99}
const randCell=()=>{for(;;){const x=1+(R()*(N-2)|0),y=1+(R()*(N-2)|0);if(!map[y][x])return{x:x+.5,y:y+.5}}};
function stepTo(m,tg,sp,dt){if(sp<=0)return;const tx=tg.x|0,ty=tg.y|0,k=tx+ty*N;if(m.ft<=0||m.fk!==k){m.fd=bfs(tx,ty);m.ft=.5;m.fk=k}
const cx=m.x|0,cy=m.y|0;let bx=m.x,by=m.y;if(cx===tx&&cy===ty){bx=tg.x;by=tg.y}else{let best=m.fd[cx+cy*N],bn=null;for(const[a,b]of D4){const nx=cx+a,ny=cy+b;if(nx<0||ny<0||nx>=N||ny>=N||map[ny][nx])continue;const v=m.fd[nx+ny*N];if(v<best){best=v;bn=[nx,ny]}}if(bn){bx=bn[0]+.5;by=bn[1]+.5}}
const dx=bx-m.x,dy=by-m.y,dd=Math.hypot(dx,dy)||1,s=Math.min(sp*dt,dd);m.x+=dx/dd*s;m.y+=dy/dd*s}
function upMon(m,dt){const d=Math.hypot(m.x-P.x,m.y-P.y);m.ft-=dt;m.wt-=dt;m.al-=dt;m.sc-=dt;let sp=1.2,tg=null;
if(m.t===0){if(decoy.t>0){tg=decoy;sp=2.2}else{if(P.run&&P.moving&&d<11){m.al=2.5;m.tg={x:P.x,y:P.y}}if(m.al>0&&m.tg){tg=m.tg;sp=2.4}}}
else if(m.t===1){m.seen=d<(light?9:4)&&Math.abs(angd(Math.atan2(m.y-P.y,m.x-P.x),P.a))<.65&&los(P.x,P.y,m.x,m.y);tg=P;sp=m.seen?0:3.1}
else{if(P.moving&&d<10&&los(P.x,P.y,m.x,m.y)){m.al=3;m.tg={x:P.x,y:P.y}}if(P.still&&d>1.3)m.al=0;if(m.al>0&&m.tg){tg=m.tg;sp=3}}
if(!tg){if(m.wt<=0||!m.wp){m.wp=randCell();m.wt=8}tg=m.wp;sp=1.2}
stepTo(m,tg,sp,dt);
if(d<9&&m.sc<=0&&sp>0){const v=(1-d/9)*.35;m.sc=m.t===1?1.4:2.5+R()*2;if(m.t===0)tn(70,.6,'sawtooth',v,38);else if(m.t===1){[1500,1760,1400].forEach((f,i)=>setTimeout(()=>tn(f,.2,'triangle',v*.3,0,1),i*140))}else{tn(220,.15,'square',v*.4,170);setTimeout(()=>tn(300,.5,'sawtooth',v*.3,140,1),180)}}
if(!m.tip&&d<7&&los(P.x,P.y,m.x,m.y)){m.tip=1;say(MD[m.t].h,m.t)}
if(d<.65&&S==='play')caught(m.t);return d}
/* ---------- ИГРОК ---------- */
const keys={};let jx=0,jy=0,rB=0,fB=0,lp=null;
const free=(x,y)=>map[y|0][x|0]===0;
function updP(dt){let fx=(keys.KeyD?1:0)-(keys.KeyA?1:0)+jx,fz=(keys.KeyW?1:0)-(keys.KeyS?1:0)-jy;const l=Math.hypot(fx,fz);if(l>1){fx/=l;fz/=l}
P.a+=((keys.ArrowRight?1:0)-(keys.ArrowLeft?1:0))*2.6*dt*sens;P.still=!!(keys.Space||fB);P.run=!!(keys.ShiftLeft||rB);
const sp=P.still?0:(P.run?2.9:1.7);P.moving=l>.12&&!P.still;const dx=Math.cos(P.a),dy=Math.sin(P.a),vx=(dx*fz-dy*fx)*sp*dt,vy=(dy*fz+dx*fx)*sp*dt,r=.25;
if(free(P.x+vx+Math.sign(vx)*r,P.y))P.x+=vx;if(free(P.x,P.y+vy+Math.sign(vy)*r))P.y+=vy;
if(P.moving){P.st+=dt*(P.run?3.2:2);if((P.st|0)!==((P.st-dt*(P.run?3.2:2))|0))tn(60+R()*30,.08,'square',P.run?.06:.025)}}
function caught(t){S='dead';jump={m:t,t0:T};if(document.pointerLockElement)document.exitPointerLock();scream();$('ctl').classList.add('hide');$('top').classList.add('hide');$('guide').classList.add('hide');
setTimeout(()=>{$('eT').textContent='Ты стал новой игрушкой';$('eD').textContent='Подсказка Проводника: '+MD[t].d;$('end').classList.remove('hide');jump=null;say(MD[t].d,t)},1700)}
function win(){S='win';speechSynthesis&&speechSynthesis.cancel();$('ctl').classList.add('hide');$('top').classList.add('hide');tn(400,1.5,'triangle',.2,900,1);$('eT').textContent='ПОБЕГ';$('eD').textContent='Дверь захлопнулась за спиной. Игрушки замолчали… но надолго ли?';$('end').classList.remove('hide');say('Ты выбрался. Но фабрика всегда ждёт нового гостя.')}
function box(){if(S!=='play'||boxes<=0||decoy.t>0)return;boxes--;let x=P.x,y=P.y;const dx=Math.cos(P.a),dy=Math.sin(P.a);for(let d=.5;d<=5;d+=.5){const nx=P.x+dx*d,ny=P.y+dy*d;if(!free(nx,ny))break;x=nx;y=ny}decoy.x=x;decoy.y=y;decoy.t=7;decoy.s=0;tn(660,.4,'triangle',.1,0,1)}
function lightT(){if(S==='play'||S==='intro'){light=!light;tn(light?900:500,.06,'square',.05)}}
/* ---------- ОБНОВЛЕНИЕ ---------- */
function upd(dt){updP(dt);let md=99;for(const m of mons){const d=upMon(m,dt);if(m.t!==1||!m.seen)md=Math.min(md,d)}if(S!=='play')return;thr=md;
if(decoy.t>0){decoy.t-=dt;decoy.s-=dt;if(decoy.s<=0){decoy.s=.5;tn([523,466,392,415][R()*4|0],.5,'triangle',.07,0,1)}}
for(let i=gears.length-1;i>=0;i--)if(Math.hypot(gears[i].x-P.x,gears[i].y-P.y)<.6){gears.splice(i,1);got++;tn(900,.3,'triangle',.15,1500);if(got===3){exOpen=true;say('Все три шестерёнки у тебя! Выход открыт. Беги в юго-восточный угол.')}else say('Шестерёнка '+got+' из 3. Тише…')}
if(P.y>N-1.45&&(P.x|0)===N-2){if(exOpen)win();else{$('t2').textContent='Нужны 3 шестерёнки';}}
fakeT-=dt;if(fakeT<=0){fakeT=45+R()*40;fk=.3;fm=R()*3|0;scream();nz(.3,.2)}
$('rd').style.opacity=thr<7?(1-thr/7)*(.5+.3*Math.sin(T*8)):0}
/* ---------- РЕНДЕР ---------- */
function render(t){const hf=H/2;g.fillStyle=cg;g.fillRect(0,0,W,hf);g.fillStyle=fg;g.fillRect(0,hf,W,H-hf);
const dx=Math.cos(P.a),dy=Math.sin(P.a),px=-dy*.66,py=dx*.66,far=light?10:6.5;
for(let x=0;x<W;x++){const cx=2*x/W-1,rx=dx+px*cx,ry=dy+py*cx;let mx=P.x|0,my=P.y|0;const ddx=Math.abs(1/rx),ddy=Math.abs(1/ry);let sx,sy,sdx,sdy;
if(rx<0){sx=-1;sdx=(P.x-mx)*ddx}else{sx=1;sdx=(mx+1-P.x)*ddx}if(ry<0){sy=-1;sdy=(P.y-my)*ddy}else{sy=1;sdy=(my+1-P.y)*ddy}
let side=0,hit=0,it=0;while(!hit&&it++<64){if(sdx<sdy){sdx+=ddx;mx+=sx;side=0}else{sdy+=ddy;my+=sy;side=1}if(mx<0||my<0||mx>=N||my>=N){hit=1;mx=Math.max(0,Math.min(N-1,mx));my=Math.max(0,Math.min(N-1,my));break}hit=map[my][mx]}
const perp=Math.max(.05,side?sdy-ddy:sdx-ddx);let wx=side?P.x+perp*rx:P.y+perp*ry;wx-=Math.floor(wx);
const lh=W*.95/perp,top=hf-lh/2;let col;if(hit===3)col=exOpen?[40,190+Math.sin(t*6)*50,90]:[130,30,30];else col=PAL[sty[my][mx]];
let b=Math.pow(Math.max(0,1-perp/far),1.5)*fl+.03;if(side)b*=.72;if(hit===1&&(wx*5)%1<.07)b*=.55;if(sty[my][mx]===2&&hit===1&&(wx*3)%1<.5)b*=.8;
g.fillStyle=`rgb(${col[0]*b|0},${col[1]*b|0},${col[2]*b|0})`;g.fillRect(x,top,1,lh+1);
if(hit===1){g.fillStyle='rgba(0,0,0,.35)';for(let k=1;k<4;k++)g.fillRect(x,top+lh*k/4,1,Math.max(1,lh*.03))}zb[x]=perp}
const list=[];mons.forEach(m=>list.push({x:m.x,y:m.y,s:SP[m.t],sc:[1.15,.95,1.2][m.t]}));gears.forEach(o=>list.push({x:o.x,y:o.y,s:GEAR,sc:.35}));if(decoy.t>0)list.push({x:decoy.x,y:decoy.y,s:BOX,sc:.3});
list.forEach(o=>o.d=(o.x-P.x)**2+(o.y-P.y)**2);list.sort((a,b)=>b.d-a.d);const inv=1/(px*dy-dx*py);
for(const o of list){const sx=o.x-P.x,sy=o.y-P.y,tx=inv*(dy*sx-dx*sy),ty=inv*(-py*sx+px*sy);if(ty<.15)continue;const scr=W/2*(1+tx/ty),full=W*.95/ty,size=full*o.sc,bottom=hf+full/2,top=bottom-size;
g.globalAlpha=Math.min(1,(Math.pow(Math.max(0,1-ty/far),1.3)*fl+.05)*1.6);const x0=Math.max(0,Math.floor(scr-size/2)),x1=Math.min(W-1,Math.ceil(scr+size/2));
for(let x=x0;x<=x1;x++){if(ty>=zb[x])continue;g.drawImage(o.s,((x-(scr-size/2))/size*128)|0,0,1,128,x,top,1,size)}}g.globalAlpha=1;
if(fk>0){g.save();g.globalAlpha=.85;g.imageSmoothingEnabled=false;const s=Math.max(W,H);g.drawImage(SP[fm],W/2-s*.45+(R()-.5)*10,H/2-s*.45,s*.9,s*.9);g.restore()}
if(jump){const k=t-jump.t0,s=Math.max(W,H)*(1.2+k*.5);g.save();g.fillStyle=Math.floor(t*30)%2?'#600':'#000';g.fillRect(0,0,W,H);g.imageSmoothingEnabled=false;g.translate((R()-.5)*16,(R()-.5)*16);g.drawImage(SP[jump.m],W/2-s/2,H/2-s*.5,s,s);g.restore()}}
let last=performance.now(),T=0;
function loop(n){requestAnimationFrame(loop);const dt=Math.min(.05,(n-last)/1000);last=n;T+=dt;
if(S==='play')upd(dt);else if(S==='menu')P.a+=dt*.12;
fl+=(1-fl)*Math.min(1,dt*6);if(R()<dt*.5)fl=.25;if(fk>0)fk-=dt;
render(T);if(S==='play'||S==='intro'){$('t1').textContent='⚙ '+got+'/3 · 🎵×'+boxes+(light?' · 🔦':'');if(S==='play'&&P.y<N-1.45)$('t2').textContent=exOpen?'Выход: юго-восток ↘':'Найди шестерёнки'}
$('bL').classList.toggle('on',!!light)}
/* ---------- ВВОД ---------- */
addEventListener('keydown',e=>{keys[e.code]=1;if(e.code==='KeyF')lightT();if(e.code==='KeyQ')box();if(e.code==='KeyV')P.a+=Math.PI});addEventListener('keyup',e=>keys[e.code]=0);
cv.addEventListener('pointerdown',e=>{au();if(S!=='play')return;lp={id:e.pointerId,x:e.clientX};try{cv.setPointerCapture(e.pointerId)}catch(x){}});
cv.addEventListener('pointermove',e=>{if(S!=='play'||!lp||lp.id!==e.pointerId)return;P.a+=(e.clientX-lp.x)*.009*sens;lp.x=e.clientX});
['pointerup','pointercancel'].forEach(v=>cv.addEventListener(v,()=>lp=null));
const jz=$('jz');function jm(e){const r=jz.getBoundingClientRect();let x=(e.clientX-r.left-65)/45,y=(e.clientY-r.top-65)/45;const l=Math.hypot(x,y);if(l>1){x/=l;y/=l}jx=x;jy=y;$('jk').style.transform=`translate(${x*40}px,${y*40}px)`}
jz.addEventListener('pointerdown',e=>{au();jz.setPointerCapture(e.pointerId);jm(e)});jz.addEventListener('pointermove',e=>{if(jz.hasPointerCapture(e.pointerId))jm(e)});['pointerup','pointercancel'].forEach(v=>jz.addEventListener(v,()=>{jx=jy=0;$('jk').style.transform=''}));
const hold=(id,on,off)=>{const b=$(id);b.addEventListener('pointerdown',e=>{au();b.setPointerCapture(e.pointerId);on()});['pointerup','pointercancel'].forEach(v=>b.addEventListener(v,off||(()=>{})))};
hold('bR',()=>rB=1,()=>rB=0);hold('bF',()=>fB=1,()=>fB=0);hold('bL',lightT);hold('bB',box);hold('bT',()=>{if(S==='play')P.a+=Math.PI});
/* ---------- ПОТОК ИГРЫ ---------- */
async function tutorial(){S='intro';skipAll=0;$('skip').classList.remove('hide');
const L=[['Приветствую, живой. Я Проводник. Когда-то я собирал здесь игрушки.'],['Фабрику закрыли в ночь, когда игрушки ожили. Теперь они ищут тех, кто нарушает тишину.'],['Твоя цель: найти три золотые шестерёнки и открыть выход в юго-восточном углу.'],
[touch?'Левый джойстик — идти. Води пальцем по экрану — поворачиваться. Кнопки справа: фонарик, шкатулка, замри, бег.':'WASD — идти, стрелки или мышь — поворот, Shift — бег, F — фонарик, Q — шкатулка, Пробел — замри.'],
['Первый — мишка Топтыжка. Он слеп, зато слышит бег и тяжёлые шаги. Брось музыкальную шкатулку, и он уйдёт на мелодию.',0],['Вторая — кукла Мари. Она быстрая, но замирает, пока ты на неё смотришь. В темноте включай фонарик и не моргай.',1],
['Третий — клоун Хихи. Он видит только движение. Услышишь смех — замри, и он пройдёт мимо.',2],['Удачи. И помни: тише… они слышат всё.']];
for(const[l,c]of L){if(skipAll)break;await say(l,c)}$('skip').classList.add('hide');$('guide').classList.add('hide');speechSynthesis&&speechSynthesis.cancel();seenTut=true;S='play'}
function begin(tut){au();$('menu').classList.add('hide');$('end').classList.add('hide');$('top').classList.remove('hide');$('ctl').classList.remove('hide');newMap();$('t2').textContent='';if(tut)tutorial();else{S='play';say('Они уже ищут тебя. Три шестерёнки и выход на юго-востоке.')}}
$('mP').onclick=()=>begin(true);$('mQ').onclick=()=>begin(false);$('eA').onclick=()=>begin(!seenTut);
$('eM').onclick=()=>{$('end').classList.add('hide');$('menu').classList.remove('hide');S='menu';newMap()};
$('skip').onclick=()=>{skipAll=1;skipR&&skipR()};
$('mS').onclick=()=>{au();audioOn=!audioOn;mg.gain.value=audioOn?1:0;if(!audioOn)speechSynthesis&&speechSynthesis.cancel();$('mS').textContent='ЗВУК И ГОЛОС: '+(audioOn?'ВКЛ':'ВЫКЛ')};
$('mB').onclick=()=>{$('menu').classList.add('hide');$('bes').classList.remove('hide')};$('bK').onclick=()=>{$('bes').classList.add('hide');$('menu').classList.remove('hide')};
MD.forEach((m,i)=>{const d=document.createElement('div');d.className='bc';const c=document.createElement('canvas');c.width=c.height=128;c.getContext('2d').drawImage(SP[i],0,0);d.appendChild(c);const t=document.createElement('div');t.innerHTML='<b>'+m.n+'</b><br>'+m.a+'<br><i>'+m.w+'</i>';d.appendChild(t);$('bl').appendChild(d)});
document.addEventListener('pointerdown',au,{once:true});
newMap();requestAnimationFrame(loop);
</script></body></html>

Game Source: Фабрика ожившего детства

Creator: BlazingBunny60

Libraries: none

Complexity: moderate (186 lines, 26.5 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: game-blazingbunny60" to link back to the original. Then publish at arcadelab.ai/publish.