Python
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Шутер</title>
<style>
  body { margin:0; background:#111; overflow:hidden; font-family:Arial; color:#fff; }
  canvas { display:block; margin:0 auto; background:#2a2a2a; cursor:crosshair; }
  #info { position:fixed; top:10px; left:10px; font-size:14px; line-height:1.5;
          background:rgba(0,0,0,.5); padding:8px 12px; border-radius:6px; }
  #gameover { position:fixed; top:50%; left:50%; transform:translate(-50%,-50%);
              font-size:48px; display:none; text-shadow:2px 2px 4px #000; }
</style>
</head>
<body>
<div id="info">
  <b>WASD</b> — движение<br>
  <b>Мышь</b> — прицел и стрельба<br>
  <b>1-4 / колесо</b> — смена оружия<br>
  <b>R</b> — перезарядка<br>
  <b>Space</b> — спавн бота
</div>
<div id="gameover">GAME OVER — нажми F5</div>
<canvas id="c" width="1000" height="700"></canvas>

<script>
const cvs = document.getElementById('c');
const ctx = cvs.getContext('2d');
const W = cvs.width, H = cvs.height;

// ======== ОРУЖИЕ ========
const WEAPONS = [
  { name:'Пистолет',  dmg:20, rpm:300, spread:0.04, speed:14, mag:12, reload:1.0, bullets:1, color:'#ffeb3b' },
  { name:'Дробовик',  dmg:15, rpm:90,  spread:0.25, speed:13, mag:6,  reload:1.8, bullets:6, color:'#ff9800' },
  { name:'Автомат',   dmg:12, rpm:600, spread:0.08, speed:16, mag:30, reload:1.5, bullets:1, color:'#4caf50' },
  { name:'Снайперка', dmg:90, rpm:60,  spread:0.005,speed:22, mag:5,  reload:2.2, bullets:1, color:'#2196f3' },
];

// ======== ВВОД ========
const keys = {};
const mouse = { x:W/2, y:H/2, down:false };
addEventListener('keydown', e => {
  keys[e.key.toLowerCase()] = true;
  if (e.key >= '1' && e.key <= '4') player.switchWeapon(+e.key - 1);
  if (e.key.toLowerCase() === 'r') player.reload();
  if (e.key === ' ') spawnBot();
});
addEventListener('keyup', e => keys[e.key.toLowerCase()] = false);
cvs.addEventListener('mousemove', e => {
  const r = cvs.getBoundingClientRect();
  mouse.x = e.clientX - r.left;
  mouse.y = e.clientY - r.top;
});
cvs.addEventListener('mousedown', () => mouse.down = true);
cvs.addEventListener('mouseup',   () => mouse.down = false);
cvs.addEventListener('wheel', e => {
  const dir = e.deltaY > 0 ? 1 : -1;
  player.switchWeapon((player.weaponIdx + dir + WEAPONS.length) % WEAPONS.length);
});

// ======== КЛАССЫ ========
class Entity {
  constructor(x,y,r,color,hp){
    this.x=x; this.y=y; this.r=r; this.color=color;
    this.hp=hp; this.maxHp=hp; this.angle=0; this.vx=0; this.vy=0;
  }
  draw(){
    ctx.save();
    ctx.translate(this.x,this.y);
    ctx.rotate(this.angle);
    ctx.fillStyle = this.color;
    ctx.beginPath(); ctx.arc(0,0,this.r,0,Math.PI*2); ctx.fill();
    // ствол
    ctx.fillStyle = '#222';
    ctx.fillRect(this.r-2, -3, this.r+6, 6);
    ctx.restore();
    // полоска HP
    const w = this.r*2;
    ctx.fillStyle = '#400';
    ctx.fillRect(this.x-w/2, this.y-this.r-10, w, 4);
    ctx.fillStyle = '#0f0';
    ctx.fillRect(this.x-w/2, this.y-this.r-10, w*(this.hp/this.maxHp), 4);
  }
  damage(d){
    this.hp -= d;
    if (this.hp <= 0) this.die();
  }
  die(){ /* переопределяется */ }
  move(){
    this.x += this.vx; this.y += this.vy;
    this.x = Math.max(this.r, Math.min(W-this.r, this.x));
    this.y = Math.max(this.r, Math.min(H-this.r, this.y));
  }
}

class Player extends Entity {
  constructor(){
    super(W/2, H/2, 16, '#4fc3f7', 100);
    this.speed = 3.2;
    this.weaponIdx = 0;
    this.weapons = WEAPONS.map(w => ({ ...w, ammo:w.mag, reserve:w.mag*3, lastShot:0, reloading:false, reloadEnd:0 }));
  }
  get weapon(){ return this.weapons[this.weaponIdx]; }
  switchWeapon(i){ this.weaponIdx = i; }
  reload(){
    const w = this.weapon;
    if (w.reloading || w.ammo === w.mag || w.reserve <= 0) return;
    w.reloading = true;
    w.reloadEnd = performance.now() + w.reload*1000;
  }
  update(){
    let dx=0, dy=0;
    if (keys['w']) dy -= 1;
    if (keys['s']) dy += 1;
    if (keys['a']) dx -= 1;
    if (keys['d']) dx += 1;
    const len = Math.hypot(dx,dy);
    if (len){ dx/=len; dy/=len; }
    this.vx = dx*this.speed; this.vy = dy*this.speed;
    this.move();
    this.angle = Math.atan2(mouse.y-this.y, mouse.x-this.x);

    const w = this.weapon;
    if (w.reloading){
      if (performance.now() >= w.reloadEnd){
        const need = w.mag - w.ammo;
        const take = Math.min(need, w.reserve);
        w.ammo += take; w.reserve -= take;
        w.reloading = false;
      }
    }
    if (mouse.down && !w.reloading){
      const interval = 60000 / w.rpm;
      if (performance.now() - w.lastShot >= interval && w.ammo > 0){
        this.shoot();
        w.lastShot = performance.now();
      } else if (w.ammo === 0) this.reload();
    }
  }
  shoot(){
    const w = this.weapon;
    w.ammo--;
    for (let i=0;i<w.bullets;i++){
      const a = this.angle + (Math.random()-0.5)*w.spread*2;
      bullets.push(new Bullet(this.x+Math.cos(this.angle)*this.r,
                              this.y+Math.sin(this.angle)*this.r,
                              a, w.speed, w.dmg, w.color, this));
    }
    // отдача
    this.x -= Math.cos(this.angle)*1.5;
    this.y -= Math.sin(this.angle)*1.5;
  }
  die(){ gameOver(); }
}

class Bot extends Entity {
  constructor(x,y){
    super(x,y,15,'#ef5350', 40 + Math.random()*30);
    this.speed = 1.2 + Math.random()*0.8;
    this.lastShot = 0;
    this.shootInterval = 800 + Math.random()*1200;
    this.accuracy = 0.08 + Math.random()*0.12;
    this.state = 'chase';
  }
  update(){
    const dx = player.x - this.x;
    const dy = player.y - this.y;
    const dist = Math.hypot(dx,dy);
    this.angle = Math.atan2(dy,dx);

    // ИИ: приближаемся, но держим дистанцию
    let targetDist = 220;
    let moveX = 0, moveY = 0;
    if (dist > targetDist + 30){
      moveX = dx/dist; moveY = dy/dist;
    } else if (dist < targetDist - 30){
      moveX = -dx/dist; moveY = -dy/dist;
    } else {
      // стрейф
      moveX = -dy/dist; moveY = dx/dist;
      if (Math.random() < 0.01) this.strafeDir = -this.strafeDir || 1;
      moveX *= (this.strafeDir || 1); moveY *= (this.strafeDir || 1);
    }
    this.vx = moveX * this.speed;
    this.vy = moveY * this.speed;
    this.move();

    // стрельба
    if (dist < 500 && performance.now() - this.lastShot > this.shootInterval){
      this.lastShot = performance.now();
      const a = this.angle + (Math.random()-0.5)*this.accuracy*2;
      bullets.push(new Bullet(this.x+Math.cos(this.angle)*this.r,
                              this.y+Math.sin(this.angle)*this.r,
                              a, 9, 8, '#ff5252', this));
    }
  }
  die(){
    score += 10;
    const idx = bots.indexOf(this);
    if (idx >= 0) bots.splice(idx,1);
    // эффект
    for (let i=0;i<10;i++) particles.push(new Particle(this.x,this.y,'#ef5350'));
  }
}

class Bullet {
  constructor(x,y,angle,speed,dmg,color,owner){
    this.x=x; this.y=y;
    this.vx = Math.cos(angle)*speed;
    this.vy = Math.sin(angle)*speed;
    this.dmg = dmg; this.color = color; this.owner = owner;
    this.life = 80;
  }
  update(){
    this.x += this.vx; this.y += this.vy;
    this.life--;
    if (this.x<0||this.x>W||this.y<0||this.y>H) this.life = 0;
  }
  draw(){
    ctx.strokeStyle = this.color;
    ctx.lineWidth = 3;
    ctx.beginPath();
    ctx.moveTo(this.x, this.y);
    ctx.lineTo(this.x - this.vx, this.y - this.vy);
    ctx.stroke();
  }
}

class Particle {
  constructor(x,y,color){
    this.x=x; this.y=y; this.color=color;
    this.vx = (Math.random()-0.5)*4;
    this.vy = (Math.random()-0.5)*4;
    this.life = 30;
  }
  update(){ this.x+=this.vx; this.y+=this.vy; this.life--; }
  draw(){
    ctx.fillStyle = this.color;
    ctx.globalAlpha = this.life/30;
    ctx.fillRect(this.x-2, this.y-2, 4, 4);
    ctx.globalAlpha = 1;
  }
}

// ======== ИГРА ========
const player = new Player();
const bots = [];
const bullets = [];
const particles = [];
let score = 0;
let gameOverFlag = false;

function spawnBot(){
  // спавн по краю
  const side = Math.floor(Math.random()*4);
  let x,y;
  if (side===0){ x=Math.random()*W; y=20; }
  else if (side===1){ x=Math.random()*W; y=H-20; }
  else if (side===2){ x=20; y=Math.random()*H; }
  else { x=W-20; y=Math.random()*H; }
  bots.push(new Bot(x,y));
}
for (let i=0;i<4;i++) spawnBot();

function gameOver(){
  gameOverFlag = true;
  document.getElementById('gameover').style.display = 'block';
}

// ======== ЦИКЛ ========
function loop(){
  if (!gameOverFlag){
    // фон — сетка
    ctx.fillStyle = '#2a2a2a';
    ctx.fillRect(0,0,W,H);
    ctx.strokeStyle = '#333';
    ctx.lineWidth = 1;
    for (let x=0;x<W;x+=40){
      ctx.beginPath(); ctx.moveTo(x,0); ctx.lineTo(x,H); ctx.stroke();
    }
    for (let y=0;y<H;y+=40){
      ctx.beginPath(); ctx.moveTo(0,y); ctx.lineTo(W,y); ctx.stroke();
    }

    player.update();
    bots.forEach(b => b.update());
    bullets.forEach(b => b.update());
    particles.forEach(p => p.update());

    // коллизии пуль
    for (let i=bullets.length-1; i>=0; i--){
      const b = bullets[i];
      if (b.life<=0){ bullets.splice(i,1); continue; }
      if (b.owner === player){
        for (const bot of bots){
          if (Math.hypot(b.x-bot.x, b.y-bot.y) < bot.r){
            bot.damage(b.dmg);
            b.life = 0;
            for (let k=0;k<4;k++) particles.push(new Particle(b.x,b.y,'#ff5252'));
            break;
          }
        }
      } else {
        if (Math.hypot(b.x-player.x, b.y-player.y) < player.r){
          player.damage(b.dmg);
          b.life = 0;
          for (let k=0;k<4;k++) particles.push(new Particle(b.x,b.y,'#4fc3f7'));
        }
      }
    }

    // спавн ботов
    if (bots.length < 3 + Math.floor(score/50)) spawnBot();

    // отрисовка
    particles.forEach(p => p.draw());
    bots.forEach(b => b.draw());
    player.draw();
    bullets.forEach(b => b.draw());

    // прицел
    ctx.strokeStyle = '#fff';
    ctx.lineWidth = 1.5;
    ctx.beginPath();
    ctx.arc(mouse.x, mouse.y, 10, 0, Math.PI*2);
    ctx.moveTo(mouse.x-15, mouse.y); ctx.lineTo(mouse.x-5, mouse.y);
    ctx.moveTo(mouse.x+5, mouse.y); ctx.lineTo(mouse.x+15, mouse.y);
    ctx.moveTo(mouse.x, mouse.y-15); ctx.lineTo(mouse.x, mouse.y-5);
    ctx.moveTo(mouse.x, mouse.y+5); ctx.lineTo(mouse.x, mouse.y+15);
    ctx.stroke();

    // HUD
    ctx.fillStyle = 'rgba(0,0,0,.6)';
    ctx.fillRect(10, H-80, 280, 70);
    ctx.fillStyle = '#fff';
    ctx.font = 'bold 18px Arial';
    const w = player.weapon;
    ctx.fillText(`${w.name}`, 20, H-55);
    ctx.font = '16px Arial';
    ctx.fillText(`Патроны: ${w.ammo} / ${w.reserve}${w.reloading?' [перезарядка]':''}`, 20, H-32);
    ctx.fillText(`Счёт: ${score}   Ботов: ${bots.length}`, 20, H-14);

    // HP бар сверху
    ctx.fillStyle = '#400';
    ctx.fillRect(W-220, 15, 200, 20);
    ctx.fillStyle = '#0f0';
    ctx.fillRect(W-220, 15, 200*(player.hp/player.maxHp), 20);
    ctx.strokeStyle = '#fff';
    ctx.strokeRect(W-220, 15, 200, 20);
    ctx.fillStyle = '#fff';
    ctx.font = 'bold 14px Arial';
    ctx.fillText(`HP: ${Math.max(0,Math.ceil(player.hp))}`, W-215, 30);
  }
  requestAnimationFrame(loop);
}
loop();
</script>
</body>
</html>

шутер

Sign in to react