汉阳造步枪 - 双手持枪校正版
by TurboKoala83275 lines20.2 KB
<!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');
// ========== 声音引擎 ==========
let audioCtx = null;
function initAudio() {
if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
if (audioCtx.state === 'suspended') audioCtx.resume();
}
function playGunshot() {
if (!audioCtx) return; const now = audioCtx.currentTime;
const bufferSize = audioCtx.sampleRate * 0.2;
const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) data[i] = Math.random() * 2 - 1;
const noise = audioCtx.createBufferSource(); noise.buffer = buffer;
const filter = audioCtx.createBiquadFilter(); filter.type = 'lowpass'; filter.frequency.value = 800;
const gain = audioCtx.createGain(); gain.gain.setValueAtTime(1, now); gain.gain.exponentialRampToValueAtTime(0.01, now + 0.15);
noise.connect(filter); filter.connect(gain); gain.connect(audioCtx.destination); noise.start(now);
const osc = audioCtx.createOscillator(); osc.type = 'sine'; osc.frequency.setValueAtTime(150, now); osc.frequency.exponentialRampToValueAtTime(50, now + 0.1);
const oscGain = audioCtx.createGain(); oscGain.gain.setValueAtTime(1, now); oscGain.gain.exponentialRampToValueAtTime(0.01, now + 0.15);
osc.connect(oscGain); oscGain.connect(audioCtx.destination); osc.start(now); osc.stop(now + 0.2);
}
function playBoltAction() {
if (!audioCtx) return; const now = audioCtx.currentTime;
const bufferSize = audioCtx.sampleRate * 0.05;
const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) data[i] = Math.random() * 2 - 1;
const noise = audioCtx.createBufferSource(); noise.buffer = buffer;
const filter = audioCtx.createBiquadFilter(); filter.type = 'bandpass'; filter.frequency.value = 3000; filter.Q.value = 2;
const gain = audioCtx.createGain(); gain.gain.setValueAtTime(0.6, now); gain.gain.exponentialRampToValueAtTime(0.01, now + 0.04);
noise.connect(filter); filter.connect(gain); gain.connect(audioCtx.destination); noise.start(now);
}
// ========== 摄像机 ==========
const camera = { x: canvas.width / 2, y: canvas.height / 2, zoom: 1.2, rotation: 0, isDragging: false, dragButton: 0, lastX: 0, lastY: 0, shakeX: 0, shakeY: 0 };
// ========== 人物尺寸 ==========
const S = { headW: 52, headH: 52, torsoW: 84, torsoH: 76, armW: 36, armH: 76, legW: 44, legH: 84 };
// ========== 开火状态 ==========
let lastTime = performance.now();
let fireState = { isFiring: false, fireTime: 0, recoilX: 0, recoilY: 0, recoilRot: 0, flashTimer: 0, shells: [], smokeParticles: [], boltOffset: 0, shellEjected: false };
// =====================================================================
// 绘制人物基础部件 (完全保持原样)
// =====================================================================
function roundRect(ctx, x, y, w, h, r) { ctx.beginPath(); ctx.moveTo(x + r, y); ctx.lineTo(x + w - r, y); ctx.quadraticCurveTo(x + w, y, x + w, y + r); ctx.lineTo(x + w, y + h - r); ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h); ctx.lineTo(x + r, y + h); ctx.quadraticCurveTo(x, y + h, x, y + h - r); ctx.lineTo(x, y + r); ctx.quadraticCurveTo(x, y, x + r, y); ctx.closePath(); }
function drawHead(ctx, x, y) {
ctx.save(); ctx.translate(x, y);
const { headW, headH } = S; const headX = -headW / 2; const headY = -headH;
ctx.fillStyle = '#E8B88A'; roundRect(ctx, headX, headY, headW, headH, 6); ctx.fill();
ctx.fillStyle = '#C99A6E'; ctx.fillRect(headX, headY, 6, headH);
ctx.fillStyle = '#2A1A0A'; ctx.fillRect(headX + 12, headY + 22, 7, 5); ctx.fillRect(headX + headW - 19, headY + 22, 7, 5);
ctx.strokeStyle = '#2A1A0A'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(headX + 10, headY + 18); ctx.lineTo(headX + 20, headY + 20); ctx.stroke(); ctx.beginPath(); ctx.moveTo(headX + headW - 10, headY + 18); ctx.lineTo(headX + headW - 20, headY + 20); ctx.stroke();
ctx.strokeStyle = '#8A5A3A'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(headX + 20, headY + 40); ctx.lineTo(headX + headW - 20, headY + 40); ctx.stroke();
const hatW = headW + 14; const hatH = 22; const hatX = -hatW / 2; const hatY = headY - hatH + 4;
ctx.fillStyle = '#4A5A6B'; ctx.beginPath(); ctx.moveTo(hatX + 10, hatY); ctx.lineTo(hatX + hatW - 10, hatY); ctx.lineTo(hatX + hatW, hatY + 8); ctx.lineTo(hatX + hatW - 4, hatY + hatH); ctx.lineTo(hatX + 4, hatY + hatH); ctx.lineTo(hatX, hatY + 8); ctx.closePath(); ctx.fill();
ctx.fillStyle = '#37444F'; ctx.fillRect(hatX + 2, hatY + hatH - 4, hatW - 4, 4); ctx.beginPath(); ctx.moveTo(hatX, hatY + 8); ctx.lineTo(hatX + 8, hatY + 2); ctx.lineTo(hatX + 8, hatY + hatH); ctx.lineTo(hatX + 2, hatY + hatH); ctx.closePath(); ctx.fill();
const starCY = hatY + 9, starR = 7; ctx.fillStyle = '#C41E3A'; ctx.beginPath(); for (let i = 0; i < 5; i++) { const angle = (i * 72 - 90) * Math.PI / 180; const x1 = starR * Math.cos(angle), y1 = starCY + starR * Math.sin(angle); const angle2 = ((i + 1) * 72 - 90) * Math.PI / 180; const innerR = starR * 0.4; const x2 = innerR * Math.cos(angle2 - Math.PI / 5), y2 = starCY + innerR * Math.sin(angle2 - Math.PI / 5); if (i === 0) ctx.moveTo(x1, y1); else ctx.lineTo(x1, y1); ctx.lineTo(x2, y2); } ctx.closePath(); ctx.fill(); ctx.strokeStyle = '#8A0A1A'; ctx.lineWidth = 1; ctx.stroke();
ctx.restore();
}
function drawTorso(ctx, x, y) {
ctx.save(); ctx.translate(x, y);
const { torsoW, torsoH } = S; const torsoX = -torsoW / 2; const torsoY = -torsoH;
ctx.fillStyle = '#5A6B7C'; roundRect(ctx, torsoX, torsoY, torsoW, torsoH, 4); ctx.fill();
ctx.fillStyle = '#3D4A57'; ctx.fillRect(torsoX, torsoY, 8, torsoH); ctx.fillStyle = '#6E8090'; ctx.fillRect(torsoX + torsoW - 6, torsoY, 6, torsoH);
ctx.fillStyle = '#C41E3A'; ctx.fillRect(torsoX + 12, torsoY + 2, 14, 8); ctx.fillRect(torsoX + torsoW - 26, torsoY + 2, 14, 8);
ctx.strokeStyle = '#3D4A57'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(0, torsoY + 12); ctx.lineTo(0, torsoY + torsoH - 4); ctx.stroke();
const beltY = torsoY + torsoH * 0.55; ctx.fillStyle = '#3A2A1A'; ctx.fillRect(torsoX, beltY, torsoW, 7); ctx.fillStyle = '#B8963A'; ctx.fillRect(-5, beltY + 1, 10, 5);
ctx.restore();
}
function drawArm(ctx, x, y, rotation, side) {
ctx.save(); ctx.translate(x, y); ctx.rotate(rotation);
const { armW, armH } = S; const armX = -armW / 2;
ctx.fillStyle = '#5A6B7C'; roundRect(ctx, armX, 0, armW, armH - 12, 3); ctx.fill();
if (side === 'left') { ctx.fillStyle = '#3D4A57'; ctx.fillRect(armX, 0, 5, armH - 12); } else { ctx.fillStyle = '#6E8090'; ctx.fillRect(armX + armW - 5, 0, 5, armH - 12); }
ctx.fillStyle = '#E8B88A'; roundRect(ctx, armX + 4, armH - 14, armW - 8, 14, 3); ctx.fill();
ctx.restore();
}
function drawLeg(ctx, x, y, rotation) {
ctx.save(); ctx.translate(x, y); ctx.rotate(rotation);
const { legW, legH } = S; const legX = -legW / 2;
ctx.fillStyle = '#5A6B7C'; roundRect(ctx, legX, 0, legW, legH * 0.55, 3); ctx.fill();
ctx.fillStyle = '#3D4A57'; ctx.fillRect(legX, 0, 6, legH * 0.55);
const bindY = legH * 0.5; const bindH = legH * 0.42;
ctx.fillStyle = '#8A8A8A'; roundRect(ctx, legX - 1, bindY, legW + 2, bindH, 2); ctx.fill();
ctx.strokeStyle = '#6A6A6A'; ctx.lineWidth = 1.2;
for (let i = 0; i < 5; i++) { const yy = bindY + 4 + i * (bindH - 8) / 4; ctx.beginPath(); ctx.moveTo(legX - 1, yy); ctx.lineTo(legX + legW + 1, yy); ctx.stroke(); }
const shoeY = bindY + bindH; const shoeH = legH * 0.08;
ctx.fillStyle = '#9A7A4A'; roundRect(ctx, legX - 3, shoeY, legW + 6, shoeH, 3); ctx.fill();
ctx.strokeStyle = '#7A5A2A'; ctx.lineWidth = 1;
for (let i = 0; i < 3; i++) { ctx.beginPath(); ctx.moveTo(legX - 3, shoeY + 2 + i * 2); ctx.lineTo(legX + legW + 3, shoeY + 2 + i * 2); ctx.stroke(); }
ctx.restore();
}
// =====================================================================
// 绘制完整人物并返回锚点
// =====================================================================
function drawSoldier(ctx, centerX, groundY, scale, anim) {
const shoulderY = -S.torsoH; const headY = shoulderY;
const legSpacing = S.torsoW / 4 - 6; const armSpacing = S.torsoW / 2 + S.armW / 2 - 2;
ctx.save(); ctx.translate(centerX, groundY + anim.bob); ctx.scale(scale, scale); ctx.rotate(anim.lean);
drawLeg(ctx, -legSpacing, 0, anim.leftLegRot); drawLeg(ctx, legSpacing, 0, anim.rightLegRot);
drawTorso(ctx, 0, 0);
// 画右臂(画面右侧,托枪手)
drawArm(ctx, armSpacing, shoulderY, anim.rightArmRot, 'right');
drawHead(ctx, 0, headY);
// 画左臂(画面左侧,持枪手)
drawArm(ctx, -armSpacing, shoulderY, anim.leftArmRot, 'left');
ctx.restore();
const bob = anim.bob;
function handWorld(armPivotLX, armPivotLY, armRot) {
const handLY = S.armH - 6; const c = Math.cos(armRot); const s = Math.sin(armRot);
const hx = -handLY * s; const hy = handLY * c;
return { x: centerX + (armPivotLX + hx) * scale, y: groundY + bob + (armPivotLY + hy) * scale };
}
return {
leftHand: handWorld(-armSpacing, shoulderY, anim.leftArmRot), // 画面左侧(角色右手,拿扳机)
rightHand: handWorld(armSpacing, shoulderY, anim.rightArmRot) // 画面右侧(角色左手,托护木)
};
}
// =====================================================================
// 枪械模型(完全保留)
// =====================================================================
function drawHanyangRifle(ctx) {
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();
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);
ctx.fillStyle = '#4A2E15'; ctx.fillRect(-30, -18, 195, 12); ctx.fillStyle = '#1A1A1A'; ctx.fillRect(-30, -6, 215, 2);
[60, 150].forEach(x => { ctx.fillStyle = '#333333'; ctx.fillRect(x, -20, 8, 32); ctx.fillStyle = '#666666'; ctx.fillRect(x, -20, 2, 32); });
ctx.fillStyle = '#5A5A5A'; ctx.fillRect(185, -5, 30, 10); ctx.fillStyle = '#9A9A9A'; ctx.fillRect(185, -5, 30, 2);
ctx.fillStyle = '#2A2A2A'; ctx.fillRect(210, -7, 5, 14);
ctx.fillStyle = '#555555'; ctx.fillRect(200, -12, 6, 5); ctx.fillStyle = '#888888'; ctx.beginPath(); ctx.moveTo(202, -12); ctx.lineTo(205, -12); ctx.lineTo(203.5, -20); ctx.closePath(); ctx.fill();
ctx.fillStyle = '#2A2A2A'; ctx.fillRect(-30, -12, 35, 24); ctx.fillStyle = '#666666'; ctx.fillRect(-30, -12, 35, 2);
ctx.save(); ctx.translate(fireState.boltOffset, 0);
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.restore();
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);
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);
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; fireState.boltOffset = 0; fireState.shellEjected = false;
playGunshot();
camera.shakeX = (Math.random() - 0.5) * 12; camera.shakeY = (Math.random() - 0.5) * 12;
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;
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;
if (elapsed < 80) fireState.boltOffset = 10 * (elapsed / 80);
else if (elapsed < 150) { fireState.boltOffset = 10; if (!fireState.shellEjected) { playBoltAction(); fireState.shells.push({ x: 3, 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 }); fireState.shellEjected = true; } }
else if (elapsed < 300) fireState.boltOffset = 10 * (1 - ((elapsed - 150) / 150)); else fireState.boltOffset = 0;
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; fireState.boltOffset = 0; }
}
function drawFireEffects(ctx) {
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(); }
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(); });
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(); });
}
// =====================================================================
// 主循环
// =====================================================================
let time = 0;
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); time += 0.02;
const phase = time * 4.0;
const leftLegRot = Math.sin(phase) * 0.45; const rightLegRot = Math.sin(phase - Math.PI * 0.6) * 0.45;
// ★★★ 修正姿态 ★★★
// leftArm (画面左侧,即角色右手,拿扳机):微微抬起
// rightArm (画面右侧,即角色左手,托护木):向前伸出托住枪管
const leftArmRot = -0.9 + Math.sin(phase) * 0.05;
const rightArmRot = -1.4 + Math.sin(phase - Math.PI * 0.6) * 0.05;
const bob = Math.abs(Math.sin(phase)) * 3.5; const lean = 0.05;
const anim = { bob, lean, leftLegRot, rightLegRot, leftArmRot, rightArmRot };
const groundY = canvas.height - 100; const cx = canvas.width / 2; const scale = 1.8;
ctx.fillStyle = '#152a1f'; ctx.fillRect(0, groundY, canvas.width, canvas.height - groundY);
const shadowScale = 1 - bob * 0.015; ctx.fillStyle = 'rgba(0, 0, 0, 0.35)';
ctx.beginPath(); ctx.ellipse(cx, groundY + 5, 75 * shadowScale, 12 * shadowScale, 0, 0, Math.PI * 2); ctx.fill();
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);
const anchors = drawSoldier(ctx, cx, groundY, scale, anim);
// ★★★ 修正挂载点:绑定到画面左侧的手(角色右手,拿扳机) ★★★
ctx.save();
ctx.translateanchors.leftHand.x, anchors.leftHand.y); // 对准扳机手
ctx.rotate(-0.1); // 微微调整枪身角度
ctx.save();
ctx.translate(fireState.recoilX, -fireState.recoilY); // 后坐力
ctx.rotate(fireState.recoilRot);
drawHanyangRifle(ctx); // 画枪
ctx.restore();
ctx.save(); drawFireEffects(ctx); ctx.restore(); // 画特效
ctx.restore(); // 结束枪械上下文
ctx.restore(); // 结束摄像机上下文
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) => {
initAudio(); 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 });
window.addEventListener('keydown', (e) => { if (e.code === 'Space') { e.preventDefault(); initAudio(); triggerFire(); } });
requestAnimationFrame(animate);
</script>
</body>
</html>Game Source: 汉阳造步枪 - 双手持枪校正版
Creator: TurboKoala83
Libraries: none
Complexity: complex (275 lines, 20.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-mup5ehg5" to link back to the original. Then publish at arcadelab.ai/publish.