| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307 |
- (() => {
- 'use strict';
- const modal = document.querySelector('#arcadeModal');
- const canvas = document.querySelector('#arcadeCanvas');
- const ctx = canvas.getContext('2d');
- const ui = name => document.querySelector('#arcade' + name);
- const W = 360, H = 480;
- const held = new Set(), pointers = new Map();
- let mode, state, paused = false, over = false, raf = 0, last = 0, accumulator = 0, best = 0;
- const random = (a,b) => a + Math.random() * (b-a);
- const clamp = (v,a,b) => Math.max(a,Math.min(b,v));
- function record() {
- if (state.score > best) {
- best = state.score;
- try { localStorage.setItem('zke-arcade-' + mode, String(best)); } catch {}
- }
- ui('Score').textContent = 'Счёт: ' + state.score;
- if (mode === 'jump') ui('Score').textContent += ' · Уровень ' + (1 + Math.floor(difficulty(state.score) * 5));
- ui('Best').textContent = 'Рекорд: ' + best;
- }
- function end(message) {
- over = true; record(); held.clear(); pointers.clear();
- ui('Status').textContent = message + ' Счёт: ' + state.score;
- ui('Pause').disabled = true;
- }
- function pause(value = !paused) {
- if (over || !modal.open) return;
- paused = value; held.clear(); pointers.clear();
- ui('Pause').textContent = paused ? 'Продолжить' : 'Пауза';
- ui('Status').textContent = paused ? 'Пауза' : 'Игра идёт';
- last = 0; accumulator = 0; draw();
- }
- function restart() {
- cancelAnimationFrame(raf); paused = false; over = false; held.clear(); pointers.clear();
- last = 0; accumulator = 0;
- try { best = Number(localStorage.getItem('zke-arcade-' + mode)) || 0; } catch { best = 0; }
- ui('Pause').textContent = 'Пауза'; ui('Pause').disabled = false;
- ui('Status').textContent = 'Игра идёт';
- state = mode === 'snake' ? newSnake() : newJump();
- record(); draw(); raf = requestAnimationFrame(frame); canvas.focus();
- }
- function open(type) {
- mode = type;
- ui('Title').textContent = type === 'jump' ? 'Прыжок оператора' : 'Змейка';
- ui('Help').textContent = type === 'jump'
- ? '← → или A/D — движение · W / Пробел — огонь · P — пауза. Края соединены. Зелёные площадки прочные, голубые движутся, рыжие ломаются, белые исчезают. Пружина ↑, ранец J, щит S.'
- : 'Стрелки или WASD — поворот · P — пауза. Собирайте еду, избегайте стен и своего хвоста.';
- document.querySelectorAll('[data-control]').forEach(button => {
- button.hidden = type === 'jump' ? ['up','down'].includes(button.dataset.control) : button.dataset.control === 'shoot';
- });
- if (!modal.open) modal.showModal();
- restart();
- }
- // All input and animation belong to the open game only.
- document.querySelectorAll('[data-arcade]').forEach(button => button.addEventListener('click', () => open(button.dataset.arcade)));
- ui('Restart').onclick = restart; ui('Pause').onclick = () => pause();
- ui('Close').onclick = () => modal.close();
- modal.addEventListener('close', () => { cancelAnimationFrame(raf); held.clear(); pointers.clear(); last = 0; });
- window.addEventListener('blur', () => pause(true));
- document.addEventListener('visibilitychange', () => { if (document.hidden) pause(true); });
- const keys = { ArrowLeft:'left', ArrowRight:'right', ArrowUp:'up', ArrowDown:'down', KeyA:'left', KeyD:'right', KeyW:'up', KeyS:'down', Space:'shoot' };
- document.addEventListener('keydown', event => {
- if (!modal.open) return;
- if (event.code === 'Escape') return;
- event.stopImmediatePropagation();
- if (event.code === 'KeyP') { event.preventDefault(); if (!event.repeat) pause(); return; }
- const control = mode === 'jump' && event.code === 'KeyW' ? 'shoot' : keys[event.code];
- if (!control) return;
- event.preventDefault();
- if (paused || over) return;
- held.add(event.code);
- if (mode === 'snake') turn(control);
- }, true);
- document.addEventListener('keyup', event => { if (modal.open) held.delete(event.code); });
- document.querySelectorAll('[data-control]').forEach(button => {
- button.addEventListener('pointerdown', event => {
- if (paused || over) return;
- event.preventDefault(); button.setPointerCapture(event.pointerId);
- pointers.set(event.pointerId, button.dataset.control);
- if (mode === 'snake') turn(button.dataset.control);
- });
- const release = event => pointers.delete(event.pointerId);
- button.addEventListener('pointerup', release);
- button.addEventListener('pointercancel', release);
- button.addEventListener('lostpointercapture', release);
- });
- const pressed = key => [...held].some(code => (mode === 'jump' && code === 'KeyW' ? 'shoot' : keys[code]) === key) || [...pointers.values()].includes(key);
- function frame(time) {
- if (!modal.open) return;
- const dt = last ? Math.min((time-last)/1000, .05) : 0; last = time;
- if (!paused && !over) {
- accumulator += dt;
- while (accumulator >= 1/120 && !over) {
- mode === 'jump' ? stepJump(1/120) : stepSnake(1/120);
- accumulator -= 1/120;
- }
- record();
- }
- draw();
- raf = requestAnimationFrame(frame);
- }
- function newSnake() {
- const s = { body:[{x:9,y:12},{x:8,y:12},{x:7,y:12}], dir:{x:1,y:0}, queue:[], elapsed:0, score:0 };
- food(s); return s;
- }
- function food(s) {
- const free = [];
- for(let y=0;y<24;y++) for(let x=0;x<18;x++)
- if(!s.body.some(p => p.x===x && p.y===y)) free.push({x,y});
- s.food = free[Math.floor(Math.random()*free.length)] || null;
- }
- function turn(control) {
- const dirs = {left:{x:-1,y:0},right:{x:1,y:0},up:{x:0,y:-1},down:{x:0,y:1}};
- const next = dirs[control], previous = state.queue.at(-1) || state.dir;
- if (!next || state.queue.length >= 2 || (next.x===previous.x && next.y===previous.y) || (next.x===-previous.x && next.y===-previous.y)) return;
- state.queue.push(next);
- }
- function stepSnake(dt) {
- const s=state; s.elapsed+=dt;
- const interval=Math.max(.065,.17-s.score*.003);
- if(s.elapsed<interval) return;
- s.elapsed-=interval;
- if(s.queue.length) s.dir=s.queue.shift();
- const head={x:s.body[0].x+s.dir.x,y:s.body[0].y+s.dir.y};
- const eating=s.food && head.x===s.food.x && head.y===s.food.y;
- const occupied=eating?s.body:s.body.slice(0,-1);
- if(head.x<0||head.x>=18||head.y<0||head.y>=24||occupied.some(p=>p.x===head.x&&p.y===head.y)) return end('Игра окончена.');
- s.body.unshift(head);
- if(eating) { s.score++; food(s); if(!s.food) end('Всё поле заполнено. Победа!'); }
- else s.body.pop();
- }
- // World Y grows downward; upward progress moves the camera, never the physics.
- function newJump() {
- const s={player:{x:170,y:390,vy:-520}, platforms:[{x:135,y:430,w:90,type:'solid',item:null}], enemies:[], shots:[], debris:[], time:0, camera:0, top:430, anchor:135, score:0, boost:0, shield:0, cooldown:0, generated:0};
- generate(s); return s;
- }
- // Difficulty rises over 600 points, then stays within normal jump reach.
- function difficulty(score) { return clamp(score / 600, 0, 1); }
- function generate(s) {
- const d = difficulty(s.score);
- while(s.top>s.camera-110) {
- s.top-=random(50+24*d,68+18*d); s.generated++;
- s.anchor=clamp(s.anchor+random(-60-10*d,60+10*d),12,W-88);
- const roll=Math.random();
- // Permanent landing in every row keeps the main route reachable.
- const platform={x:s.anchor,y:s.top,w:86-30*d,type:'solid',item:null};
- if(s.generated>5 && roll<.12+.48*d) { platform.type='moving'; platform.base=platform.x; platform.phase=random(0,6); platform.speed=1+1.3*d; }
- if(s.generated%8===0) platform.item=['spring','jet','shield'][Math.floor(Math.random()*3)];
- s.platforms.push(platform);
- if(s.generated>5 && Math.random()<.25+.4*d) s.platforms.push({x:clamp(s.anchor+(s.anchor<180?130:-130),0,300),y:s.top+25,w:56-10*d,type:Math.random()<.5?'break':'vanish',item:null});
- const enemyInterval = Math.max(3,8-Math.floor(d*5));
- if(s.generated>14 && s.generated%enemyInterval===0) s.enemies.push({x:s.anchor<180?280:55,y:s.top-30,base:s.anchor<180?280:55,phase:random(0,6),r:16,speed:1.5+2*d,range:18+20*d});
- }
- }
- function stepJump(dt) {
- const s=state,p=s.player;
- s.time += dt;
- s.debris.forEach(d => { d.life -= dt; d.x += d.vx*dt; d.y += d.vy*dt; d.vy += d.gravity*dt; d.angle += d.spin*dt; });
- s.debris = s.debris.filter(d => d.life > 0);
- s.boost=Math.max(0,s.boost-dt); s.shield=Math.max(0,s.shield-dt); s.cooldown-=dt;
- const speed=240;
- p.x+=((pressed('right')?1:0)-(pressed('left')?1:0))*speed*dt;
- if(p.x>W) p.x=-24; if(p.x<-24) p.x=W;
- const oldBottom=p.y+30;
- p.vy=s.boost>0?-640:p.vy+1450*dt;
- p.y+=p.vy*dt;
- s.platforms.forEach(a => {
- if(a.type==='moving') { a.phase+=dt*(a.speed||1); a.x=clamp(a.base+Math.sin(a.phase)*22,0,W-a.w); }
- });
- if(p.vy>0) {
- const landing=s.platforms.filter(a=>!a.dead&&oldBottom<=a.y&&p.y+30>=a.y&&p.x+24>a.x&&p.x<a.x+a.w).sort((a,b)=>a.y-b.y)[0];
- if(landing) {
- if(landing.type==='break') shatter(landing);
- else {
- p.y=landing.y-30; p.vy=-520;
- if(landing.type==='vanish') shatter(landing);
- if(landing.item==='spring') p.vy=-850;
- if(landing.item==='jet') s.boost=2.5;
- if(landing.item==='shield') s.shield=7;
- if(landing.item!=='spring') landing.item=null;
- }
- }
- }
- if(pressed('shoot')&&s.cooldown<=0) { s.shots.push({x:p.x+12,y:p.y}); s.cooldown=.2; }
- s.shots.forEach(b=>b.y-=750*dt);
- s.enemies.forEach(e=>{
- e.phase+=dt*(e.speed||2); e.x=e.base+Math.sin(e.phase)*(e.range||22);
- s.shots.forEach(b=>{ if(!b.dead&&!e.dead&&Math.hypot(b.x-e.x,b.y-e.y)<e.r+6) {e.dead=true;b.dead=true;} });
- if(!e.dead&&p.x+24>e.x-e.r&&p.x<e.x+e.r&&p.y+30>e.y-e.r&&p.y<e.y+e.r) {
- if(s.shield>0||s.boost>0) e.dead=true;
- else if(p.vy>0&&oldBottom<=e.y) {e.dead=true;p.vy=-520;}
- else end('Столкновение с охранным дроном.');
- }
- });
- s.camera=Math.min(s.camera,p.y-H*.42);
- s.score=Math.max(s.score,Math.floor(-s.camera/10));
- s.platforms=s.platforms.filter(a=>!a.dead&&a.y<s.camera+H+70);
- s.enemies=s.enemies.filter(e=>!e.dead&&e.y<s.camera+H+60);
- s.shots=s.shots.filter(b=>!b.dead&&b.y>s.camera-30);
- generate(s);
- if(p.y>s.camera+H+30) end('Падение. Попробуйте ещё раз!');
- }
- function shatter(platform) {
- if(platform.dead) return;
- platform.dead=true;
- const ghost=platform.type==='vanish';
- for(let i=0;i<8;i++) {
- state.debris.push({
- x:platform.x+(i+.5)*platform.w/8,y:platform.y+5,
- w:platform.w/8-1,h:ghost?5:10,
- vx:(i-3.5)*random(12,24),vy:ghost?random(-65,-15):random(-120,-45),
- gravity:ghost?30:550,angle:0,spin:random(-5,5),
- life:ghost?.65:1.1,color:ghost?'#b2e9ea':'#bd8151',
- highlight:ghost?'#ffffff':'#f5c68a'
- });
- }
- }
- function rounded(x,y,w,h,r,color) {
- ctx.fillStyle=color; ctx.beginPath(); ctx.roundRect(x,y,w,h,r); ctx.fill();
- }
- function drawOperator(x,y,vy,cooldown) {
- // Outlined suit, helmet visor and boots retain the 24 × 30 collision silhouette.
- ctx.save(); ctx.translate(x+12,y+15);
- ctx.rotate(clamp(((pressed('right')?1:0)-(pressed('left')?1:0))*.09,-.09,.09));
- rounded(-12,-3,24,18,5,'#15262a');
- rounded(-10,-2,20,15,4,'#e6d5a5');
- rounded(-14,0,5,10,2,'#789a83'); rounded(9,0,5,10,2,'#789a83');
- rounded(-9,10,7,5,2,'#405a5f'); rounded(2,10,7,5,2,'#405a5f');
- rounded(-12,-16,24,20,8,'#15262a');
- rounded(-10,-15,20,18,7,'#f2e7c9');
- rounded(-8,-12,16,11,4,'#255769');
- rounded(-6,-11,12,4,2,'#67bbcd');
- rect(-5,-10,4,2,'#d9faff');
- rounded(-6,4,12,6,2,'#526c64');
- circle(-3,7,1.3,'#c9ff6b'); circle(2,7,1.3,'#ffa967');
- rect(-1,-18,3,5,'#b8cbd1');
- if(cooldown>.14) { circle(.5,-20,4,'#ffe98a'); circle(.5,-20,2,'#fffbea'); }
- ctx.restore();
- }
- function drawDrone(x,y,time) {
- ctx.save(); ctx.translate(x,y);
- // Wide rotor arms, armoured shell and one red sensor distinguish enemies.
- rect(-25,-6,50,4,'#738395');
- [-23,23].forEach(dx=>{
- rect(dx-2,-12,4,10,'#a0b0b9');
- const span=8+Math.sin(time*50)*4;
- rounded(dx-span,-13,span*2,3,1,'#c1d6df');
- });
- rounded(-17,-12,34,26,9,'#18222f');
- rounded(-15,-10,30,22,8,'#718496');
- rounded(-12,-9,24,6,3,'#afbec8');
- rounded(-12,-3,24,11,5,'#293345');
- circle(0,2,7,'#752a46'); circle(0,2,4.5,'#ff526e'); circle(-1,0,1.7,'#ffe4d1');
- rect(-10,11,5,4,'#344959'); rect(5,11,5,4,'#344959');
- rect(-11,-5,3,2,'#ffcb77'); rect(8,-5,3,2,'#ffcb77');
- ctx.restore();
- }
- function rect(x,y,w,h,color) {ctx.fillStyle=color;ctx.fillRect(x,y,w,h);}
- function circle(x,y,r,color) {ctx.fillStyle=color;ctx.beginPath();ctx.arc(x,y,r,0,Math.PI*2);ctx.fill();}
- function draw() {
- rect(0,0,W,H,'#0d191c');
- ctx.strokeStyle='#1a3033';ctx.lineWidth=1;
- const grid = mode === 'snake' ? 20 : 24;
- const offset=mode==='jump'?((-state.camera)%grid):0;
- for(let x=0;x<W;x+=grid){ctx.beginPath();ctx.moveTo(x,0);ctx.lineTo(x,H);ctx.stroke();}
- for(let y=offset;y<H;y+=grid){ctx.beginPath();ctx.moveTo(0,y);ctx.lineTo(W,y);ctx.stroke();}
- if(mode==='snake') drawSnake(); else drawJump();
- if(paused||over) {
- rect(0,0,W,H,'rgba(4,10,12,.72)');
- ctx.fillStyle='#dfffbd';ctx.font='bold 24px monospace';ctx.textAlign='center';
- ctx.fillText(paused?'ПАУЗА':'КОНЕЦ ИГРЫ',W/2,H/2-10);
- ctx.font='14px monospace';ctx.fillText('Счёт: '+state.score,W/2,H/2+22);ctx.textAlign='left';
- }
- }
- function drawSnake() {
- const s=state;
- if(s.food) {circle(s.food.x*20+10,s.food.y*20+10,7,'#ff965e');}
- s.body.forEach((p,i)=>rect(p.x*20+1,p.y*20+1,18,18,i===0?'#dfff91':'#77b86c'));
- const h=s.body[0];rect(h.x*20+5,h.y*20+5,3,3,'#13201c');rect(h.x*20+12,h.y*20+5,3,3,'#13201c');
- }
- function drawJump() {
- const s=state,p=s.player,cy=s.camera;
- s.platforms.forEach(a=>{
- const y=a.y-cy;
- rect(a.x,y,a.w,9,({solid:'#9ce578',moving:'#65c5e6',break:'#c68d62',vanish:'#e0e9df'})[a.type]);
- rect(a.x+3,y+9,a.w-6,3,'#38524c');
- if(a.type==='break') {ctx.strokeStyle='#302522';ctx.beginPath();ctx.moveTo(a.x+20,y);ctx.lineTo(a.x+29,y+5);ctx.lineTo(a.x+22,y+9);ctx.stroke();}
- if(a.item) {rect(a.x+a.w/2-9,y-20,18,18,a.item==='spring'?'#ffe38a':a.item==='jet'?'#ff986e':'#8cdeed');ctx.fillStyle='#152322';ctx.font='bold 14px monospace';ctx.fillText(({spring:'↑',jet:'J',shield:'S'})[a.item],a.x+a.w/2-4,y-6);}
- });
- s.debris.forEach(d => {
- ctx.save(); ctx.globalAlpha = Math.min(1,d.life/.35);
- ctx.translate(d.x,d.y-cy); ctx.rotate(d.angle);
- rect(-d.w/2,-d.h/2,d.w,d.h,d.color);
- rect(-d.w/2,-d.h/2,d.w,2,d.highlight);
- ctx.restore();
- });
- s.enemies.forEach(e=>drawDrone(e.x,e.y-cy,s.time));
- s.shots.forEach(b=>rect(b.x-2,b.y-cy,4,12,'#fff2a1'));
- const y=p.y-cy;
- if(s.shield>0){ctx.strokeStyle='#8cdeed';ctx.lineWidth=2;ctx.beginPath();ctx.arc(p.x+12,y+15,25,0,Math.PI*2);ctx.stroke();}
- if(s.boost>0){rect(p.x+5,y+28,14,18+Math.sin(performance.now()/50)*5,'#ff965e');}
- drawOperator(p.x,y,p.vy,s.cooldown);
- ctx.fillStyle='#95b5aa';ctx.font='11px monospace';
- if(s.boost>0)ctx.fillText('РАНЕЦ '+s.boost.toFixed(1),10,20);
- else if(s.shield>0)ctx.fillText('ЩИТ '+Math.ceil(s.shield),10,20);
- }
- })();
|