<!DOCTYPE html>
<html lang="en">
<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>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
user-select: none; /* 禁止选中,提升按钮点击感 */
}
body {
min-height: 100vh;
background: linear-gradient(145deg, #5db0ff 0%, #b2dbff 100%);
display: flex;
justify-content: center;
align-items: center;
font-family: 'Segoe UI', Roboto, system-ui, sans-serif;
padding: 12px;
margin: 0;
}
.game-wrapper {
max-width: 900px;
width: 100%;
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(4px);
border-radius: 48px 48px 32px 32px;
padding: 20px 20px 30px;
box-shadow: 0 20px 30px rgba(0, 30, 60, 0.5);
border: 1px solid rgba(255,255,255,0.4);
}
/* 顶部信息栏:左(关卡/血量) 右(武器) */
.top-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 10px 15px 10px;
flex-wrap: wrap;
gap: 10px;
}
.stats {
background: rgba(10, 30, 50, 0.7);
backdrop-filter: blur(6px);
padding: 12px 24px;
border-radius: 60px;
border: 2px solid white;
color: white;
font-weight: bold;
font-size: 1.4rem;
text-shadow: 2px 2px 0 #1a3f5f;
box-shadow: 0 5px 0 #0b2a3a;
letter-spacing: 1px;
display: flex;
gap: 30px;
}
.stats span {
display: flex;
align-items: center;
gap: 8px;
}
.stats span::before {
content: "⚔️";
filter: drop-shadow(0 2px 2px black);
}
.stats span:first-child::before { content: "🏰"; }
.stats span:last-child::before { content: "❤️"; font-size: 1.4rem; }
.weapons {
display: flex;
gap: 12px;
background: rgba(0, 20, 40, 0.6);
backdrop-filter: blur(6px);
padding: 8px 15px;
border-radius: 60px;
border: 2px solid #ffd78c;
}
.weapon-btn {
width: 64px;
height: 64px;
border-radius: 40px;
background: radial-gradient(circle at 30% 20%, #f0f4fa, #b0c5de);
border: 3px solid #3f2600;
font-size: 1rem;
font-weight: 800;
color: #1f2c3c;
cursor: pointer;
box-shadow: 0 6px 0 #5f3f1a, 0 6px 8px black;
transition: 0.05s linear;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
line-height: 1.2;
text-transform: uppercase;
padding: 0 4px;
}
.weapon-btn.active {
background: radial-gradient(circle at 30% 20%, #ffe99c, #ddb560);
border-color: #ffc107;
transform: translateY(3px);
box-shadow: 0 3px 0 #5f3f1a, 0 6px 10px gold;
}
.weapon-btn:active {
transform: translateY(5px);
box-shadow: 0 2px 0 #3f2e1a, 0 6px 8px black;
}
/* 画布区域 */
.canvas-container {
position: relative;
display: flex;
justify-content: center;
margin: 10px 0 15px;
}
#gameCanvas {
width: 100%;
max-width: 800px;
aspect-ratio: 800 / 400;
display: block;
border-radius: 40px;
border: 5px solid white;
box-shadow: 0 12px 0 #2b5f7a, 0 15px 25px black;
background: transparent; /* 背景在canvas内绘制渐变 */
cursor: crosshair;
touch-action: none; /* 防止触摸时滚动/缩放 */
}
/* 游戏结束浮层 */
.gameover-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background: rgba(0, 10, 25, 0.8);
backdrop-filter: blur(3px);
border-radius: 40px;
z-index: 20;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease;
}
.gameover-overlay.show {
opacity: 1;
pointer-events: all;
}
.gameover-card {
background: linear-gradient(145deg, #c02b2b, #8a0c0c);
padding: 30px 45px;
border-radius: 80px;
border: 5px solid #ffbb33;
color: white;
font-weight: 900;
font-size: 3rem;
text-shadow: 4px 4px 0 #4a0000;
box-shadow: 0 15px 0 #4f0000;
margin-bottom: 30px;
}
.restart-btn {
background: #f9e57c;
border: none;
padding: 20px 50px;
border-radius: 80px;
font-size: 2.2rem;
font-weight: bold;
color: #1f3a4b;
border-bottom: 8px solid #b07c1e;
cursor: pointer;
transition: 0.05s linear;
box-shadow: 0 10px 20px black;
}
.restart-btn:active {
transform: translateY(6px);
border-bottom-width: 2px;
}
/* 底部三兄弟:圆形按钮 */
.action-bar {
display: flex;
justify-content: center;
gap: 40px;
margin-top: 20px;
flex-wrap: wrap;
}
.action-btn {
width: 90px;
height: 90px;
border-radius: 999px;
background: radial-gradient(circle at 30% 20%, #faf6e7, #c5b58c);
border: 5px solid #191e28;
font-size: 1.8rem;
font-weight: 800;
color: #1e2b38;
text-transform: uppercase;
cursor: pointer;
box-shadow: 0 12px 0 #0f1a24, 0 10px 20px black;
transition: 0.04s linear;
display: flex;
align-items: center;
justify-content: center;
line-height: 1;
user-select: none;
touch-action: manipulation;
}
.action-btn:active {
transform: translateY(8px);
box-shadow: 0 4px 0 #0f1a24, 0 8px 12px black;
}
.action-btn span {
font-size: 2.6rem;
filter: drop-shadow(2px 4px 0 #2b3b4b);
}
/* 简单提示 */
.tip {
text-align: center;
color: white;
text-shadow: 2px 2px 0 #0b3550;
font-weight: bold;
margin-top: 12px;
font-size: 1.1rem;
}
</style>
</head>
<body>
<div class="game-wrapper">
<!-- 顶部栏:左状态 / 右武器 -->
<div class="top-bar">
<div class="stats">
<span id="levelDisplay">关卡 1</span>
<span id="hpDisplay">❤️ 5</span>
</div>
<div class="weapons" id="weaponGroup">
<button class="weapon-btn active" data-weapon="pistol">🔫<br>手枪</button>
<button class="weapon-btn" data-weapon="smg">⚡<br>冲锋</button>
<button class="weapon-btn" data-weapon="rifle">🎯<br>步枪</button>
<button class="weapon-btn" data-weapon="shotgun">💥<br>霰弹</button>
</div>
</div>
<!-- 画布容器 (包含游戏结束浮层) -->
<div class="canvas-container">
<canvas id="gameCanvas" width="800" height="400"></canvas>
<div class="gameover-overlay" id="gameoverOverlay">
<div class="gameover-card">GAME OVER</div>
<button class="restart-btn" id="restartButton">↺ 重新开始</button>
</div>
</div>
<!-- 底部移动按钮 -->
<div class="action-bar">
<button class="action-btn" id="moveLeft"><span>◀</span><br>左移</button>
<button class="action-btn" id="jumpBtn"><span>▲</span><br>跳跃</button>
<button class="action-btn" id="moveRight"><span>▶</span><br>右移</button>
</div>
<div class="tip">👉 点击画布开火 · 武器实时切换 · 左/右/跳跃</div>
</div>
<script>
(function() {
// ---------- 画布与上下文 ----------
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const w = 800, h = 400;
canvas.width = w; canvas.height = h;
// ---------- 游戏全局状态 ----------
let gameActive = true; // 游戏是否运行(非结束)
let gameOverFlag = false; // 辅助显示结束层
// 玩家属性
let player = {
x: 150, // 圆心x
y: 320, // 圆心y (动态)
vy: 0,
radius: 16, // 碰撞半径
hp: 5,
maxHp: 5,
invincible: 0, // 无敌帧计数
grounded: false
};
// 物理常量
const GRAVITY = 0.6;
const GROUND_Y = 355; // 地面高度 (圆心接触地面时 y = GROUND_Y - player.radius = 339 但我们动态调整)
// 实际地面线位置 (脚落地) 用于绘制
const GROUND_LINE = 365; // 绘制地面的y坐标
// 移动控制 flags
let leftPressed = false;
let rightPressed = false;
const MOVE_SPEED = 4.5;
// 关卡
let level = 1;
// 敌人数组
let enemies = [];
// 子弹数组
let bullets = [];
// 武器系统
let currentWeapon = 'pistol'; // 默认手枪
const weaponCooldowns = {
pistol: 200, // 毫秒
smg: 80,
rifle: 250,
shotgun: 450
};
let lastShootTime = 0;
// 敌人参数
const ENEMY_RADIUS = 14;
let baseEnemySpeed = 1.2;
// ---------- 辅助函数 ----------
function updateUI() {
document.getElementById('levelDisplay').innerHTML = `🏰 关卡 ${level}`;
document.getElementById('hpDisplay').innerHTML = `❤️ ${player.hp}`;
}
// 生成敌兵 (根据关卡)
function spawnEnemiesForLevel() {
enemies = [];
const count = 3 + level * 2; // 5,7,9...
for (let i = 0; i < count; i++) {
let x = w + 40 + Math.random() * 200; // 从右边出现
let y = GROUND_LINE - 45 - Math.random() * 40; // 离地高度不同
// 限制最低位置
if (y > GROUND_LINE - ENEMY_RADIUS - 5) y = GROUND_LINE - ENEMY_RADIUS - 10;
if (y < 50) y = 70;
enemies.push({
x: x,
y: y,
radius: ENEMY_RADIUS,
speed: baseEnemySpeed + level * 0.25,
hp: 1 // 所有敌人一枪死(但步枪伤害2也是秒)
});
}
}
// 重置整个游戏
function resetGame() {
gameActive = true;
gameOverFlag = false;
document.getElementById('gameoverOverlay').classList.remove('show');
player = {
x: 150,
y: 320,
vy: 0,
radius: 16,
hp: 5,
maxHp: 5,
invincible: 0,
grounded: false
};
level = 1;
bullets = [];
enemies = [];
leftPressed = false;
rightPressed = false;
spawnEnemiesForLevel();
updateUI();
}
// 检查游戏结束
function checkGameOver() {
if (player.hp <= 0 && gameActive) {
gameActive = false;
gameOverFlag = true;
document.getElementById('gameoverOverlay').classList.add('show');
}
}
// 子弹发射 (canvas点击)
function shootBullet(targetX, targetY) {
if (!gameActive) return; // 游戏结束不能开火
const now = performance.now();
const cd = weaponCooldowns[currentWeapon];
if (now - lastShootTime < cd) return;
lastShootTime = now;
// 发射起始点:玩家圆心
const fromX = player.x;
const fromY = player.y;
// 计算方向向量
let dx = targetX - fromX;
let dy = targetY - fromY;
const len = Math.sqrt(dx*dx + dy*dy);
if (len < 0.001) return; // 避免零向量
const normDx = dx / len;
const normDy = dy / len;
// 子弹速度基数
let baseSpeed = 9;
let bulletDamage = 1;
let bulletRadius = 5;
// 根据武器修改属性
switch (currentWeapon) {
case 'pistol':
baseSpeed = 11;
bulletRadius = 5;
bulletDamage = 1;
break;
case 'smg':
baseSpeed = 13;
bulletRadius = 4;
bulletDamage = 1;
break;
case 'rifle':
baseSpeed = 15;
bulletRadius = 6;
bulletDamage = 2; // 可秒杀任何敌人(hp=1但无所谓)
break;
case 'shotgun':
// 霰弹枪: 5颗散射,角度偏移 ±0.2 rad
for (let i = 0; i < 5; i++) {
let angleVariation = (i - 2) * 0.15; // -0.3, -0.15, 0, 0.15, 0.3
let angle = Math.atan2(dy, dx) + angleVariation;
let scatterDx = Math.cos(angle);
let scatterDy = Math.sin(angle);
bullets.push({
x: fromX,
y: fromY,
vx: scatterDx * (baseSpeed + 2),
vy: scatterDy * (baseSpeed + 2),
radius: 5,
damage: 1,
active: true
});
}
return; // 直接返回,避免下面再添加单发
default: break;
}
// 手枪/冲锋/步枪 单发
bullets.push({
x: fromX,
y: fromY,
vx: normDx * baseSpeed,
vy: normDy * baseSpeed,
radius: bulletRadius,
damage: bulletDamage,
active: true
});
}
// ---------- 更新逻辑 ----------
function update() {
if (!gameActive) return;
// 玩家移动 (左右)
if (leftPressed) player.x -= MOVE_SPEED;
if (rightPressed) player.x += MOVE_SPEED;
player.x = Math.max(40 + player.radius, Math.min(w - 40 - player.radius, player.x));
// 重力与跳跃
player.vy += GRAVITY;
player.y += player.vy;
// 地面碰撞 (圆心y <= GROUND_Y - player.radius 时着地)
const groundCollisionY = GROUND_Y - player.radius;
if (player.y > groundCollisionY) {
player.y = groundCollisionY;
player.vy = 0;
player.grounded = true;
} else {
player.grounded = false;
}
// 玩家天花板限制 (防止飞出画面)
if (player.y - player.radius < 20) {
player.y = 20 + player.radius;
if (player.vy < 0) player.vy = 0;
}
// 无敌帧递减
if (player.invincible > 0) player.invincible--;
// 敌人移动 (向左)
for (let i = enemies.length - 1; i >= 0; i--) {
const e = enemies[i];
e.x -= e.speed;
// 移除离开左边的敌人 (消失但不扣血)
if (e.x + e.radius < -30) {
enemies.splice(i, 1);
continue;
}
// 玩家与敌人碰撞 (并且玩家不是无敌)
const dx = e.x - player.x;
const dy = e.y - player.y;
const dist = Math.sqrt(dx*dx + dy*dy);
if (dist < e.radius + player.radius && player.invincible <= 0) {
// 玩家扣血,敌人消失,短暂无敌
player.hp = Math.max(0, player.hp - 1);
player.invincible = 45; // 约0.75秒 (60fps)
enemies.splice(i, 1);
updateUI();
checkGameOver();
if (!gameActive) return;
}
}
// 子弹移动 & 碰撞
for (let i = bullets.length - 1; i >= 0; i--) {
const b = bullets[i];
b.x += b.vx;
b.y += b.vy;
// 边界移除
if (b.x + b.radius < 0 || b.x - b.radius > w || b.y + b.radius < 0 || b.y - b.radius > h) {
bullets.splice(i, 1);
continue;
}
let bulletUsed = false;
for (let j = enemies.length - 1; j >= 0; j--) {
const e = enemies[j];
const dx = b.x - e.x;
const dy = b.y - e.y;
const dist = Math.sqrt(dx*dx + dy*dy);
if (dist < b.radius + e.radius) {
// 子弹命中
e.hp -= b.damage;
bulletUsed = true;
if (e.hp <= 0) {
enemies.splice(j, 1);
}
break; // 子弹消失,跳出内循环
}
}
if (bulletUsed) {
bullets.splice(i, 1);
}
}
// 如果敌人全灭,进入下一关
if (enemies.length === 0 && gameActive) {
level++;
spawnEnemiesForLevel();
// 玩家位置重置到偏左,加一点奖励血量?
player.x = 150;
player.y = GROUND_Y - player.radius; // 直接落地
player.vy = 0;
// 不回复血量,但可以给一点点奖励(可选)
updateUI();
}
// 再次检查游戏结束 (hp为0时)
checkGameOver();
}
// ---------- 绘制 ----------
function draw() {
ctx.clearRect(0, 0, w, h);
// 背景渐变 (从亮蓝到浅蓝)
const gradient = ctx.createLinearGradient(0, 0, 0, h);
gradient.addColorStop(0, '#8ac4ff');
gradient.addColorStop(1, '#c2e3ff');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, w, h);
// 绘制阴影地面
ctx.fillStyle = '#3d7a3d';
ctx.shadowColor = '#1e3b1e';
ctx.shadowBlur = 10;
ctx.fillRect(0, GROUND_LINE-5, w, 20);
ctx.shadowBlur = 0;
ctx.shadowColor = 'transparent';
// 绘制所有敌人 (暗红火柴人风格,简单圆代替)
enemies.forEach(e => {
// 敌人画成黑色火材头 + 眼睛
ctx.beginPath();
ctx.arc(e.x, e.y, e.radius, 0, 2*Math.PI);
ctx.fillStyle = '#3a281f';
ctx.shadowBlur = 12;
ctx.shadowColor = '#1a0f0a';
ctx.fill();
ctx.strokeStyle = '#e06830';
ctx.lineWidth = 2;
ctx.stroke();
// 眼睛
ctx.beginPath();
ctx.arc(e.x-4, e.y-4, 3, 0, 2*Math.PI);
ctx.fillStyle = 'white';
ctx.fill();
ctx.beginPath();
ctx.arc(e.x+4, e.y-4, 3, 0, 2*Math.PI);
ctx.fill();
ctx.fillStyle = '#200';
ctx.beginPath();
ctx.arc(e.x-4, e.y-5, 1.5, 0, 2*Math.PI);
ctx.fill();
ctx.beginPath();
ctx.arc(e.x+4, e.y-5, 1.5, 0, 2*Math.PI);
ctx.fill();
});
// 绘制子弹
bullets.forEach(b => {
ctx.beginPath();
ctx.arc(b.x, b.y, b.radius, 0, 2*Math.PI);
ctx.fillStyle = '#ffd966';
ctx.shadowBlur = 12;
ctx.shadowColor = '#ffaa33';
ctx.fill();
});
// 绘制玩家火柴人 (带无敌闪烁)
if (player.invincible > 0 && (Math.floor(Date.now() / 150) % 2 === 0)) {
ctx.globalAlpha = 0.5; // 闪烁
} else {
ctx.globalAlpha = 1.0;
}
// 火柴人身体基于player圆心
const headY = player.y - 22;
const neckY = player.y - 12;
const bodyEndY = player.y + 8;
// 头
ctx.shadowBlur = 10;
ctx.shadowColor = '#2c5778';
ctx.beginPath();
ctx.arc(player.x, headY, 12, 0, 2*Math.PI);
ctx.fillStyle = '#ffe0a3';
ctx.fill();
ctx.strokeStyle = '#765c3a';
ctx.lineWidth = 2;
ctx.stroke();
// 眼睛和微笑
ctx.fillStyle = '#232323';
ctx.beginPath();
ctx.arc(player.x-4, headY-2, 2, 0, 2*Math.PI);
ctx.fill();
ctx.beginPath();
ctx.arc(player.x+4, headY-2, 2, 0, 2*Math.PI);
ctx.fill();
ctx.beginPath();
ctx.arc(player.x, headY+3, 3, 0.1, Math.PI-0.1);
ctx.lineWidth = 2;
ctx.strokeStyle = '#a0522d';
ctx.stroke();
// 身体 (线)
ctx.beginPath();
ctx.moveTo(player.x, headY+6);
ctx.lineTo(player.x, bodyEndY);
ctx.lineWidth = 4;
ctx.strokeStyle = '#1e4f7a';
ctx.stroke();
// 手臂
ctx.beginPath();
ctx.moveTo(player.x-10, headY+2);
ctx.lineTo(player.x, headY+12);
ctx.lineTo(player.x+10, headY+2);
ctx.strokeStyle = '#1e4f7a';
ctx.stroke();
// 腿
ctx.beginPath();
ctx.moveTo(player.x-8, bodyEndY+6);
ctx.lineTo(player.x, bodyEndY);
ctx.lineTo(player.x+8, bodyEndY+6);
ctx.stroke();
// 武器指示 (手持小点)
ctx.beginPath();
ctx.arc(player.x+13, headY-2, 6, 0, 2*Math.PI);
ctx.fillStyle = '#c9a03d';
ctx.shadowBlur = 15;
ctx.fill();
ctx.globalAlpha = 1.0;
ctx.shadowBlur = 0;
ctx.shadowColor = 'transparent';
}
// 游戏循环
function gameLoop() {
if (gameActive) {
update();
}
draw();
requestAnimationFrame(gameLoop);
}
// ---------- 事件绑定 ----------
// 武器切换
document.querySelectorAll('.weapon-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
document.querySelectorAll('.weapon-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
currentWeapon = btn.dataset.weapon;
});
});
// 左移 按下/释放
const leftBtn = document.getElementById('moveLeft');
leftBtn.addEventListener('mousedown', (e) => { e.preventDefault(); leftPressed = true; });
leftBtn.addEventListener('mouseup', () => { leftPressed = false; });
leftBtn.addEventListener('mouseleave', () => { leftPressed = false; });
leftBtn.addEventListener('touchstart', (e) => { e.preventDefault(); leftPressed = true; });
leftBtn.addEventListener('touchend', (e) => { e.preventDefault(); leftPressed = false; });
leftBtn.addEventListener('touchcancel', (e) => { e.preventDefault(); leftPressed = false; });
// 右移
const rightBtn = document.getElementById('moveRight');
rightBtn.addEventListener('mousedown', (e) => { e.preventDefault(); rightPressed = true; });
rightBtn.addEventListener('mouseup', () => { rightPressed = false; });
rightBtn.addEventListener('mouseleave', () => { rightPressed = false; });
rightBtn.addEventListener('touchstart', (e) => { e.preventDefault(); rightPressed = true; });
rightBtn.addEventListener('touchend', (e) => { e.preventDefault(); rightPressed = false; });
rightBtn.addEventListener('touchcancel', (e) => { e.preventDefault(); rightPressed = false; });
// 跳跃
document.getElementById('jumpBtn').addEventListener('mousedown', (e) => {
e.preventDefault();
if (player.grounded && gameActive) {
player.vy = -11;
player.grounded = false;
}
});
document.getElementById('jumpBtn').addEventListener('touchstart', (e) => {
e.preventDefault();
if (player.grounded && gameActive) {
player.vy = -11;
player.grounded = false;
}
});
// 画布开火 (点击)
canvas.addEventListener('click', (e) => {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const canvasX = (e.clientX - rect.left) * scaleX;
const canvasY = (e.clientY - rect.top) * scaleY;
shootBullet(canvasX, canvasY);
});
canvas.addEventListener('touchstart', (e) => {
e.preventDefault(); // 防止页面缩放
const rect = canvas.getBoundingClientRect();
const touch = e.touches[0];
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const canvasX = (touch.clientX - rect.left) * scaleX;
const canvasY = (touch.clientY - rect.top) * scaleY;
shootBullet(canvasX, canvasY);
});
// 重新开始按钮
document.getElementById('restartButton').addEventListener('click', () => {
resetGame();
});
// 同时也允许点击浮层外不操作
// 初始化
resetGame();
gameLoop();
// 额外的键盘控制 (可选,方便调试)
window.addEventListener('keydown', (e) => {
if (e.key === 'a' || e.key === 'A') { leftPressed = true; e.preventDefault(); }
if (e.key === 'd' || e.key === 'D') { rightPressed = true; e.preventDefault(); }
if (e.key === ' ' || e.key === 'Space') {
e.preventDefault();
if (player.grounded && gameActive) { player.vy = -11; player.grounded = false; }
}
});
window.addEventListener('keyup', (e) => {
if (e.key === 'a' || e.key === 'A') { leftPressed = false; }
if (e.key === 'd' || e.key === 'D') { rightPressed = false; }
});
})();
</script>
</body>
</html>