红军战士 - 受击动画联动测试
by TurboKoala83412 lines21.4 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>红军战士 - 受击动画联动测试</title>
<style>
body { margin: 0; background: #1a1a2e; overflow: hidden; font-family: 'Microsoft YaHei', sans-serif; touch-action: none; }
canvas { display: block; background: linear-gradient(180deg, #0f1a1a 0%, #1a2a2a 100%); }
.info { position: absolute; top: 10px; left: 50%; transform: translateX(-50%); color: #8A9A7A; font-size: 12px; letter-spacing: 2px; text-shadow: 0 0 10px rgba(138, 154, 122, 0.8); pointer-events: none; z-index: 10; }
</style>
</head>
<body>
<div class="info">★ 红军战士 · 受击动画联动测试 ★</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;
});
// ========== 配色 ==========
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 };
// ========== 状态机 ==========
let time = 0;
let lastTime = performance.now();
// 僵尸攻击状态
let attackState = { isAttacking: false, startTime: 0, duration: 1000, hasHit: false };
// ★ 主角受击状态
let playerHitState = {
isHit: false,
startTime: 0,
duration: 700, // 受击动画时长 700ms
};
// 摄像机
const camera = { x: 0, y: 0, zoom: 1.0, isDragging: false, dragButton: 0, lastX: 0, lastY: 0 };
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 lerp(start, end, amt) { return (1 - amt) * start + amt * end; }
function easeOut(t) { return 1 - Math.pow(1 - t, 3); }
function easeInOut(t) { return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t; }
// =====================================================================
// 绘制红军战士 (带受击动画)
// =====================================================================
function drawSoldier(ctx, centerX, groundY, scale = 1, anim = {}) {
const a = {
bob: anim.bob || 0,
headRot: anim.headRot || 0,
torsoRot: anim.torsoRot || 0,
leftArmRot: anim.leftArmRot || 0,
rightArmRot: anim.rightArmRot || 0,
leftLegRot: anim.leftLegRot || 0,
rightLegRot: anim.rightLegRot || 0,
recoilX: 0, // 受击位移
recoilY: 0,
lean: 0, // 身体前倾/后仰
};
// 计算受击进度
let hitProgress = 0;
if (playerHitState.isHit) {
hitProgress = Math.min(1, (performance.now() - playerHitState.startTime) / playerHitState.duration);
}
if (playerHitState.isHit) {
// 阶段1:冲击 (0-0.2) -> 上半身后仰,头后甩
if (hitProgress < 0.2) {
const t = hitProgress / 0.2;
a.lean = lerp(0, -0.4, t); // 身体后仰
a.headRot = lerp(0, 0.3, t); // 头部后甩
a.recoilX = lerp(0, -20, t); // 整体向后位移
a.leftArmRot = lerp(0, -0.8, t); // 手臂前甩
a.rightArmRot = lerp(0, -0.6, t);
a.bob = lerp(0, -10, t); // 下沉
}
// 阶段2:屈服 (0.2-0.5) -> 持续后仰,膝盖弯曲
else if (hitProgress < 0.5) {
const t = (hitProgress - 0.2) / 0.3;
a.lean = lerp(-0.4, -0.2, easeInOut(t));
a.headRot = lerp(0.3, 0.1, easeInOut(t));
a.recoilX = lerp(-20, -10, easeInOut(t));
a.leftArmRot = lerp(-0.8, -0.4, easeInOut(t));
a.rightArmRot = lerp(-0.6, -0.2, easeInOut(t));
a.bob = lerp(-10, 0, easeInOut(t));
a.leftLegRot = lerp(0, 0.3, easeInOut(t)); // 腿弯
a.rightLegRot = lerp(0, -0.3, easeInOut(t));
}
// 阶段3:恢复 (0.5-1.0) -> 前倾,找回重心
else {
const t = (hitProgress - 0.5) / 0.5;
a.lean = lerp(-0.2, 0, easeOut(t));
a.headRot = lerp(0.1, 0, easeOut(t));
a.recoilX = lerp(-10, 0, easeOut(t));
a.leftArmRot = lerp(-0.4, 0, easeOut(t));
a.rightArmRot = lerp(-0.2, 0, easeOut(t));
a.bob = lerp(0, 0, easeOut(t));
a.leftLegRot = lerp(0.3, 0, easeOut(t));
a.rightLegRot = lerp(-0.3, 0, easeOut(t));
}
}
if (playerHitState.isHit && hitProgress >= 1) {
playerHitState.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(centerX + a.recoilX * scale, groundY + a.bob);
ctx.scale(scale, scale);
ctx.rotate(a.lean);
// 腿
drawLeg(ctx, -legSpacing, 0, a.leftLegRot);
drawLeg(ctx, legSpacing, 0, a.rightLegRot);
// 躯干
drawTorso(ctx, 0, 0, a.torsoRot);
// 右臂
drawArm(ctx, armSpacing, shoulderY, a.rightArmRot, 'right');
// 头部
drawHead(ctx, 0, headY, a.headRot);
// 左臂
drawArm(ctx, -armSpacing, shoulderY, a.leftArmRot, 'left');
ctx.restore();
}
// 绘制红军战士的部件 (简化版,复用之前的逻辑)
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);
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 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, 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.collar; 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 = 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) {
ctx.save(); ctx.translate(x, y); ctx.rotate(rotation);
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.restore();
}
// =====================================================================
// 绘制僵尸 (复用之前的逻辑,保持完整)
// =====================================================================
function drawZombieHead(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 = 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 = '#1A1A1A'; ctx.fillRect(headX + headW - 17, headY + 22, 5, 2);
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 helmetW = headW + 20; const helmetH = 30; const helmetX = -helmetW / 2; const helmetY = headY - helmetH + 6;
ctx.fillStyle = Z.helmet; ctx.beginPath(); ctx.moveTo(helmetX + 10, helmetY); ctx.quadraticCurveTo(helmetX + helmetW / 2, helmetY - 8, helmetX + helmetW - 10, helmetY); ctx.quadraticCurveTo(helmetX + helmetW, helmetY + 10, helmetX + helmetW - 6, helmetY + helmetH); ctx.lineTo(helmetX + 6, helmetY + helmetH); ctx.quadraticCurveTo(helmetX, helmetY + 10, helmetX + 10, helmetY); ctx.closePath(); ctx.fill();
ctx.fillStyle = Z.helmetDark; ctx.beginPath(); ctx.moveTo(helmetX, helmetY + 8); ctx.lineTo(helmetX + 14, helmetY + 2); ctx.lineTo(helmetX + 14, helmetY + helmetH); ctx.lineTo(helmetX + 4, helmetY + helmetH); ctx.closePath(); ctx.fill();
ctx.fillStyle = Z.helmetLite; ctx.beginPath(); ctx.moveTo(helmetX + 18, helmetY + 2); ctx.quadraticCurveTo(helmetX + helmetW / 2, helmetY - 6, helmetX + helmetW - 18, helmetY + 2); ctx.lineTo(helmetX + helmetW - 22, helmetY + 6); ctx.quadraticCurveTo(helmetX + helmetW / 2, helmetY - 2, helmetX + 22, helmetY + 6); ctx.closePath(); ctx.fill();
ctx.fillStyle = '#2A2A2A'; ctx.beginPath(); ctx.moveTo(helmetX + 6, helmetY + helmetH - 4); ctx.lineTo(helmetX + helmetW - 6, helmetY + helmetH - 4); ctx.lineTo(helmetX + helmetW - 4, helmetY + helmetH); ctx.lineTo(helmetX + 4, helmetY + helmetH); ctx.closePath(); ctx.fill();
ctx.fillStyle = '#1A1A1A'; ctx.beginPath(); ctx.arc(helmetX + helmetW - 15, helmetY + 12, 5, 0, Math.PI * 2); ctx.fill();
ctx.restore();
}
function drawZombieTorso(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 = 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, rotation = 0, side = 'left') {
ctx.save(); ctx.translate(x, y); ctx.rotate(rotation);
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, rotation = 0, side = 'left') {
ctx.save(); ctx.translate(x, y); ctx.rotate(rotation);
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, centerX, groundY, scale = 1, anim = {}) {
let atkProgress = 0;
if (attackState.isAttacking) {
atkProgress = Math.min(1, (performance.now() - attackState.startTime) / attackState.duration);
}
let lungeOffset = 0, bodyPitch = 0, headPitch = 0, armForward = 0, legSplit = 0, bob = anim.bob || 0;
if (attackState.isAttacking) {
if (atkProgress < 0.3) {
const t = atkProgress / 0.3;
lungeOffset = lerp(0, -15, t); bodyPitch = lerp(0, -0.3, t); headPitch = lerp(0, 0.2, t);
armForward = lerp(0, -0.3, t); legSplit = lerp(0, 15, t); bob = lerp(bob, -10, t);
} else if (atkProgress < 0.5) {
const t = (atkProgress - 0.3) / 0.2;
lungeOffset = lerp(-15, 60, easeInOut(t)); bodyPitch = lerp(-0.3, 0.5, easeInOut(t));
headPitch = lerp(0.2, 0.4, easeInOut(t)); armForward = lerp(-0.3, 1.5, easeInOut(t));
legSplit = lerp(15, -10, easeInOut(t)); bob = lerp(-10, 5, easeInOut(t));
} else if (atkProgress < 0.7) {
const t = (atkProgress - 0.5) / 0.2;
lungeOffset = lerp(60, 65, t); bodyPitch = lerp(0.5, 0.6, t);
headPitch = lerp(0.4, -0.5, t); armForward = lerp(1.5, 1.8, t);
legSplit = lerp(-10, -5, t); bob = lerp(5, 0, t);
} else {
const t = (atkProgress - 0.7) / 0.3;
lungeOffset = lerp(65, 0, easeInOut(t)); bodyPitch = lerp(0.6, 0, easeInOut(t));
headPitch = lerp(-0.5, 0, easeInOut(t)); armForward = lerp(1.8, 0, easeInOut(t));
legSplit = lerp(-5, 0, easeInOut(t)); bob = lerp(0, anim.bob || 0, easeInOut(t));
}
}
if (attackState.isAttacking && atkProgress >= 1) attackState.isAttacking = false;
const shoulderY = -S.torsoH; const headY = shoulderY;
const legSpacing = S.torsoW / 4 - 4; const armSpacing = S.torsoW / 2 + S.armW / 2 - 2;
ctx.save();
ctx.translate(centerX + lungeOffset * scale, groundY + bob);
ctx.scale(scale, scale);
ctx.rotate(bodyPitch);
drawZombieLeg(ctx, -legSpacing, 0, (anim.leftLegRot || 0) + legSplit * 0.05, 'left');
drawZombieLeg(ctx, legSpacing, 0, (anim.rightLegRot || 0) - legSplit * 0.05, 'right');
drawZombieTorso(ctx, 0, 0, 0);
drawZombieArm(ctx, armSpacing, shoulderY, (anim.rightArmRot || 0) - armForward, 'right');
ctx.save(); ctx.translate(0, headY); ctx.rotate(headPitch); ctx.translate(0, -headY);
drawZombieHead(ctx, 0, headY, anim.headRot || 0); ctx.restore();
drawZombieArm(ctx, -armSpacing, shoulderY, (anim.leftArmRot || 0) - armForward, 'left');
ctx.restore();
}
// =====================================================================
// 主循环
// =====================================================================
function animate(now) {
const dt = (now - lastTime) / 1000; lastTime = now;
if (dt > 0.1) return;
ctx.clearRect(0, 0, W, H);
time += 0.02;
const phase = time * 3.5;
const leftLegRot = Math.sin(phase) * 0.35;
const rightLegRot = Math.sin(phase - Math.PI * 0.7) * 0.25 - 0.1;
const leftArmRot = -1.2 + Math.sin(phase) * 0.1;
const rightArmRot = -1.0 + Math.sin(phase - Math.PI * 0.7) * 0.1;
const bob = Math.abs(Math.sin(phase)) * 4.0 + Math.sin(phase * 2) * 1.5;
const headRot = Math.sin(phase * 3) * 0.08 + Math.sin(time * 0.5) * 0.04;
const zombieAnim = { bob, headRot, leftArmRot, rightArmRot, leftLegRot, rightLegRot };
const soldierAnim = { bob: 0, headRot: 0, torsoRot: 0, leftArmRot: 0, rightArmRot: 0, leftLegRot: 0, rightLegRot: 0 };
const groundY = H / 2 + 100;
const cx = W / 2;
const scale = 1.6;
// 地面
ctx.fillStyle = '#1A2A1A'; ctx.fillRect(0, groundY, W, H - groundY);
ctx.fillStyle = 'rgba(74, 26, 26, 0.2)';
ctx.beginPath(); ctx.ellipse(cx - 50, groundY + 10, 40, 8, 0, 0, Math.PI * 2); ctx.fill();
ctx.save();
ctx.translate(camera.x, camera.y); ctx.rotate(camera.rotation);
ctx.scale(camera.zoom, camera.zoom); ctx.translate(-camera.x, -camera.y);
// 绘制僵尸(在主角前面)
drawZombie(ctx, cx + 60, groundY, scale, zombieAnim);
// 绘制主角(在僵尸后面,受击时向后位移)
drawSoldier(ctx, cx, groundY, scale, soldierAnim);
ctx.restore();
// UI 信息
ctx.fillStyle = 'rgba(255, 255, 255, 0.5)'; ctx.font = '12px sans-serif';
ctx.fillText(`点击画面 / 按空格键 触发扑咬 | 缩放: ${camera.zoom.toFixed(2)}x`, 20, 30);
requestAnimationFrame(animate);
}
// =====================================================================
// 触发联动:僵尸扑咬 + 主角受击
// =====================================================================
function triggerAttack() {
if (attackState.isAttacking) return;
attackState.isAttacking = true;
attackState.startTime = performance.now();
// ★ 延迟一点点触发主角受击,模拟“被击中”的瞬间
setTimeout(() => {
playerHitState.isHit = true;
playerHitState.startTime = performance.now();
}, 400); // 400ms 后触发,对应僵尸扑击阶段
}
window.addEventListener('touchstart', (e) => { if (e.touches.length === 1) triggerAttack(); }, { passive: true });
window.addEventListener('mousedown', (e) => { if (e.button === 0) triggerAttack(); });
window.addEventListener('keydown', (e) => { if (e.code === 'Space') { e.preventDefault(); triggerAttack(); } });
// 摄像机拖拽
canvas.addEventListener('contextmenu', e => e.preventDefault());
canvas.addEventListener('mousedown', (e) => {
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 });
requestAnimationFrame(animate);
</script>
</body>
</html>Game Source: 红军战士 - 受击动画联动测试
Creator: TurboKoala83
Libraries: none
Complexity: complex (412 lines, 21.4 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-muqv223q" to link back to the original. Then publish at arcadelab.ai/publish.