分享丨火柴人
88
6 小时前
6 小时前
发布于 中国

<!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>

评论 (1)