血染红旗 - 测试版 v0.4 (广阔视野)
by TurboKoala83760 lines40.8 KB
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>血染红旗 - 测试版 v0.4 (广阔视野)</title>
<style>
body { margin: 0; background: #0a0a12; overflow: hidden; font-family: 'Microsoft YaHei', sans-serif; touch-action: none; }
canvas { display: block; }
.info { position: absolute; top: 8px; left: 50%; transform: translateX(-50%); color: #8A9A7A; font-size: 11px; letter-spacing: 3px; text-shadow: 0 0 10px rgba(138, 154, 122, 0.8); pointer-events: none; z-index: 10; }
</style>
</head>
<body>
<div class="info">★ 血染红旗 · 测试版 v0.4 ★</div>
<canvas id="game"></canvas>
<script>
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
let W = window.innerWidth;
let H = window.innerHeight;
canvas.width = W;
canvas.height = H;
window.addEventListener('resize', () => {
W = window.innerWidth; H = window.innerHeight;
canvas.width = W; canvas.height = H;
joystick.baseX = 100; joystick.baseY = H - 120;
if (!joystick.active) { joystick.thumbX = joystick.baseX; joystick.thumbY = joystick.baseY; }
});
// ============================================================
// 配色
// ============================================================
const Z = {
uniform:'#5A6B4A', uniformDark:'#3D4A32', uniformLite:'#7A8B6A',
skin:'#7A8A6A', skinDark:'#5A6A4A', skinRotten:'#4A5A3A',
blood:'#4A1A1A', bloodLite:'#7A2A2A', bloodFresh:'#8A1A1A',
bone:'#E8E0D0', helmet:'#4A4A4A', helmetDark:'#2A2A2A',
helmetLite:'#6A6A6A', eye:'#FF3333', eyeDark:'#8A1A1A',
strap:'#6B5A3A', bind:'#5A6A4A', shoe:'#1A1A1A',
};
const C = {
uniform:'#5A6B7C', uniformDark:'#3D4A57', uniformLite:'#6E8090',
collar:'#C41E3A', skin:'#E8B88A', skinDark:'#C99A6E',
hat:'#4A5A6B', hatDark:'#37444F', bind:'#8A8A8A',
straw:'#9A7A4A', strawDark:'#7A5A2A', belt:'#3A2A1A',
};
const S = { headW:52, headH:52, torsoW:84, torsoH:76, armW:36, armH:76, legW:44, legH:84 };
// ============================================================
// 游戏状态
// ============================================================
const gameState = {
player: { x: 0, y: 0, hp: 100, maxHp: 100, facing: 1, speed: 200, isHit: false, hitTime: 0, isAiming: false, killCount: 0 },
// ★ 修改1:摄像机缩放从 0.9 改为 0.6,视野拉大近一倍
camera: { x: 0, y: 0, zoom: 0.6, shakeX: 0, shakeY: 0 },
zombies: [], bullets: [], bloodParticles: [], damageTexts: [],
spawnTimer: 0, gameOver: false,
};
const joystick = { active: false, touchId: null, baseX: 100, baseY: H - 120, thumbX: 100, thumbY: H - 120, maxRadius: 60, dx: 0, dy: 0 };
const input = { aim: false, fire: false };
const gunState = { isFiring: false, fireTime: 0, recoilX: 0, recoilY: 0, recoilRot: 0, flashTimer: 0, boltOffset: 0, shellEjected: false, shells: [], smokeParticles: [] };
let lastTime = performance.now();
let time = 0;
function lerp(a, b, t) { return (1 - t) * a + t * b; }
function easeInOut(t) { return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t; }
function easeOut(t) { return 1 - Math.pow(1 - t, 3); }
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(); }
// 声音引擎
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 bs = audioCtx.sampleRate * 0.2; const buf = audioCtx.createBuffer(1, bs, audioCtx.sampleRate); const d = buf.getChannelData(0);
for (let i = 0; i < bs; i++) d[i] = Math.random() * 2 - 1;
const n = audioCtx.createBufferSource(); n.buffer = buf;
const f = audioCtx.createBiquadFilter(); f.type = 'lowpass'; f.frequency.value = 800;
const g = audioCtx.createGain(); g.gain.setValueAtTime(1, now); g.gain.exponentialRampToValueAtTime(0.01, now + 0.15);
n.connect(f); f.connect(g); g.connect(audioCtx.destination); n.start(now);
const o = audioCtx.createOscillator(); o.type = 'sine'; o.frequency.setValueAtTime(150, now); o.frequency.exponentialRampToValueAtTime(50, now + 0.1);
const og = audioCtx.createGain(); og.gain.setValueAtTime(1, now); og.gain.exponentialRampToValueAtTime(0.01, now + 0.15);
o.connect(og); og.connect(audioCtx.destination); o.start(now); o.stop(now + 0.2);
}
function playBoltAction() {
if (!audioCtx) return; const now = audioCtx.currentTime;
const bs = audioCtx.sampleRate * 0.05; const buf = audioCtx.createBuffer(1, bs, audioCtx.sampleRate); const d = buf.getChannelData(0);
for (let i = 0; i < bs; i++) d[i] = Math.random() * 2 - 1;
const n = audioCtx.createBufferSource(); n.buffer = buf;
const f = audioCtx.createBiquadFilter(); f.type = 'bandpass'; f.frequency.value = 3000; f.Q.value = 2;
const g = audioCtx.createGain(); g.gain.setValueAtTime(0.6, now); g.gain.exponentialRampToValueAtTime(0.01, now + 0.04);
n.connect(f); f.connect(g); g.connect(audioCtx.destination); n.start(now);
}
// ============================================================
// 绘制红军战士
// ============================================================
function drawHead(ctx, x, y, rotation = 0) {
ctx.save(); ctx.translate(x, y); ctx.rotate(rotation);
const { headW, headH } = S; const headX = -headW / 2; const headY = -headH;
ctx.fillStyle = C.skin; roundRect(ctx, headX, headY, headW, headH, 6); ctx.fill();
ctx.fillStyle = C.skinDark; 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 = C.hat; 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 = C.hatDark; ctx.fillRect(hatX + 2, hatY + hatH - 4, hatW - 4, 4);
const starCY = hatY + 9, starR = 7; ctx.fillStyle = C.collar; ctx.beginPath(); for (let i = 0; i < 5; i++) { const a = (i * 72 - 90) * Math.PI / 180; const x1 = starR * Math.cos(a), y1 = starCY + starR * Math.sin(a); const a2 = ((i + 1) * 72 - 90) * Math.PI / 180; const ir = starR * 0.4; const x2 = ir * Math.cos(a2 - Math.PI / 5), y2 = starCY + ir * Math.sin(a2 - 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, rotation = 0) {
ctx.save(); ctx.translate(x, y); ctx.rotate(rotation);
const { torsoW, torsoH } = S; const torsoX = -torsoW / 2; const torsoY = -torsoH;
ctx.fillStyle = C.uniform; roundRect(ctx, torsoX, torsoY, torsoW, torsoH, 4); ctx.fill();
ctx.fillStyle = C.uniformDark; ctx.fillRect(torsoX, torsoY, 8, torsoH);
ctx.fillStyle = C.uniformLite; ctx.fillRect(torsoX + torsoW - 6, torsoY, 6, torsoH);
ctx.fillStyle = C.collar; ctx.fillRect(torsoX + 12, torsoY + 2, 14, 8); ctx.fillRect(torsoX + torsoW - 26, torsoY + 2, 14, 8);
ctx.strokeStyle = C.uniformDark; 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 = C.belt; 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, scaleY = 1) {
ctx.save(); ctx.translate(x, y); ctx.rotate(rotation); ctx.scale(1, scaleY);
const { armW, armH } = S; const armX = -armW / 2;
ctx.fillStyle = C.uniform; roundRect(ctx, armX, 0, armW, armH - 12, 3); ctx.fill();
if (side === 'left') { ctx.fillStyle = C.uniformDark; ctx.fillRect(armX, 0, 5, armH - 12); }
else { ctx.fillStyle = C.uniformLite; ctx.fillRect(armX + armW - 5, 0, 5, armH - 12); }
ctx.fillStyle = C.skin; 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 = C.uniform; roundRect(ctx, legX, 0, legW, legH * 0.55, 3); ctx.fill();
ctx.fillStyle = C.uniformDark; ctx.fillRect(legX, 0, 6, legH * 0.55);
const bindY = legH * 0.5; const bindH = legH * 0.42;
ctx.fillStyle = C.bind; 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 = C.straw; roundRect(ctx, legX - 3, shoeY, legW + 6, shoeH, 3); ctx.fill();
ctx.strokeStyle = C.strawDark; 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 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(gunState.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 drawSoldier(ctx, worldX, worldY, scale, anim) {
const p = gameState.player;
const a = {
bob: anim.bob || 0, lean: anim.lean || 0, bodyRotation: anim.bodyRotation || 0,
leftLegRot: anim.leftLegRot || 0, rightLegRot: anim.rightLegRot || 0,
leftArmRot: anim.leftArmRot || 0, rightArmRot: anim.rightArmRot || 0,
facing: anim.facing || 1,
hitLean: 0, hitRecoilX: 0, headHitRot: 0,
};
if (p.isHit) {
const t = Math.min(1, (performance.now() - p.hitTime) / 700);
if (t < 0.2) { const k = t / 0.2; a.hitLean = lerp(0, -0.35, k); a.headHitRot = lerp(0, 0.35, k); a.hitRecoilX = lerp(0, -20, k); }
else if (t < 0.5) { const k = (t - 0.2) / 0.3; a.hitLean = lerp(-0.35, -0.15, easeInOut(k)); a.headHitRot = lerp(0.35, 0.1, easeInOut(k)); a.hitRecoilX = lerp(-20, -8, easeInOut(k)); }
else { const k = (t - 0.5) / 0.5; a.hitLean = lerp(-0.15, 0, easeOut(k)); a.headHitRot = lerp(0.1, 0, easeOut(k)); a.hitRecoilX = lerp(-8, 0, easeOut(k)); }
if (t >= 1) p.isHit = false;
}
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(worldX + a.hitRecoilX * a.facing, worldY + a.bob);
ctx.scale(a.facing * scale, scale);
ctx.rotate(a.lean + a.hitLean); ctx.rotate(a.bodyRotation);
drawLeg(ctx, -legSpacing, 0, a.leftLegRot);
drawLeg(ctx, legSpacing, 0, a.rightLegRot);
drawTorso(ctx, 0, 0, 0);
drawArm(ctx, armSpacing, shoulderY, a.rightArmRot, 'right', 0.85);
drawHead(ctx, 0, headY, a.headHitRot);
ctx.restore();
const handLY = (S.armH - 6);
const armC = Math.cos(a.leftArmRot); const armS = Math.sin(a.leftArmRot);
const localX = -armSpacing - handLY * armS;
const localY = shoulderY + handLY * armC;
const bodyC = Math.cos(a.bodyRotation); const bodyS = Math.sin(a.bodyRotation);
const rotX = localX * bodyC - localY * bodyS;
const rotY = localX * bodyS + localY * bodyC;
const leftHandX = worldX + (a.hitRecoilX + rotX) * a.facing * scale;
const leftHandY = worldY + a.bob + (rotY) * scale;
const aimP = p.isAiming ? 1 : 0;
const gunOffsetX = lerp(0, 15, aimP) * a.facing;
const gunOffsetY = lerp(0, -18, aimP);
ctx.save();
ctx.translate(leftHandX + gunOffsetX, leftHandY + gunOffsetY);
ctx.scale(a.facing, 1);
ctx.rotate(lerp(-0.05, 0, aimP));
ctx.save();
ctx.translate(gunState.recoilX, -gunState.recoilY); ctx.rotate(gunState.recoilRot);
drawHanyangRifle(ctx);
ctx.restore();
if (gunState.flashTimer > 0) {
const fa = gunState.flashTimer / 0.06;
ctx.save(); ctx.globalAlpha = fa; 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();
}
gunState.smokeParticles.forEach(pt => { ctx.save(); ctx.globalAlpha = pt.alpha; ctx.fillStyle = '#AAAAAA'; ctx.fillRect(pt.x - pt.size/2, pt.y - pt.size/2, pt.size, pt.size); ctx.restore(); });
gunState.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(); });
ctx.restore();
ctx.save();
ctx.translate(worldX + a.hitRecoilX * a.facing, worldY + a.bob);
ctx.scale(a.facing * scale, scale);
ctx.rotate(a.lean + a.hitLean); ctx.rotate(a.bodyRotation);
drawArm(ctx, -armSpacing, shoulderY, a.leftArmRot, 'left');
ctx.restore();
}
// ============================================================
// 绘制僵尸
// ============================================================
function drawZombieHead(ctx, x, y, rot = 0) {
ctx.save(); ctx.translate(x, y); ctx.rotate(rot);
const { headW, headH } = S; const headX = -headW/2; const headY = -headH;
ctx.fillStyle = Z.skinRotten; ctx.fillRect(-8,-8,16,8);
ctx.fillStyle = Z.skin; roundRect(ctx, headX, headY, headW, headH, 6); ctx.fill();
ctx.fillStyle = Z.skinDark; ctx.fillRect(headX, headY, 6, headH);
ctx.fillStyle = Z.skinRotten; ctx.fillRect(headX+6, headY+2, headW-12, 8);
ctx.fillStyle = '#1A1A1A'; ctx.fillRect(headX+10, headY+20, 9, 7);
ctx.fillStyle = Z.eye; ctx.fillRect(headX+12, headY+22, 5, 3);
ctx.fillStyle = Z.eyeDark; ctx.fillRect(headX+12, headY+22, 2, 1);
ctx.fillStyle = Z.blood; ctx.fillRect(headX+headW-19, headY+19, 9, 8);
ctx.fillStyle = Z.bloodFresh; ctx.fillRect(headX+headW-18, headY+21, 7, 4);
ctx.fillStyle = Z.blood; ctx.beginPath(); ctx.moveTo(headX+12,headY+36); ctx.lineTo(headX+20,headY+33); ctx.lineTo(headX+28,headY+38); ctx.lineTo(headX+36,headY+34); ctx.lineTo(headX+40,headY+40); ctx.lineTo(headX+30,headY+43); ctx.lineTo(headX+18,headY+42); ctx.closePath(); ctx.fill();
ctx.fillStyle = Z.bone; ctx.fillRect(headX+18, headY+36, 4, 4); ctx.fillRect(headX+24, headY+37, 4, 3); ctx.fillRect(headX+30, headY+36, 4, 4);
const hw = headW+20, hh = 30, hx = -hw/2, hy = headY - hh + 6;
ctx.fillStyle = Z.helmet; ctx.beginPath(); ctx.moveTo(hx+10,hy); ctx.quadraticCurveTo(hx+hw/2, hy-8, hx+hw-10, hy); ctx.quadraticCurveTo(hx+hw, hy+10, hx+hw-6, hy+hh); ctx.lineTo(hx+6, hy+hh); ctx.quadraticCurveTo(hx, hy+10, hx+10, hy); ctx.closePath(); ctx.fill();
ctx.fillStyle = Z.helmetDark; ctx.beginPath(); ctx.moveTo(hx, hy+8); ctx.lineTo(hx+14, hy+2); ctx.lineTo(hx+14, hy+hh); ctx.lineTo(hx+4, hy+hh); ctx.closePath(); ctx.fill();
ctx.fillStyle = Z.helmetLite; ctx.beginPath(); ctx.moveTo(hx+18, hy+2); ctx.quadraticCurveTo(hx+hw/2, hy-6, hx+hw-18, hy+2); ctx.lineTo(hx+hw-22, hy+6); ctx.quadraticCurveTo(hx+hw/2, hy-2, hx+22, hy+6); ctx.closePath(); ctx.fill();
ctx.fillStyle = '#2A2A2A'; ctx.beginPath(); ctx.moveTo(hx+6, hy+hh-4); ctx.lineTo(hx+hw-6, hy+hh-4); ctx.lineTo(hx+hw-4, hy+hh); ctx.lineTo(hx+4, hy+hh); ctx.closePath(); ctx.fill();
ctx.fillStyle = '#1A1A1A'; ctx.beginPath(); ctx.arc(hx+hw-15, hy+12, 5, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = Z.blood; ctx.fillRect(hx+hw-17, hy+14, 3, 8);
ctx.restore();
}
function drawZombieTorso(ctx, x, y, rot = 0) {
ctx.save(); ctx.translate(x, y); ctx.rotate(rot);
const { torsoW, torsoH } = S; const torsoX = -torsoW/2; const torsoY = -torsoH;
ctx.fillStyle = Z.uniform; roundRect(ctx, torsoX, torsoY, torsoW, torsoH, 4); ctx.fill();
ctx.fillStyle = Z.uniformDark; ctx.fillRect(torsoX, torsoY, 8, torsoH);
ctx.fillStyle = Z.uniformLite; ctx.fillRect(torsoX+torsoW-6, torsoY, 6, torsoH);
ctx.fillStyle = '#4A5A6A'; ctx.fillRect(torsoX+12, torsoY+2, 14, 8); ctx.fillRect(torsoX+torsoW-26, torsoY+2, 14, 8);
const beltY = torsoY + torsoH*0.55; ctx.fillStyle = '#2A1A0A'; ctx.fillRect(torsoX, beltY, torsoW, 7);
ctx.strokeStyle = Z.strap; ctx.lineWidth = 6;
ctx.beginPath(); ctx.moveTo(torsoX+8, torsoY+4); ctx.lineTo(torsoX+torsoW-8, beltY+3); ctx.stroke();
ctx.beginPath(); ctx.moveTo(torsoX+torsoW-8, torsoY+4); ctx.lineTo(torsoX+8, beltY+3); ctx.stroke();
ctx.fillStyle = '#5A4A3A'; ctx.fillRect(torsoX+20, torsoY+20, 10, 12); ctx.fillRect(torsoX+20, torsoY+36, 10, 12);
ctx.fillRect(torsoX+torsoW-30, torsoY+20, 10, 12); ctx.fillRect(torsoX+torsoW-30, torsoY+36, 10, 12);
ctx.fillStyle = Z.blood; ctx.beginPath(); ctx.moveTo(torsoX+torsoW-25, torsoY+15); ctx.lineTo(torsoX+torsoW-15, torsoY+20); ctx.lineTo(torsoX+torsoW-20, torsoY+30); ctx.closePath(); ctx.fill();
ctx.restore();
}
function drawZombieArm(ctx, x, y, rot = 0, side = 'left') {
ctx.save(); ctx.translate(x, y); ctx.rotate(rot);
const { armW, armH } = S; const armX = -armW/2;
ctx.fillStyle = Z.uniform; roundRect(ctx, armX, 0, armW, armH-12, 3); ctx.fill();
if (side === 'left') { ctx.fillStyle = Z.uniformDark; ctx.fillRect(armX, 0, 5, armH-12); }
else { ctx.fillStyle = Z.uniformLite; ctx.fillRect(armX+armW-5, 0, 5, armH-12); }
ctx.fillStyle = Z.skin; roundRect(ctx, armX+4, armH-14, armW-8, 14, 3); ctx.fill();
ctx.strokeStyle = Z.skinDark; ctx.lineWidth = 2;
for (let i = 0; i < 3; i++) { ctx.beginPath(); ctx.moveTo(armX+8+i*6, armH); ctx.lineTo(armX+8+i*6, armH+8); ctx.stroke(); }
ctx.restore();
}
function drawZombieLeg(ctx, x, y, rot = 0, side = 'left') {
ctx.save(); ctx.translate(x, y); ctx.rotate(rot);
const { legW, legH } = S; const legX = -legW/2;
ctx.fillStyle = Z.uniform; roundRect(ctx, legX, 0, legW, legH*0.55, 3); ctx.fill();
ctx.fillStyle = Z.uniformDark; ctx.fillRect(legX, 0, 6, legH*0.55);
const bindY = legH*0.5; const bindH = legH*0.42;
ctx.fillStyle = Z.bind; roundRect(ctx, legX-1, bindY, legW+2, bindH, 2); ctx.fill();
const shoeY = bindY+bindH; const shoeH = legH*0.08;
ctx.fillStyle = Z.shoe; roundRect(ctx, legX-3, shoeY, legW+6, shoeH, 3); ctx.fill();
ctx.restore();
}
function drawZombie(ctx, z, scale = 1) {
const now = performance.now();
let leftLegRot = 0, rightLegRot = 0, leftArmRot = 0, rightArmRot = 0;
let bob = 0, headRot = 0, lungeOffset = 0, bodyPitch = 0, headPitch = 0, armForward = 0, legSplit = 0;
let deathAlpha = 1;
if (z.state === 'dead') {
const deathTime = now - z.deathTime;
const fallProg = Math.min(1, deathTime / 600);
const easeFall = easeOut(fallProg);
bodyPitch = -1.5 * easeFall;
lungeOffset = -20 * easeFall;
bob = 0;
armForward = 0;
leftArmRot = 0; rightArmRot = 0;
leftLegRot = -0.2; rightLegRot = 0.2;
if (deathTime > 800) {
deathAlpha = Math.max(0, 1 - (deathTime - 800) / 700);
}
}
else if (z.state === 'hit') {
const hitProg = Math.min(1, (now - z.hitTime) / 250);
const ease = easeOut(hitProg);
bodyPitch = -0.5 * (1 - ease);
lungeOffset = -50 * (1 - ease);
armForward = -0.8 * (1 - ease);
headPitch = -0.3 * (1 - ease);
leftArmRot = -0.5 - 0.5 * (1 - ease);
rightArmRot = -0.5 - 0.5 * (1 - ease);
bob = 0;
}
else if (z.state === 'attack') {
const atkProgress = Math.min(1, (now - z.attackStartTime) / 800);
if (atkProgress < 0.3) {
const k = atkProgress / 0.3;
lungeOffset = lerp(0, -15, k); bodyPitch = lerp(0, -0.3, k); headPitch = lerp(0, 0.2, k);
armForward = lerp(0, -0.3, k); legSplit = lerp(0, 15, k);
} else if (atkProgress < 0.5) {
const k = (atkProgress - 0.3) / 0.2;
lungeOffset = lerp(-15, 60, easeInOut(k)); bodyPitch = lerp(-0.3, 0.5, easeInOut(k));
headPitch = lerp(0.2, 0.4, easeInOut(k)); armForward = lerp(-0.3, 1.5, easeInOut(k));
legSplit = lerp(15, -10, easeInOut(k));
} else if (atkProgress < 0.7) {
const k = (atkProgress - 0.5) / 0.2;
lungeOffset = lerp(60, 65, k); bodyPitch = lerp(0.5, 0.6, k);
headPitch = lerp(0.4, -0.5, k); armForward = lerp(1.5, 1.8, k);
} else {
const k = (atkProgress - 0.7) / 0.3;
lungeOffset = lerp(65, 0, easeInOut(k)); bodyPitch = lerp(0.6, 0, easeInOut(k));
headPitch = lerp(-0.5, 0, easeInOut(k)); armForward = lerp(1.8, 0, easeInOut(k));
}
if (atkProgress >= 1) z.state = 'walk';
}
else {
const phase = time * 3.5 + (z.seed || 0) * 10;
leftLegRot = Math.sin(phase) * 0.35;
rightLegRot = Math.sin(phase - Math.PI * 0.7) * 0.25 - 0.1;
leftArmRot = -1.2 + Math.sin(phase) * 0.1;
rightArmRot = -1.0 + Math.sin(phase - Math.PI * 0.7) * 0.1;
bob = Math.abs(Math.sin(phase)) * 4.0;
headRot = Math.sin(phase * 3) * 0.08;
}
const shoulderY = -S.torsoH; const headY = shoulderY;
const legSpacing = S.torsoW/4 - 4; const armSpacing = S.torsoW/2 + S.armW/2 - 2;
ctx.save();
if (deathAlpha < 1) {
ctx.globalAlpha = deathAlpha;
}
ctx.translate(z.x + lungeOffset * z.facing * scale, z.y + bob);
ctx.scale(z.facing * scale, scale);
ctx.rotate(bodyPitch);
drawZombieLeg(ctx, -legSpacing, 0, leftLegRot + legSplit*0.05, 'left');
drawZombieLeg(ctx, legSpacing, 0, rightLegRot - legSplit*0.05, 'right');
drawZombieTorso(ctx, 0, 0, 0);
drawZombieArm(ctx, armSpacing, shoulderY, rightArmRot - armForward, 'right');
ctx.save(); ctx.translate(0, headY); ctx.rotate(headPitch); ctx.translate(0, -headY);
drawZombieHead(ctx, 0, headY, headRot); ctx.restore();
drawZombieArm(ctx, -armSpacing, shoulderY, leftArmRot - armForward, 'left');
ctx.restore();
if (z.state !== 'dead' && z.hp < z.maxHp) {
const barW = 50, barH = 5;
const bx = z.x - barW/2, by = z.y - 200;
ctx.fillStyle = 'rgba(0,0,0,0.6)'; ctx.fillRect(bx-1, by-1, barW+2, barH+2);
ctx.fillStyle = '#8A1A1A'; ctx.fillRect(bx, by, barW, barH);
ctx.fillStyle = '#C41E3A'; ctx.fillRect(bx, by, barW * (z.hp / z.maxHp), barH);
}
}
// ============================================================
// 生成僵尸
// ============================================================
function spawnZombie() {
const p = gameState.player;
const angle = Math.random() * Math.PI * 2;
// ★ 修改2:生成距离拉大(800~1200),避免在屏幕边缘突然出现
const range = 800 + Math.random() * 400;
const z = {
x: p.x + Math.cos(angle) * range, y: p.y + Math.sin(angle) * range * 0.3,
hp: 60, maxHp: 60, speed: 50 + Math.random() * 30,
state: 'walk', attackStartTime: 0, attackCooldown: 0, hitTime: 0, deathTime: 0,
facing: 1, seed: Math.random(), hasHitPlayer: false,
};
gameState.zombies.push(z);
}
// ============================================================
// 射击
// ============================================================
function fire() {
if (gunState.isFiring) return;
const p = gameState.player;
gunState.isFiring = true;
gunState.fireTime = performance.now();
gunState.flashTimer = 0.06;
gunState.boltOffset = 0;
gunState.shellEjected = false;
playGunshot();
gameState.camera.shakeX = (Math.random() - 0.5) * 10;
gameState.camera.shakeY = (Math.random() - 0.5) * 10;
const dir = p.facing;
const startX = p.x + dir * 200;
const startY = p.y - 100;
gameState.bullets.push({
x: startX, y: startY, prevX: startX, prevY: startY,
vx: dir * 1800, life: 1.0,
});
for (let i = 0; i < 5; i++) {
gunState.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 updateGun(dt) {
if (!gunState.isFiring) return;
const now = performance.now();
const elapsed = now - gunState.fireTime;
if (elapsed < 150) { const t = elapsed / 150; gunState.recoilX = -15 * Math.sin(t * Math.PI); gunState.recoilY = 8 * Math.sin(t * Math.PI); gunState.recoilRot = 0.08 * Math.sin(t * Math.PI); }
else { const t = Math.min((elapsed - 150) / 200, 1); const eo = 1 - Math.pow(1 - t, 3); gunState.recoilX = -15 * (1 - eo); gunState.recoilY = 8 * (1 - eo); gunState.recoilRot = 0.08 * (1 - eo); }
if (gunState.flashTimer > 0) gunState.flashTimer -= dt;
if (elapsed < 80) gunState.boltOffset = 10 * (elapsed / 80);
else if (elapsed < 150) { gunState.boltOffset = 10; if (!gunState.shellEjected) { playBoltAction(); gunState.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 }); gunState.shellEjected = true; } }
else if (elapsed < 300) gunState.boltOffset = 10 * (1 - ((elapsed - 150) / 150));
else gunState.boltOffset = 0;
for (let i = gunState.shells.length - 1; i >= 0; i--) { const s = gunState.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) gunState.shells.splice(i, 1); }
for (let i = gunState.smokeParticles.length - 1; i >= 0; i--) { const pt = gunState.smokeParticles[i]; pt.x += pt.vx; pt.y += pt.vy; pt.vy -= 0.1; pt.vx *= 0.96; pt.size += 0.5; pt.alpha -= pt.decay; pt.life -= pt.decay; if (pt.alpha <= 0 || pt.life <= 0) gunState.smokeParticles.splice(i, 1); }
if (elapsed > 500 && gunState.flashTimer <= 0 && gunState.shells.length === 0 && gunState.smokeParticles.length === 0) { gunState.isFiring = false; gunState.recoilX = 0; gunState.recoilY = 0; gunState.recoilRot = 0; }
}
// ============================================================
// 主循环
// ============================================================
function animate(now) {
const dt = (now - lastTime) / 1000; lastTime = now;
if (dt > 0.1) return;
time += dt;
const p = gameState.player;
const cam = gameState.camera;
if (!gameState.gameOver) {
let vx = joystick.dx * p.speed;
let vy = joystick.dy * p.speed;
if (p.isAiming) { vx *= 0.2; vy *= 0.2; }
const mag = Math.hypot(vx, vy);
if (mag > p.speed) { vx = (vx/mag)*p.speed; vy = (vy/mag)*p.speed; }
p.x += vx * dt; p.y += vy * dt;
if (vx > 10) p.facing = 1;
if (vx < -10) p.facing = -1;
p.isAiming = input.aim;
gameState.zombies.forEach(z => {
if (z.state === 'dead') return;
if (z.state === 'hit') {
if (now - z.hitTime > 250) z.state = 'walk';
return;
}
if (z.state === 'attack') return;
const dx = p.x - z.x; const dy = p.y - z.y; const d = Math.hypot(dx, dy);
if (d < 150 && z.attackCooldown <= 0) {
z.state = 'attack'; z.attackStartTime = now;
z.attackCooldown = 1500; z.hasHitPlayer = false;
setTimeout(() => {
if (z.hasHitPlayer || z.state === 'dead') return;
const dd = Math.hypot(p.x - z.x, p.y - z.y);
if (dd < 250) {
p.hp -= 15; p.isHit = true; p.hitTime = performance.now();
gameState.camera.shakeX = (Math.random() - 0.5) * 20; gameState.camera.shakeY = (Math.random() - 0.5) * 20;
gameState.damageTexts.push({ x: p.x, y: p.y - 200, text: '-15', life: 1.0, color: '#FF3333' });
}
z.hasHitPlayer = true;
}, 500);
} else if (d > 0) {
const speed = z.speed;
z.x += (dx / d) * speed * dt; z.y += (dy / d) * speed * dt;
z.facing = dx > 0 ? 1 : -1;
}
if (z.attackCooldown > 0) z.attackCooldown -= dt * 1000;
});
for (let i = gameState.zombies.length - 1; i >= 0; i--) {
const z = gameState.zombies[i];
if (z.hp <= 0 && z.state !== 'dead') {
z.state = 'dead';
z.deathTime = now;
p.killCount++;
for (let k = 0; k < 15; k++) {
gameState.bloodParticles.push({ x: z.x, y: z.y - 100, vx: (Math.random()-0.5)*500, vy: -Math.random()*400, life: 0.8, size: 3 + Math.random()*6 });
}
gameState.damageTexts.push({ x: z.x, y: z.y - 250, text: '击杀', life: 1.2, color: '#FFDD44' });
}
if (z.state === 'dead' && now - z.deathTime > 1500) {
gameState.zombies.splice(i, 1);
}
}
for (let i = gameState.bullets.length - 1; i >= 0; i--) {
const b = gameState.bullets[i];
b.prevX = b.x; b.prevY = b.y;
b.x += b.vx * dt;
b.life -= dt;
let hit = false;
for (let j = gameState.zombies.length - 1; j >= 0; j--) {
const z = gameState.zombies[j];
if (z.state === 'dead') continue;
const boxX = z.x - 50; const boxY = z.y - 200; const boxW = 100; const boxH = 200;
if (b.x > boxX && b.x < boxX + boxW && b.y > boxY && b.y < boxY + boxH) {
z.hp -= 40;
if (z.hp > 0) {
z.state = 'hit'; z.hitTime = now;
}
gameState.damageTexts.push({ x: z.x, y: z.y - 220, text: '-40', life: 1.0, color: '#FFAA00' });
for (let k = 0; k < 10; k++) {
gameState.bloodParticles.push({ x: z.x, y: z.y - 100, vx: (Math.random()-0.5)*400, vy: -Math.random()*300, life: 0.6, size: 3 + Math.random()*5 });
}
hit = true; break;
}
}
if (hit || b.life <= 0) gameState.bullets.splice(i, 1);
}
for (let i = gameState.bloodParticles.length - 1; i >= 0; i--) {
const bp = gameState.bloodParticles[i];
bp.vy += 800 * dt; bp.x += bp.vx * dt; bp.y += bp.vy * dt; bp.life -= dt;
if (bp.life <= 0) gameState.bloodParticles.splice(i, 1);
}
for (let i = gameState.damageTexts.length - 1; i >= 0; i--) {
const t = gameState.damageTexts[i]; t.y -= 40 * dt; t.life -= dt;
if (t.life <= 0) gameState.damageTexts.splice(i, 1);
}
gameState.spawnTimer -= dt;
// ★ 修改3:僵尸上限从 8 提升到 12
if (gameState.spawnTimer <= 0 && gameState.zombies.length < 12) { spawnZombie(); gameState.spawnTimer = 2.0 + Math.random() * 2.0; }
if (p.hp <= 0) { p.hp = 0; gameState.gameOver = true; }
cam.x = lerp(cam.x, p.x, 0.08); cam.y = lerp(cam.y, p.y, 0.08);
}
updateGun(dt);
ctx.clearRect(0, 0, W, H);
ctx.fillStyle = '#0A0A12'; ctx.fillRect(0, 0, W, H);
ctx.save();
ctx.translate(W/2, H/2 + 80);
ctx.scale(cam.zoom, cam.zoom);
ctx.translate(-cam.x + cam.shakeX, -cam.y + cam.shakeY);
ctx.fillStyle = '#0F1A0F'; ctx.fillRect(cam.x - W, cam.y - H, W * 2, H * 2);
// ★ 修改4:网格尺寸从 100 改为 150
const gridSize = 150;
ctx.strokeStyle = 'rgba(74, 90, 74, 0.15)'; ctx.lineWidth = 1;
const startX = Math.floor((cam.x - W) / gridSize) * gridSize;
const startY = Math.floor((cam.y - H) / gridSize) * gridSize;
for (let x = startX; x < cam.x + W; x += gridSize) { for (let y = startY; y < cam.y + H; y += gridSize) { ctx.strokeRect(x, y, gridSize, gridSize); } }
ctx.fillStyle = 'rgba(74, 26, 26, 0.15)';
for (let x = startX; x < cam.x + W; x += gridSize * 3) {
for (let y = startY; y < cam.y + H; y += gridSize * 3) {
const seed = Math.sin(x * 12.9898 + y * 78.233) * 43758.5453; const rand = seed - Math.floor(seed);
if (rand > 0.8) { ctx.beginPath(); ctx.ellipse(x + 50, y + 50, 30, 10, 0, 0, Math.PI * 2); ctx.fill(); }
}
}
gameState.bloodParticles.forEach(bp => { ctx.fillStyle = `rgba(138, 26, 26, ${bp.life})`; ctx.fillRect(bp.x - bp.size/2, bp.y - bp.size/2, bp.size, bp.size); });
gameState.zombies.forEach(z => drawZombie(ctx, z, 1.5));
let moveState = 'idle';
if (Math.abs(joystick.dx) > 0.05 || Math.abs(joystick.dy) > 0.05) moveState = 'walk';
const phase = time * 4.0;
const walkLeftLeg = Math.sin(phase) * 0.45 * (moveState === 'walk' ? 1 : 0);
const walkRightLeg = Math.sin(phase - Math.PI*0.6) * 0.45 * (moveState === 'walk' ? 1 : 0);
const aimP = p.isAiming ? 1 : 0;
const soldierAnim = {
bob: lerp(Math.abs(Math.sin(phase)) * 3 * (moveState === 'walk' ? 1 : 0), 0, aimP),
lean: lerp(0.05, 0.1, aimP), bodyRotation: lerp(0, -0.2, aimP),
leftLegRot: lerp(walkLeftLeg, 0.05, aimP), rightLegRot: lerp(walkRightLeg, -0.05, aimP),
leftArmRot: lerp(-0.7 + Math.sin(phase)*0.05*(moveState==='walk'?1:0), -1.1, aimP),
rightArmRot: lerp(0.1 + Math.sin(phase - Math.PI*0.6)*0.05*(moveState==='walk'?1:0), -0.45, aimP),
facing: p.facing,
};
drawSoldier(ctx, p.x, p.y, 1.5, soldierAnim);
gameState.bullets.forEach(b => {
ctx.fillStyle = '#FFDD44'; ctx.fillRect(b.x - 12, b.y - 2, 24, 4);
ctx.fillStyle = '#FFFFFF'; ctx.fillRect(b.x - 12, b.y - 1, 24, 2);
});
gameState.damageTexts.forEach(t => {
ctx.save(); ctx.globalAlpha = t.life; ctx.fillStyle = t.color; ctx.font = 'bold 20px sans-serif'; ctx.textAlign = 'center';
ctx.fillText(t.text, t.x, t.y); ctx.restore();
});
ctx.restore();
ctx.fillStyle = 'rgba(0,0,0,0.6)'; ctx.fillRect(20, 20, 200, 24);
ctx.strokeStyle = '#444'; ctx.lineWidth = 2; ctx.strokeRect(20, 20, 200, 24);
const hpRatio = p.hp / p.maxHp; ctx.fillStyle = hpRatio > 0.5 ? '#C41E3A' : '#FF3333';
ctx.fillRect(22, 22, 196 * hpRatio, 20);
ctx.fillStyle = '#FFF'; ctx.font = 'bold 14px sans-serif'; ctx.textAlign = 'left';
ctx.fillText(`HP: ${Math.ceil(p.hp)} / ${p.maxHp}`, 30, 37);
ctx.fillStyle = 'rgba(0,0,0,0.6)'; ctx.fillRect(W - 150, 20, 130, 24);
ctx.strokeStyle = '#444'; ctx.strokeRect(W - 150, 20, 130, 24);
ctx.fillStyle = '#C41E3A'; ctx.font = 'bold 14px sans-serif'; ctx.textAlign = 'center';
ctx.fillText(`击杀: ${p.killCount}`, W - 85, 37);
if (gameState.gameOver) {
ctx.fillStyle = 'rgba(0,0,0,0.7)'; ctx.fillRect(0, 0, W, H);
ctx.fillStyle = '#C41E3A'; ctx.font = 'bold 40px sans-serif'; ctx.textAlign = 'center';
ctx.fillText('战 死', W/2, H/2 - 20);
ctx.fillStyle = '#FFF'; ctx.font = '18px sans-serif';
ctx.fillText(`最终击杀: ${p.killCount}`, W/2, H/2 + 30);
ctx.font = '14px sans-serif'; ctx.fillStyle = '#888';
ctx.fillText('点击屏幕重新开始', W/2, H/2 + 70);
}
ctx.save(); ctx.globalAlpha = 0.6;
ctx.fillStyle = 'rgba(255,255,255,0.1)'; ctx.beginPath(); ctx.arc(joystick.baseX, joystick.baseY, joystick.maxRadius, 0, Math.PI*2); ctx.fill();
ctx.strokeStyle = 'rgba(255,255,255,0.3)'; ctx.lineWidth = 2; ctx.stroke();
ctx.fillStyle = joystick.active ? 'rgba(196, 30, 58, 0.8)' : 'rgba(196, 30, 58, 0.4)';
ctx.beginPath(); ctx.arc(joystick.thumbX, joystick.thumbY, 25, 0, Math.PI*2); ctx.fill();
ctx.strokeStyle = 'rgba(255,255,255,0.6)'; ctx.stroke(); ctx.restore();
ctx.save();
ctx.fillStyle = input.aim ? 'rgba(196, 30, 58, 0.9)' : 'rgba(255,255,255,0.15)';
ctx.beginPath(); ctx.arc(W - 110, H - 70, 35, 0, Math.PI*2); ctx.fill();
ctx.strokeStyle = 'rgba(255,255,255,0.5)'; ctx.lineWidth = 2; ctx.stroke();
ctx.fillStyle = '#fff'; ctx.font = 'bold 14px sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText('瞄准', W - 110, H - 70);
ctx.fillStyle = input.fire ? 'rgba(196, 30, 58, 0.95)' : 'rgba(255,255,255,0.15)';
ctx.beginPath(); ctx.arc(W - 50, H - 140, 45, 0, Math.PI*2); ctx.fill();
ctx.strokeStyle = 'rgba(255,255,255,0.5)'; ctx.stroke();
ctx.fillStyle = '#fff'; ctx.font = 'bold 16px sans-serif';
ctx.fillText('开火', W - 50, H - 140);
ctx.restore();
requestAnimationFrame(animate);
}
// ============================================================
// 输入事件
// ============================================================
function handleTouchStart(e) {
e.preventDefault(); initAudio();
if (gameState.gameOver) {
gameState.gameOver = false; gameState.player.hp = 100; gameState.player.killCount = 0;
gameState.player.x = 0; gameState.player.y = 0; gameState.zombies = []; gameState.bullets = [];
gameState.bloodParticles = []; gameState.damageTexts = []; return;
}
for (let i = 0; i < e.touches.length; i++) {
const touch = e.touches[i]; const tx = touch.clientX; const ty = touch.clientY;
if (tx < W * 0.5) {
if (!joystick.active) {
joystick.active = true; joystick.touchId = touch.identifier;
const dx = tx - joystick.baseX; const dy = ty - joystick.baseY; const d = Math.hypot(dx, dy);
if (d < joystick.maxRadius) { joystick.thumbX = tx; joystick.thumbY = ty; joystick.dx = dx / joystick.maxRadius; joystick.dy = dy / joystick.maxRadius; }
else { joystick.thumbX = joystick.baseX + (dx/d)*joystick.maxRadius; joystick.thumbY = joystick.baseY + (dy/d)*joystick.maxRadius; joystick.dx = dx / d; joystick.dy = dy / d; }
}
} else {
const dAim = Math.hypot(tx - (W - 110), ty - (H - 70)); const dFire = Math.hypot(tx - (W - 50), ty - (H - 140));
if (dFire < 45) { input.fire = true; fire(); } else if (dAim < 35) { input.aim = true; }
}
}
}
function handleTouchMove(e) {
e.preventDefault();
for (let i = 0; i < e.touches.length; i++) {
const touch = e.touches[i];
if (touch.identifier === joystick.touchId) {
const tx = touch.clientX; const ty = touch.clientY;
const dx = tx - joystick.baseX; const dy = ty - joystick.baseY; const d = Math.hypot(dx, dy);
if (d < joystick.maxRadius) { joystick.thumbX = tx; joystick.thumbY = ty; joystick.dx = dx / joystick.maxRadius; joystick.dy = dy / joystick.maxRadius; }
else { joystick.thumbX = joystick.baseX + (dx/d)*joystick.maxRadius; joystick.thumbY = joystick.baseY + (dy/d)*joystick.maxRadius; joystick.dx = dx / d; joystick.dy = dy / d; }
}
}
}
function handleTouchEnd(e) {
e.preventDefault();
let stillActive = false;
for (let i = 0; i < e.touches.length; i++) { if (e.touches[i].identifier === joystick.touchId) stillActive = true; }
if (!stillActive) { joystick.active = false; joystick.thumbX = joystick.baseX; joystick.thumbY = joystick.baseY; joystick.dx = 0; joystick.dy = 0; }
input.aim = false; input.fire = false;
}
window.addEventListener('touchstart', handleTouchStart, { passive: false });
window.addEventListener('touchmove', handleTouchMove, { passive: false });
window.addEventListener('touchend', handleTouchEnd, { passive: false });
window.addEventListener('touchcancel', handleTouchEnd, { passive: false });
window.addEventListener('mousedown', (e) => {
initAudio();
if (gameState.gameOver) { gameState.gameOver = false; gameState.player.hp = 100; gameState.player.killCount = 0; gameState.player.x = 0; gameState.player.y = 0; gameState.zombies = []; return; }
if (e.button === 0) fire();
});
window.addEventListener('keydown', e => { if (e.code === 'Space') { e.preventDefault(); fire(); } });
requestAnimationFrame(animate);
</script>
</body>
</html>Game Source: 血染红旗 - 测试版 v0.4 (广阔视野)
Creator: TurboKoala83
Libraries: none
Complexity: complex (760 lines, 40.8 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: v0-4-turbokoala83" to link back to the original. Then publish at arcadelab.ai/publish.