Красный Салик
by TurboGamer88354 lines17.7 KB
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Красный Салик</title>
<style>
*{box-sizing:border-box;-webkit-tap-highlight-color:transparent}
html,body{margin:0;padding:0;height:100%;overflow:hidden;background:#0a0a1a;font-family:Arial;touch-action:none;user-select:none;-webkit-user-select:none}
#wrap{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;width:100%}
canvas{border:3px solid #e63946;border-radius:8px;box-shadow:0 0 30px rgba(230,57,70,.5);background:#1a1a2e;display:block;max-width:100vw;max-height:80vh}
#hint{color:#e63946;font-size:12px;text-align:center;padding:6px;text-shadow:0 0 6px #000}
#touch{display:flex;justify-content:space-between;width:100%;max-width:900px;padding:10px 20px;gap:10px}
.btn{width:70px;height:70px;border-radius:50%;background:rgba(230,57,70,.35);border:3px solid #e63946;color:#fff;font-size:28px;display:flex;align-items:center;justify-content:center;user-select:none;-webkit-user-select:none;touch-action:none;flex-shrink:0}
.btn:active{background:rgba(230,57,70,.75)}
.group{display:flex;gap:12px}
</style>
</head>
<body>
<div id="wrap">
<div id="hint">← → / A D — движение | ПРОБЕЛ / ↑ — прыжок | R — рестарт</div>
<canvas id="game" width="900" height="550"></canvas>
<div id="touch">
<div class="group"><div class="btn" id="left">◀</div><div class="btn" id="right">▶</div></div>
<div class="btn" id="jump">▲</div>
</div>
</div>
<script>
(function(){
"use strict";
const canvas=document.getElementById('game'),ctx=canvas.getContext('2d');
const W=900,H=550,GRAVITY=0.65,GROUND_Y=470;
const keys={left:false,right:false,jump:false,enter:false};
// ==== ФИКС: кнопки не залипают, canvas не теряет фокус ====
function bindBtn(id,key){
const el=document.getElementById(id);
const setOn=e=>{e.preventDefault();e.stopPropagation();keys[key]=true;};
const setOff=e=>{e.preventDefault();e.stopPropagation();keys[key]=false;};
el.addEventListener('touchstart',setOn,{passive:false});
el.addEventListener('touchend',setOff,{passive:false});
el.addEventListener('touchcancel',setOff,{passive:false});
el.addEventListener('mousedown',setOn);
el.addEventListener('mouseup',setOff);
el.addEventListener('mouseleave',setOff);
}
bindBtn('left','left');bindBtn('right','right');bindBtn('jump','jump');
document.addEventListener('keydown',e=>{
const k=e.key.toLowerCase();
if(k==='arrowleft'||k==='a'){keys.left=true;e.preventDefault();}
if(k==='arrowright'||k==='d'){keys.right=true;e.preventDefault();}
if(k==='arrowup'||k==='w'||k===' '){keys.jump=true;e.preventDefault();}
if(k==='enter'){keys.enter=true;}
if(k==='r'){restart();}
},{passive:false});
document.addEventListener('keyup',e=>{
const k=e.key.toLowerCase();
if(k==='arrowleft'||k==='a')keys.left=false;
if(k==='arrowright'||k==='d')keys.right=false;
if(k==='arrowup'||k==='w'||k===' ')keys.jump=false;
if(k==='enter')keys.enter=false;
});
// фикс: при потере фокуса отпускаем всё
window.addEventListener('blur',()=>{keys.left=keys.right=keys.jump=keys.enter=false;});
document.addEventListener('visibilitychange',()=>{if(document.hidden){keys.left=keys.right=keys.jump=keys.enter=false;}});
canvas.addEventListener('click',()=>{initAudio();if(audioCtx.state==='suspended')audioCtx.resume();canvas.focus();});
// ==== звук ====
let audioCtx=null;
function initAudio(){if(!audioCtx){try{audioCtx=new (window.AudioContext||window.webkitAudioContext)();}catch(e){}}}
function tone(f,d,type,v){if(!audioCtx)return;try{const o=audioCtx.createOscillator(),g=audioCtx.createGain();o.type=type||'square';o.frequency.value=f;g.gain.value=v||0.05;o.connect(g);g.connect(audioCtx.destination);o.start();o.stop(audioCtx.currentTime+d);}catch(e){}}
const sfx={
jump(){initAudio();tone(440,0.08,'square',0.06);setTimeout(()=>tone(660,0.08,'square',0.06),40);},
hit(){initAudio();tone(180,0.12,'sawtooth',0.07);},
star(){initAudio();tone(880,0.06,'sine',0.07);setTimeout(()=>tone(1320,0.08,'sine',0.07),50);},
kill(){initAudio();tone(300,0.1,'square',0.08);setTimeout(()=>tone(200,0.12,'square',0.08),60);},
explode(){initAudio();tone(80,0.3,'sawtooth',0.1);},
bossHit(){initAudio();tone(150,0.15,'sawtooth',0.09);},
win(){initAudio();[523,659,784,1046].forEach((f,i)=>setTimeout(()=>tone(f,0.2,'triangle',0.08),i*120));}
};
const melody=[523,587,659,523,659,587,523,440,523,587,659,587,523,440,392,440];
let mi=0;
setInterval(()=>{if(!audioCtx)return;tone(melody[mi%melody.length],0.12,'triangle',0.016);mi++;},260);
// ==== игрок ====
const player={x:80,y:400,w:38,h:52,vx:0,vy:0,speed:5,jumpPower:-13,onGround:false,facing:1,lives:3,invuln:0};
// ==== состояние ====
let level=1,levelComplete=false,bossDefeated=false,message="",messageTimer=0,gameOver=false;
let platforms=[],stars=[],enemies=[],boss=null,totalStars=0;
function showMessage(t,time){message=t;messageTimer=time||120;}
// ==== враги ====
// голубой — ходит по земле, 1 HP
function blueEnemy(x,y){return{x:x,y:y,w:34,h:34,vx:1.6,vy:0,hp:1,maxHp:1,color:'#4fc3f7',alive:true,type:'blue',dir:1,pl:x-120,pr:x+120};}
// золотой — ходит по земле, 2 HP
function goldEnemy(x,y){return{x:x,y:y,w:38,h:38,vx:2,vy:0,hp:2,maxHp:2,color:'#ffd700',alive:true,type:'gold',dir:1,pl:x-120,pr:x+120};}
// чёрная бомба — круглая, взрывается
function bomb(x,y){return{x:x,y:y,w:30,h:30,vx:1.2,vy:0,hp:1,maxHp:1,color:'#111',alive:true,type:'bomb',fuse:150};}
// ==== уровни ====
function buildLevel(n){
platforms=[{x:0,y:GROUND_Y,w:W,h:80,ground:true}];
stars=[];enemies=[];boss=null;
if(n===1){
platforms.push({x:220,y:380,w:140,h:18},{x:440,y:310,w:140,h:18},{x:680,y:250,w:140,h:18});
stars=[{x:290,y:340},{x:510,y:270},{x:750,y:210},{x:120,y:420}];
enemies=[blueEnemy(380,GROUND_Y-34),blueEnemy(620,GROUND_Y-34),bomb(300,220)];
}else if(n===2){
platforms.push({x:100,y:390,w:120,h:18},{x:300,y:330,w:120,h:18},{x:520,y:380,w:120,h:18},{x:700,y:300,w:140,h:18});
stars=[{x:160,y:350},{x:360,y:290},{x:580,y:340},{x:770,y:260},{x:60,y:420}];
enemies=[blueEnemy(450,GROUND_Y-34),goldEnemy(650,GROUND_Y-38),blueEnemy(200,GROUND_Y-34),bomb(500,200)];
}else if(n===3){
platforms.push({x:80,y:400,w:100,h:18},{x:240,y:340,w:100,h:18},{x:400,y:280,w:100,h:18},{x:560,y:340,w:100,h:18},{x:720,y:400,w:100,h:18});
stars=[{x:130,y:360},{x:290,y:300},{x:450,y:240},{x:610,y:300},{x:770,y:360},{x:60,y:420}];
enemies=[blueEnemy(350,GROUND_Y-34),goldEnemy(550,GROUND_Y-38),blueEnemy(750,GROUND_Y-34),bomb(200,180),bomb(680,180)];
}else if(n===4){
platforms.push({x:60,y:400,w:100,h:18},{x:220,y:340,w:100,h:18},{x:380,y:280,w:100,h:18},{x:540,y:340,w:100,h:18},{x:700,y:280,w:140,h:18},{x:400,y:180,w:120,h:18});
stars=[{x:110,y:360},{x:270,y:300},{x:430,y:240},{x:590,y:300},{x:770,y:240},{x:460,y:140}];
enemies=[goldEnemy(300,GROUND_Y-38),goldEnemy(550,GROUND_Y-38),blueEnemy(750,GROUND_Y-34),bomb(450,200),bomb(250,180),blueEnemy(150,GROUND_Y-34)];
}else if(n===5){
platforms.push({x:60,y:380,w:120,h:18},{x:720,y:380,w:120,h:18},{x:400,y:300,w:100,h:18});
stars=[];
boss={x:W/2-80,y:140,w:160,h:140,hp:8,maxHp:8,vx:2.2,dir:1,shootTimer:70,projectiles:[],hitFlash:0};
enemies=[blueEnemy(200,GROUND_Y-34),blueEnemy(700,GROUND_Y-34)];
}
}
function restart(){
player.lives=3;totalStars=0;levelComplete=false;bossDefeated=false;gameOver=false;
loadLevel(1);
}
function loadLevel(n){
level=n;levelComplete=false;bossDefeated=false;gameOver=false;
player.x=80;player.y=GROUND_Y-player.h;player.vy=0;player.vx=0;player.onGround=false;player.invuln=60;
buildLevel(n);
showMessage(n===5?"⚠ ФИНАЛЬНЫЙ ЧЁРНЫЙ БОСС ⚠":"Уровень "+n,90);
}
loadLevel(1);
function die(){
player.lives--;sfx.hit();
if(player.lives<=0){gameOver=true;showMessage("💀 Игра окончена. Нажми R",999);}
else{player.x=80;player.y=GROUND_Y-player.h;player.vy=0;player.vx=0;player.invuln=90;}
}
// ==== UPDATE ====
function update(){
if(messageTimer>0)messageTimer--;
if(keys.enter){keys.enter=false;
if(levelComplete&&level<5)loadLevel(level+1);
}
if(gameOver||bossDefeated)return;
// движение
player.vx=0;
if(keys.left){player.vx=-player.speed;player.facing=-1;}
if(keys.right){player.vx=player.speed;player.facing=1;}
if(keys.jump&&player.onGround){player.vy=player.jumpPower;player.onGround=false;sfx.jump();}
player.vy+=GRAVITY;if(player.vy>18)player.vy=18;
player.x+=player.vx;
if(player.x<0)player.x=0;
if(player.x+player.w>W)player.x=W-player.w;
player.y+=player.vy;
player.onGround=false;
for(const p of platforms){
if(player.vy>=0&&player.x+player.w>p.x+2&&player.x<p.x+p.w-2&&player.y+player.h>=p.y&&player.y+player.h<=p.y+p.h+Math.abs(player.vy)+6){
player.y=p.y-player.h;player.vy=0;player.onGround=true;
}
}
if(player.y>H+80)die();
if(player.invuln>0)player.invuln--;
// звёзды
for(let i=stars.length-1;i>=0;i--){
const s=stars[i];
if(Math.abs(player.x+player.w/2-s.x)<28&&Math.abs(player.y+player.h/2-s.y)<32){stars.splice(i,1);totalStars++;sfx.star();}
}
// враги
for(const e of enemies){
if(!e.alive)continue;
if(e.type==='bomb'){
// бомба просто падает и катится, потом взрывается
e.x+=e.vx;e.vy+=GRAVITY*0.4;e.y+=e.vy;
for(const p of platforms){
if(e.vy>0&&e.x+e.w>p.x&&e.x<p.x+p.w&&e.y+e.h>=p.y&&e.y+e.h<=p.y+p.h+8){
e.y=p.y-e.h;e.vy=0;
}
}
if(e.x<20||e.x+e.w>W-20)e.vx*=-1;
e.fuse--;
if(e.fuse<=0){
e.alive=false;sfx.explode();
if(Math.abs(player.x+player.w/2-(e.x+e.w/2))<90&&Math.abs(player.y+player.h/2-(e.y+e.h/2))<90&&player.invuln<=0)die();
}
if(player.x+player.w>e.x&&player.x<e.x+e.w&&player.y+player.h>e.y&&player.y<e.y+e.h&&player.invuln<=0)die();
} else {
// голубые/золотые ходят по земле
e.x+=e.dir*e.vx;
if(e.x<e.pl||e.x+e.w>e.pr)e.dir*=-1;
if(e.x<0){e.x=0;e.dir=1;}
if(e.x+e.w>W){e.x=W-e.w;e.dir=-1;}
// гравитация для наземных
e.vy+=GRAVITY;e.y+=e.vy;
let onPlat=false;
for(const p of platforms){
if(e.vy>=0&&e.x+e.w>p.x&&e.x<p.x+p.w&&e.y+e.h>=p.y&&e.y+e.h<=p.y+p.h+Math.abs(e.vy)+6){
e.y=p.y-e.h;e.vy=0;onPlat=true;
}
}
if(!onPlat&&e.y>H+100){e.alive=false;continue;}
// столкновение с игроком
if(player.x+player.w>e.x&&player.x<e.x+e.w&&player.y+player.h>e.y&&player.y<e.y+e.h){
if(player.vy>2&&player.y+player.h<e.y+e.h/2+12){
e.hp--;player.vy=-11;sfx.bossHit();
if(e.hp<=0){e.alive=false;sfx.kill();}
} else if(player.invuln<=0)die();
}
}
}
// босс — ЧЁРНЫЙ
if(boss&&!bossDefeated){
boss.hitFlash=Math.max(0,boss.hitFlash-1);
boss.x+=boss.vx;
if(boss.x<50){boss.x=50;boss.vx=Math.abs(boss.vx);}
if(boss.x+boss.w>W-50){boss.x=W-50-boss.w;boss.vx=-Math.abs(boss.vx);}
boss.shootTimer--;
if(boss.shootTimer<=0){
boss.shootTimer=55;
const dx=(player.x+player.w/2)-(boss.x+boss.w/2);
const dy=(player.y+player.h/2)-(boss.y+boss.h/2);
const len=Math.hypot(dx,dy)||1;
boss.projectiles.push({x:boss.x+boss.w/2,y:boss.y+boss.h,vx:dx/len*4.5,vy:dy/len*4.5});
}
for(let i=boss.projectiles.length-1;i>=0;i--){
const pr=boss.projectiles[i];pr.x+=pr.vx;pr.y+=pr.vy;
if(pr.x<-50||pr.x>W+50||pr.y>H+50||pr.y<-50){boss.projectiles.splice(i,1);continue;}
if(player.x+player.w>pr.x-8&&player.x<pr.x+8&&player.y+player.h>pr.y-8&&player.y<pr.y+8&&player.invuln<=0){boss.projectiles.splice(i,1);die();}
}
if(player.vy>2&&player.x+player.w>boss.x&&player.x<boss.x+boss.w&&player.y+player.h>boss.y&&player.y<boss.y+boss.h&&player.y+player.h<boss.y+boss.h/2+15){
boss.hp--;boss.hitFlash=20;player.vy=-12;sfx.bossHit();
if(boss.hp<=0){bossDefeated=true;levelComplete=true;sfx.win();showMessage("🏆 ЧЁРНЫЙ БОСС ПОБЕЖДЁН! Нажми R",999);}
}
if(boss.hp>0&&player.x+player.w>boss.x+5&&player.x<boss.x+boss.w-5&&player.y+player.h>boss.y+5&&player.y<boss.y+boss.h-5&&player.invuln<=0)die();
}
if(level<5&&stars.length===0&&!levelComplete){levelComplete=true;showMessage("✨ Уровень пройден! ENTER — дальше",999);}
}
// ==== DRAW ====
function drawSalik(){
if(player.invuln>0&&Math.floor(player.invuln/4)%2===0)return;
const x=player.x,y=player.y,w=player.w,h=player.h;
ctx.save();ctx.translate(x+w/2,y+h/2);ctx.scale(player.facing,1);
ctx.fillStyle='#e63946';ctx.beginPath();ctx.ellipse(0,4,w/2,h/2.2,0,0,Math.PI*2);ctx.fill();
ctx.strokeStyle='#8b0000';ctx.lineWidth=2;ctx.stroke();
ctx.fillStyle='#fff';ctx.beginPath();ctx.arc(-7,-6,6,0,Math.PI*2);ctx.arc(7,-6,6,0,Math.PI*2);ctx.fill();
ctx.fillStyle='#000';ctx.beginPath();ctx.arc(-6,-6,3,0,Math.PI*2);ctx.arc(8,-6,3,0,Math.PI*2);ctx.fill();
ctx.strokeStyle='#000';ctx.lineWidth=2;ctx.beginPath();ctx.arc(0,5,7,0.15,Math.PI-0.15);ctx.stroke();
ctx.restore();
}
function drawEnemy(e){
if(!e.alive)return;
const cx=e.x+e.w/2,cy=e.y+e.h/2;
if(e.type==='bomb'){
ctx.fillStyle='#111';ctx.beginPath();ctx.arc(cx,cy,e.w/2,0,Math.PI*2);ctx.fill();
ctx.strokeStyle='#444';ctx.lineWidth=2;ctx.stroke();
ctx.fillStyle='#ff4444';ctx.beginPath();ctx.arc(cx,cy-3,3+Math.sin(Date.now()/100)*2,0,Math.PI*2);ctx.fill();
ctx.fillStyle='#fff';ctx.beginPath();ctx.arc(cx-6,cy-4,3,0,Math.PI*2);ctx.arc(cx+6,cy-4,3,0,Math.PI*2);ctx.fill();
ctx.fillStyle='#000';ctx.beginPath();ctx.arc(cx-6,cy-4,1.5,0,Math.PI*2);ctx.arc(cx+6,cy-4,1.5,0,Math.PI*2);ctx.fill();
return;
}
// наземные голубые/золотые — тело-квадрат с глазами
ctx.fillStyle=e.color;ctx.fillRect(e.x,e.y,e.w,e.h);
ctx.strokeStyle='#000';ctx.lineWidth=2;ctx.strokeRect(e.x,e.y,e.w,e.h);
ctx.fillStyle='#fff';ctx.beginPath();ctx.arc(e.x+10,e.y+12,4,0,Math.PI*2);ctx.arc(e.x+e.w-10,e.y+12,4,0,Math.PI*2);ctx.fill();
ctx.fillStyle='#000';ctx.beginPath();ctx.arc(e.x+10,e.y+12,2,0,Math.PI*2);ctx.arc(e.x+e.w-10,e.y+12,2,0,Math.PI*2);ctx.fill();
if(e.maxHp>1){
ctx.fillStyle='#333';ctx.fillRect(e.x,e.y-8,e.w,4);
ctx.fillStyle='#ffd700';ctx.fillRect(e.x,e.y-8,e.w*(e.hp/e.maxHp),4);
}
}
function draw(){
ctx.clearRect(0,0,W,H);
const g=ctx.createLinearGradient(0,0,0,H);g.addColorStop(0,'#2a1a4a');g.addColorStop(1,'#0f0f1f');
ctx.fillStyle=g;ctx.fillRect(0,0,W,H);
for(let i=0;i<50;i++){ctx.fillStyle='rgba(255,255,255,'+(0.08+Math.sin(i+Date.now()/800)*0.12)+')';ctx.beginPath();ctx.arc((i*137)%W,(i*73)%300,1.5,0,Math.PI*2);ctx.fill();}
for(const p of platforms){
ctx.fillStyle=p.ground?'#3a2a5a':'#5a4a7a';ctx.fillRect(p.x,p.y,p.w,p.h);
ctx.strokeStyle='#e63946';ctx.lineWidth=2;ctx.strokeRect(p.x,p.y,p.w,p.h);
}
for(const s of stars){
ctx.fillStyle='#ffd700';ctx.beginPath();
for(let i=0;i<5;i++){const a=-Math.PI/2+i*Math.PI*2/5;ctx.lineTo(s.x+Math.cos(a)*12,s.y+Math.sin(a)*12);const a2=a+Math.PI/5;ctx.lineTo(s.x+Math.cos(a2)*5,s.y+Math.sin(a2)*5);}
ctx.closePath();ctx.fill();
}
for(const e of enemies)drawEnemy(e);
// ЧЁРНЫЙ БОСС
if(boss&&!bossDefeated){
ctx.save();ctx.translate(boss.x,boss.y);
if(boss.hitFlash>0){ctx.shadowColor='#ff0000';ctx.shadowBlur=25;}
ctx.fillStyle='#111';ctx.fillRect(0,0,boss.w,boss.h);
ctx.strokeStyle='#444';ctx.lineWidth=4;ctx.strokeRect(0,0,boss.w,boss.h);
ctx.fillStyle='#222';ctx.fillRect(10,10,boss.w-20,20);
ctx.fillStyle='#ff0000';ctx.beginPath();ctx.arc(45,60,14,0,Math.PI*2);ctx.arc(boss.w-45,60,14,0,Math.PI*2);ctx.fill();
ctx.fillStyle='#fff';ctx.beginPath();ctx.arc(50,55,5,0,Math.PI*2);ctx.arc(boss.w-40,55,5,0,Math.PI*2);ctx.fill();
ctx.fillStyle='#000';ctx.beginPath();ctx.arc(boss.w/2,100,22,0,Math.PI);ctx.fill();
ctx.fillStyle='#333';ctx.fillRect(15,8,boss.w-30,12);
ctx.fillStyle='#ff4444';ctx.fillRect(15,8,(boss.w-30)*(boss.hp/boss.maxHp),12);
ctx.restore();
for(const pr of boss.projectiles){ctx.fillStyle='#ff4444';ctx.beginPath();ctx.arc(pr.x,pr.y,7,0,Math.PI*2);ctx.fill();ctx.strokeStyle='#8b0000';ctx.lineWidth=2;ctx.stroke();}
}
drawSalik();
ctx.fillStyle='#fff';ctx.font='bold 18px Arial';ctx.textAlign='left';
ctx.fillText('❤️'.repeat(Math.max(0,player.lives)),20,30);
ctx.fillText('⭐ '+totalStars,20,55);
ctx.textAlign='right';ctx.fillText('Уровень '+level+'/5',W-20,30);ctx.textAlign='left';
if(messageTimer>0){
ctx.fillStyle='rgba(0,0,0,0.75)';ctx.fillRect(W/2-260,60,520,50);
ctx.fillStyle='#e63946';ctx.font='bold 22px Arial';ctx.textAlign='center';
ctx.fillText(message,W/2,95);ctx.textAlign='left';
}
if(bossDefeated){
ctx.fillStyle='rgba(0,0,0,0.92)';ctx.fillRect(0,0,W,H);
ctx.fillStyle='#e63946';ctx.font='bold 38px Arial';ctx.textAlign='center';
ctx.fillText('🏆 ЧЁРНЫЙ БОСС ПОБЕЖДЁН',W/2,H/2-50);
ctx.font='24px Arial';ctx.fillStyle='#ffd700';
ctx.fillText('Красный Салик прошёл все 5 уровней!',W/2,H/2+10);
ctx.font='18px Arial';ctx.fillStyle='#aaa';
ctx.fillText('R — играть снова',W/2,H/2+70);
ctx.textAlign='left';
}
if(gameOver){
ctx.fillStyle='rgba(0,0,0,0.85)';ctx.fillRect(0,0,W,H);
ctx.fillStyle='#e63946';ctx.font='bold 40px Arial';ctx.textAlign='center';
ctx.fillText('💀 ИГРА ОКОНЧЕНА',W/2,H/2-30);
ctx.font='20px Arial';ctx.fillStyle='#fff';
ctx.fillText('R — начать заново',W/2,H/2+30);
ctx.textAlign='left';
}
}
function loop(){update();draw();requestAnimationFrame(loop);}
loop();
})();
</script>
</body>
</html>Game Source: Красный Салик
Creator: TurboGamer88
Libraries: none
Complexity: complex (354 lines, 17.7 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-turbogamer88-mvtewz99" to link back to the original. Then publish at arcadelab.ai/publish.