🎮ArcadeLab

汉阳造步枪 - 瘦身精准版(无刺刀) + 开火动画

by TurboKoala83
461 lines12.2 KB
▶ Play
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>汉阳造步枪 - 瘦身精准版(无刺刀) + 开火动画</title>
<style>
  body { margin: 0; background: #1a1a2e; display: flex; justify-content: center; align-items: center; height: 100vh; overflow: hidden; font-family: 'Microsoft YaHei', sans-serif; }
  canvas { background: linear-gradient(180deg, #16213e 0%, #0f3460 100%); border-radius: 12px; box-shadow: 0 0 40px rgba(196, 30, 58, 0.3); cursor: crosshair; }
  .info { position: absolute; top: 20px; left: 50%; transform: translateX(-50%); color: #c41e3a; font-size: 14px; letter-spacing: 4px; text-shadow: 0 0 10px rgba(196, 30, 58, 0.8); pointer-events: none; }
  .controls { position: absolute; bottom: 20px; color: #888; font-size: 12px; letter-spacing: 2px; text-align: center; width: 100%; pointer-events: none; }
</style>
</head>
<body>
<div class="info">★ 汉阳造步枪 · 瘦身精准版(无刺刀) ★</div>
<div class="controls">点击画面 / 按空格键 开火 | 左键拖拽平移 | 右键拖拽旋转 | 滚轮缩放 | 双击重置视角</div>
<canvas id="game" width="900" height="600"></canvas>

<script>
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');

// ========== 摄像机系统 ==========
const camera = {
  x: canvas.width / 2,
  y: canvas.height / 2,
  zoom: 1.0,
  rotation: 0,
  isDragging: false,
  dragButton: 0,
  lastX: 0,
  lastY: 0,
  shakeX: 0, // 开火镜头震动
  shakeY: 0
};

// ========== 开火动画状态 ==========
let lastTime = performance.now();
let fireState = {
  isFiring: false,
  fireTime: 0,
  recoilX: 0,      // 后坐力水平偏移
  recoilY: 0,      // 后坐力垂直偏移
  recoilRot: 0,    // 后坐力旋转
  flashTimer: 0,   // 火光持续时间
  shells: [],      // 抛壳列表
  smokeParticles: [] // 烟雾粒子列表
};

// =====================================================================
// 【原汁原味】汉阳造步枪 - 瘦身精准版(无刺刀)
// 原点 (0,0) 在【扳机位置】,向右为枪口方向
// =====================================================================
function drawHanyangRifle(ctx) {
  
  // 1. 枪托后部(木质)
  ctx.fillStyle = '#5E3A20';
  ctx.beginPath();
  ctx.moveTo(-30, -6);
  ctx.lineTo(-160, -6);
  ctx.lineTo(-165, 20);
  ctx.lineTo(-130, 25);
  ctx.lineTo(-30, 15);
  ctx.closePath();
  ctx.fill();

  ctx.fillStyle = '#3A2210';
  ctx.fillRect(-160, 18, 130, 7);
  ctx.fillStyle = '#4A2E15';
  ctx.fillRect(-160, -6, 130, 3);

  // 枪托托底板
  ctx.fillStyle = '#2A2A2A';
  ctx.beginPath();
  ctx.moveTo(-160, -6);
  ctx.lineTo(-165, 18);
  ctx.lineTo(-160, 20);
  ctx.lineTo(-155, 2);
  ctx.closePath();
  ctx.fill();

  // 握把凹陷
  ctx.fillStyle = 'rgba(0,0,0,0.15)';
  ctx.beginPath();
  ctx.arc(-70, 18, 12, 0, Math.PI, true);
  ctx.fill();

  // 2. 下护木
  ctx.fillStyle = '#5E3A20';
  ctx.fillRect(-30, -6, 215, 16);

  ctx.fillStyle = '#8A5A3A';
  ctx.fillRect(-30, -6, 215, 3);
  ctx.fillStyle = '#3A2210';
  ctx.fillRect(-30, 7, 215, 3);

  // 3. 上护木
  ctx.fillStyle = '#4A2E15';
  ctx.fillRect(-30, -18, 195, 12);

  ctx.fillStyle = '#1A1A1A';
  ctx.fillRect(-30, -6, 215, 2);

  // 4. 金属枪箍
  const clampPositions = [60, 150];
  clampPositions.forEach(x => {
    ctx.fillStyle = '#333333';
    ctx.fillRect(x, -20, 8, 32);
    ctx.fillStyle = '#666666';
    ctx.fillRect(x, -20, 2, 32);
  });

  // 5. 外露枪管
  const barrelStart = 185;
  const barrelLength = 30;

  ctx.fillStyle = '#5A5A5A';
  ctx.fillRect(barrelStart, -5, barrelLength, 10);

  ctx.fillStyle = '#9A9A9A';
  ctx.fillRect(barrelStart, -5, barrelLength, 2);

  // 6. 枪口帽与准星
  const muzzleX = barrelStart + barrelLength;

  ctx.fillStyle = '#2A2A2A';
  ctx.fillRect(muzzleX - 5, -7, 5, 14);

  ctx.fillStyle = '#555555';
  ctx.fillRect(muzzleX - 15, -12, 6, 5);

  ctx.fillStyle = '#888888';
  ctx.beginPath();
  ctx.moveTo(muzzleX - 13, -12);
  ctx.lineTo(muzzleX - 10, -12);
  ctx.lineTo(muzzleX - 11.5, -20);
  ctx.closePath();
  ctx.fill();

  // 7. 机匣、枪栓与扳机
  ctx.fillStyle = '#2A2A2A';
  ctx.fillRect(-30, -12, 35, 24);

  ctx.fillStyle = '#666666';
  ctx.fillRect(-30, -12, 35, 2);

  // 枪栓拉柄
  ctx.fillStyle = '#4A4A4A';
  ctx.fillRect(-10, -24, 6, 12);

  ctx.fillStyle = '#888888';
  ctx.beginPath();
  ctx.arc(-7, -24, 6, 0, Math.PI * 2);
  ctx.fill();

  // 扳机护圈
  ctx.strokeStyle = '#555555';
  ctx.lineWidth = 3;
  ctx.beginPath();
  ctx.arc(0, 12, 10, 0, Math.PI);
  ctx.stroke();

  ctx.fillStyle = '#333333';
  ctx.fillRect(-2, 8, 4, 12);

  // 8. 表尺照门
  ctx.fillStyle = '#333333';
  ctx.fillRect(20, -18, 12, 6);

  ctx.fillStyle = '#666666';
  ctx.fillRect(24, -32, 4, 14);

  ctx.fillStyle = '#1A1A1A';
  ctx.fillRect(25, -32, 2, 4);

  // 9. 枪背带
  ctx.fillStyle = '#222222';
  ctx.beginPath();
  ctx.arc(64, 10, 4, 0, Math.PI * 2);
  ctx.fill();

  ctx.beginPath();
  ctx.arc(-145, 25, 4, 0, Math.PI * 2);
  ctx.fill();

  ctx.strokeStyle = '#5A4A3A';
  ctx.lineWidth = 5;
  ctx.beginPath();
  ctx.moveTo(-145, 25);
  ctx.quadraticCurveTo(-50, 65, 20, 40);
  ctx.lineTo(64, 10);
  ctx.stroke();
}

// =====================================================================
// 开火逻辑与特效
// =====================================================================
function triggerFire() {
  if (fireState.isFiring) return;
  fireState.isFiring = true;
  fireState.fireTime = performance.now();
  fireState.flashTimer = 0.06;

  // 镜头震动
  camera.shakeX = (Math.random() - 0.5) * 12;
  camera.shakeY = (Math.random() - 0.5) * 12;

  // 弹壳抛出:从枪栓位置 (-7, -24) 抛出
  fireState.shells.push({
    x: -7, y: -24,
    vx: 2 + Math.random() * 3,
    vy: -6 - Math.random() * 4,
    rotation: Math.random() * Math.PI,
    vRot: (Math.random() - 0.5) * 0.4,
    bounce: 0,
    life: 1.0
  });

  // 烟雾生成:从枪口中心 (200, -5) 喷出
  for (let i = 0; i < 5; i++) {
    fireState.smokeParticles.push({
      x: 200 + (Math.random() - 0.5) * 10,
      y: -5 + (Math.random() - 0.5) * 10,
      vx: 2 + Math.random() * 4,
      vy: -1 - Math.random() * 3,
      size: 5 + Math.random() * 10,
      alpha: 0.8,
      life: 1.0,
      decay: 0.02 + Math.random() * 0.03
    });
  }
}

function updateFire(dt) {
  if (!fireState.isFiring) return;

  const now = performance.now();
  const elapsed = now - fireState.fireTime;

  // 后坐力:0-150ms 猛地向后,150-350ms 平滑恢复
  if (elapsed < 150) {
    const t = elapsed / 150;
    fireState.recoilX = -15 * Math.sin(t * Math.PI);
    fireState.recoilY = 8 * Math.sin(t * Math.PI);
    fireState.recoilRot = 0.08 * Math.sin(t * Math.PI);
  } else {
    const t = Math.min((elapsed - 150) / 200, 1);
    const easeOut = 1 - Math.pow(1 - t, 3);
    fireState.recoilX = -15 * (1 - easeOut);
    fireState.recoilY = 8 * (1 - easeOut);
    fireState.recoilRot = 0.08 * (1 - easeOut);
  }

  if (fireState.flashTimer > 0) {
    fireState.flashTimer -= dt;
  }

  // 物理更新:弹壳
  for (let i = fireState.shells.length - 1; i >= 0; i--) {
    const s = fireState.shells[i];
    s.vy += 0.4;
    s.x += s.vx;
    s.y += s.vy;
    s.rotation += s.vRot;

    if (s.y > 40 && s.vy > 0) {
      s.y = 40;
      s.vy *= -0.4;
      s.vx *= 0.7;
      s.vRot *= -0.6;
      s.bounce++;
    }
    if (s.bounce > 2 || s.y > 100) {
      fireState.shells.splice(i, 1);
    }
  }

  // 物理更新:烟雾
  for (let i = fireState.smokeParticles.length - 1; i >= 0; i--) {
    const p = fireState.smokeParticles[i];
    p.x += p.vx;
    p.y += p.vy;
    p.vy -= 0.1;
    p.vx *= 0.96;
    p.size += 0.5;
    p.alpha -= p.decay;
    p.life -= p.decay;
    if (p.alpha <= 0 || p.life <= 0) {
      fireState.smokeParticles.splice(i, 1);
    }
  }

  if (elapsed > 500 && fireState.flashTimer <= 0 && fireState.shells.length === 0 && fireState.smokeParticles.length === 0) {
    fireState.isFiring = false;
    fireState.recoilX = 0; fireState.recoilY = 0; fireState.recoilRot = 0;
  }
}

function drawFireEffects(ctx) {
  // 1. 枪口火光
  if (fireState.flashTimer > 0) {
    const flashAlpha = fireState.flashTimer / 0.06;
    ctx.save();
    ctx.globalAlpha = flashAlpha;
    ctx.translate(200, -5); // 对准枪管外露段中心

    ctx.fillStyle = '#FFFF00';
    ctx.beginPath();
    ctx.moveTo(0, 0);
    ctx.lineTo(30, -15);
    ctx.lineTo(15, 0);
    ctx.lineTo(35, 15);
    ctx.lineTo(0, 5);
    ctx.closePath();
    ctx.fill();

    ctx.fillStyle = '#FFAA00';
    ctx.beginPath();
    ctx.arc(5, 0, 15, 0, Math.PI * 2);
    ctx.fill();

    ctx.fillStyle = '#FFFFFF';
    ctx.beginPath();
    ctx.arc(5, 0, 8, 0, Math.PI * 2);
    ctx.fill();
    ctx.restore();
  }

  // 2. 烟雾
  fireState.smokeParticles.forEach(p => {
    ctx.save();
    ctx.globalAlpha = p.alpha;
    ctx.fillStyle = '#AAAAAA';
    ctx.fillRect(p.x - p.size/2, p.y - p.size/2, p.size, p.size);
    ctx.restore();
  });

  // 3. 弹壳
  fireState.shells.forEach(s => {
    ctx.save();
    ctx.translate(s.x, s.y);
    ctx.rotate(s.rotation);
    ctx.fillStyle = '#D4AF37';
    ctx.fillRect(-4, -2, 8, 4);
    ctx.fillStyle = '#B8963A';
    ctx.fillRect(-4, -2, 2, 4);
    ctx.restore();
  });
}

// =====================================================================
// 主循环 + 摄像机变换
// =====================================================================
function animate(now) {
  const dt = (now - lastTime) / 1000;
  lastTime = now;

  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // 背景网格
  ctx.strokeStyle = 'rgba(255, 255, 255, 0.05)';
  ctx.lineWidth = 1;
  for (let i = 0; i < canvas.width; i += 60) {
    ctx.beginPath(); ctx.moveTo(i, 0); ctx.lineTo(i, canvas.height); ctx.stroke();
  }
  for (let i = 0; i < canvas.height; i += 60) {
    ctx.beginPath(); ctx.moveTo(0, i); ctx.lineTo(canvas.width, i); ctx.stroke();
  }

  updateFire(dt);

  // ===== 应用摄像机变换 =====
  ctx.save();
  // 加入镜头震动偏移
  ctx.translate(camera.x + camera.shakeX, camera.y + camera.shakeY);
  ctx.rotate(camera.rotation);
  ctx.scale(camera.zoom, camera.zoom);
  ctx.translate(-camera.x, -camera.y);

  // 绘制枪械(带后坐力)
  ctx.save();
  ctx.translate(canvas.width / 2 - 70, canvas.height / 2); 
  
  // 后坐力平移和旋转
  ctx.translate(fireState.recoilX, -fireState.recoilY);
  ctx.rotate(fireState.recoilRot);
  
  drawHanyangRifle(ctx);
  ctx.restore();

  // 绘制开火特效(特效坐标跟随枪械原点)
  ctx.save();
  ctx.translate(canvas.width / 2 - 70, canvas.height / 2);
  drawFireEffects(ctx);
  ctx.restore();

  ctx.restore();

  // UI 信息
  ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';
  ctx.font = '12px sans-serif';
  ctx.fillText(`点击或空格开火 | 缩放: ${camera.zoom.toFixed(2)}x | 旋转: ${(camera.rotation * 180 / Math.PI).toFixed(0)}°`, 20, 30);

  requestAnimationFrame(animate);
}

// =====================================================================
// 交互事件
// =====================================================================
canvas.addEventListener('contextmenu', e => e.preventDefault());

canvas.addEventListener('mousedown', (e) => {
  if (e.button === 0) { // 左键开火
    triggerFire();
  }
  camera.isDragging = true;
  camera.dragButton = e.button;
  camera.lastX = e.clientX;
  camera.lastY = e.clientY;
});

canvas.addEventListener('mousemove', (e) => {
  if (!camera.isDragging) return;
  const dx = e.clientX - camera.lastX;
  const dy = e.clientY - camera.lastY;

  if (camera.dragButton === 0) {
    camera.x += dx; camera.y += dy;
  } else if (camera.dragButton === 2) {
    camera.rotation += dx * 0.005;
  }
  camera.lastX = e.clientX;
  camera.lastY = e.clientY;
});

canvas.addEventListener('mouseup', () => { camera.isDragging = false; });
canvas.addEventListener('mouseleave', () => { camera.isDragging = false; });

canvas.addEventListener('wheel', (e) => {
  e.preventDefault();
  camera.zoom *= e.deltaY > 0 ? 0.9 : 1.1;
  camera.zoom = Math.max(0.2, Math.min(5.0, camera.zoom));
}, { passive: false });

canvas.addEventListener('dblclick', () => {
  camera.x = canvas.width / 2;
  camera.y = canvas.height / 2;
  camera.zoom = 1.0;
  camera.rotation = 0;
});

window.addEventListener('keydown', (e) => {
  if (e.code === 'Space') {
    e.preventDefault();
    triggerFire();
  }
});

requestAnimationFrame(animate);
</script>
</body>
</html>

Game Source: 汉阳造步枪 - 瘦身精准版(无刺刀) + 开火动画

Creator: TurboKoala83

Libraries: none

Complexity: complex (461 lines, 12.2 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-turbokoala83-mup549lz" to link back to the original. Then publish at arcadelab.ai/publish.