
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">⚓ 打海盗 · 小游戏</title>
<style>
* {
box-sizing: border-box;
user-select: none;
}
body {
margin: 0;
min-height: 100vh;
background: linear-gradient(145deg, #0a1f2e 0%, #1b3a4b 100%);
display: flex;
align-items: center;
justify-content: center;
font-family: 'Segoe UI', Roboto, system-ui, sans-serif;
}
.game-wrapper {
background: #1e2f3a;
border-radius: 2rem;
padding: 1.2rem 1.2rem 1.5rem;
box-shadow: 0 20px 30px rgba(0, 0, 0, 0.6), inset 0 0 0 2px #5d8b9c;
}
canvas {
display: block;
width: 800px;
height: 500px;
border-radius: 1.2rem;
box-shadow: inset 0 0 30px #00000055, 0 10px 20px #00000080;
cursor: crosshair;
background: #0e2632;
background-image: radial-gradient(circle at 20% 20%, #2e5e6e33 0%, transparent 30%),
radial-gradient(circle at 90% 70%, #4e8a9e33 0%, transparent 40%);
}
.ui-panel {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 1rem;
color: #e9f2f0;
text-shadow: 0 2px 3px black;
font-weight: 600;
padding: 0 0.5rem;
}
.stats {
display: flex;
gap: 2.2rem;
background: #0d1f26;
padding: 0.6rem 1.8rem;
border-radius: 3rem;
border: 1px solid #3e6b7c;
box-shadow: inset 0 0 8px #00000070;
}
.stat-item {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 1.4rem;
}
.stat-icon {
font-size: 1.6rem;
filter: drop-shadow(0 3px 3px black);
}
button {
background: #dc6e3b;
border: none;
color: #fff4e6;
font-weight: bold;
font-size: 1.3rem;
padding: 0.6rem 2.2rem;
border-radius: 3rem;
cursor: pointer;
box-shadow: 0 7px 0 #7a3418, 0 10px 18px black;
transition: 0.05s linear;
letter-spacing: 1px;
display: flex;
align-items: center;
gap: 0.6rem;
border: 1px solid #ffb085;
text-shadow: 0 2px 0 #632f0e;
}
button:active {
transform: translateY(5px);
box-shadow: 0 2px 0 #7a3418, 0 8px 18px black;
}
button:disabled {
opacity: 0.6;
transform: translateY(4px);
box-shadow: 0 3px 0 #7a3418, 0 8px 18px black;
pointer-events: none;
}
.hp-bar {
display: flex;
align-items: center;
gap: 0.8rem;
background: #0d1f26;
padding: 0.5rem 1.2rem;
border-radius: 3rem;
border: 1px solid #b34e4e;
box-shadow: inset 0 0 8px #00000070;
}
.hp-icon {
font-size: 1.8rem;
}
.hp-text {
font-size: 1.4rem;
min-width: 3.2rem;
text-align: center;
color: #fbc2c2;
font-weight: 800;
}
.hp-bar-fill {
width: 120px;
height: 18px;
background: #2e1a1a;
border-radius: 20px;
overflow: hidden;
border: 1px solid #a55555;
box-shadow: inset 0 2px 5px black;
}
.hp-bar-inner {
height: 100%;
width: 100%;
background: linear-gradient(90deg, #e03a3a, #b81f1f);
border-radius: 20px;
transition: width 0.15s ease;
box-shadow: 0 0 12px #ff6565;
}
</style>
</head>
<body>
<div class="game-wrapper">
<canvas id="gameCanvas" width="800" height="500"></canvas>
<div class="ui-panel">
<div class="stats">
<div class="stat-item">
<span class="stat-icon">☠️</span>
<span id="scoreDisplay">0</span>
</div>
<div class="stat-item">
<span class="stat-icon">🔫</span>
<span id="shotsDisplay">0</span>
</div>
</div>
<div class="hp-bar">
<span class="hp-icon">❤️</span>
<div class="hp-bar-fill">
<div id="hpBarInner" class="hp-bar-inner" style="width: 100%"></div>
</div>
<span id="hpText" class="hp-text">100</span>
</div>
<button id="restartBtn">
<span>⚓</span> 重新出海
</button>
</div>
</div>
<script>
(function() {
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const scoreDisplay = document.getElementById('scoreDisplay');
const shotsDisplay = document.getElementById('shotsDisplay');
const hpBarInner = document.getElementById('hpBarInner');
const hpText = document.getElementById('hpText');
const restartBtn = document.getElementById('restartBtn');
// ----- 游戏配置 -----
const CANVAS_W = 800, CANVAS_H = 500;
const PLAYER_RADIUS = 22; // 炮台/船的大小
const PIRATE_RADIUS = 26; // 海盗半径
const BULLET_RADIUS = 5;
const BULLET_SPEED = 7.8;
const PIRATE_SPEED = 1.2;
const MAX_PIRATES = 8; // 同时最多海盗数量
const PLAYER_MAX_HP = 100;
const SPAWN_INTERVAL_FRAMES = 50; // 每50帧生成一个海盗 (约0.8~1秒60fps)
const DAMAGE_PER_PIRATE = 10; // 每个海盗撞到玩家造成的伤害
// ----- 游戏状态 -----
let player = {
x: CANVAS_W / 2,
y: CANVAS_H / 2,
hp: PLAYER_MAX_HP,
maxHp: PLAYER_MAX_HP,
radius: PLAYER_RADIUS,
invincibleFrames: 0 // 受伤后的短暂无敌闪烁,防止瞬间多次扣血
};
let pirates = []; // 海盗对象数组
let bullets = []; // 子弹对象数组
let score = 0;
let shotsFired = 0; // 总开火次数 (统计用)
let frameCounter = 0; // 用于生成计时
let gameOver = false;
let gameWin = false; // 无胜利条件,仅用于表示游戏结束
// 鼠标/触摸位置 (用于炮口指向)
let aimX = CANVAS_W / 2;
let aimY = CANVAS_H / 2;
let mouseInside = false; // 是否在canvas内,用于绘制准星
// 无敌闪烁计时
const INVINCIBLE_DURATION = 25; // 帧数
// ----- 辅助函数 -----
function clamp(value, min, max) {
return Math.min(max, Math.max(min, value));
}
// 重置游戏 (保留鼠标位置不变)
function resetGame() {
gameOver = false;
gameWin = false;
player = {
x: CANVAS_W / 2,
y: CANVAS_H / 2,
hp: PLAYER_MAX_HP,
maxHp: PLAYER_MAX_HP,
radius: PLAYER_RADIUS,
invincibleFrames: 0
};
pirates = [];
bullets = [];
score = 0;
shotsFired = 0;
frameCounter = 0;
// 初始生成2个海盗,让游戏不那麼空
for (let i = 0; i < 2; i++) {
spawnPirate();
}
updateUI();
}
// 生成一个海盗 (位置避开玩家太近)
function spawnPirate() {
if (pirates.length >= MAX_PIRATES) return;
let spawnX, spawnY;
let maxAttempts = 100;
let attempts = 0;
const minDistFromPlayer = 180; // 离玩家至少180像素
do {
// 主要在画布边缘附近生成,看起来像从外面驶入
const edge = Math.floor(Math.random() * 4);
if (edge === 0) { // 上边缘
spawnX = Math.random() * CANVAS_W;
spawnY = -PIRATE_RADIUS * 2;
} else if (edge === 1) { // 下边缘
spawnX = Math.random() * CANVAS_W;
spawnY = CANVAS_H + PIRATE_RADIUS * 2;
} else if (edge === 2) { // 左边缘
spawnX = -PIRATE_RADIUS * 2;
spawnY = Math.random() * CANVAS_H;
} else { // 右边缘
spawnX = CANVAS_W + PIRATE_RADIUS * 2;
spawnY = Math.random() * CANVAS_H;
}
attempts++;
// 如果离玩家太近,再试一次 (但减少尝试次数避免死循环)
} while (Math.hypot(spawnX - player.x, spawnY - player.y) < minDistFromPlayer && attempts < maxAttempts);
// 如果尝试太多次还没找到位置,就直接在边缘随机位置
if (attempts >= maxAttempts) {
if (Math.random() < 0.5) {
spawnX = Math.random() < 0.5 ? -PIRATE_RADIUS * 2 : CANVAS_W + PIRATE_RADIUS * 2;
spawnY = Math.random() * CANVAS_H;
} else {
spawnX = Math.random() * CANVAS_W;
spawnY = Math.random() < 0.5 ? -PIRATE_RADIUS * 2 : CANVAS_H + PIRATE_RADIUS * 2;
}
}
// 随机速度方向朝向玩家 (稍后每帧更新朝向玩家,但生成时先给个方向)
const angleToPlayer = Math.atan2(player.y - spawnY, player.x - spawnX);
// 海盗不直接朝向玩家,我们使用追踪逻辑:速度方向会在update里计算。
pirates.push({
x: spawnX,
y: spawnY,
radius: PIRATE_RADIUS,
angle: 0, // 用于绘制方向
speed: PIRATE_SPEED,
// 添加一点随机偏移,让不同海盗速度稍微不同
speedFactor: 0.85 + Math.random() * 0.5,
});
}
// 开火: 从玩家位置向瞄准点发射一颗子弹
function fireBullet(targetX, targetY) {
if (gameOver) return;
// 计算方向
let dx = targetX - player.x;
let dy = targetY - player.y;
const length = Math.hypot(dx, dy);
if (length < 0.001) return; // 零向量
const normDx = dx / length;
const normDy = dy / length;
// 子弹从玩家边缘出发 (避免卡在身体里)
const startX = player.x + normDx * (player.radius + 2);
const startY = player.y + normDy * (player.radius + 2);
bullets.push({
x: startX,
y: startY,
vx: normDx * BULLET_SPEED,
vy: normDy * BULLET_SPEED,
radius: BULLET_RADIUS,
life: 1, // 存活标记
});
shotsFired++;
updateUI();
}
// 更新UI (分数、血量、射击数)
function updateUI() {
scoreDisplay.textContent = score;
shotsDisplay.textContent = shotsFired;
const hpPercent = Math.max(0, (player.hp / player.maxHp) * 100);
hpBarInner.style.width = `${hpPercent}%`;
hpText.textContent = Math.max(0, player.hp);
// 根据血量改变血条颜色
if (hpPercent > 60) {
hpBarInner.style.background = 'linear-gradient(90deg, #e03a3a, #b81f1f)';
} else if (hpPercent > 30) {
hpBarInner.style.background = 'linear-gradient(90deg, #e08a3a, #b85f1f)';
} else {
hpBarInner.style.background = 'linear-gradient(90deg, #a51f1f, #6e1010)';
}
}
// ----- 玩家受伤处理 (带无敌) -----
function damagePlayer(amount) {
if (gameOver) return;
if (player.invincibleFrames > 0) return; // 无敌状态
player.hp = Math.max(0, player.hp - amount);
player.invincibleFrames = INVINCIBLE_DURATION;
updateUI();
if (player.hp <= 0) {
gameOver = true;
player.hp = 0;
updateUI();
}
}
// ----- 更新逻辑 (每帧) -----
function update() {
if (gameOver) return;
// 1. 减少无敌帧
if (player.invincibleFrames > 0) {
player.invincibleFrames--;
}
// 2. 生成海盗 (使用frameCounter)
frameCounter++;
if (frameCounter % SPAWN_INTERVAL_FRAMES === 0) {
spawnPirate();
}
// 3. 海盗移动 (向玩家靠近)
for (let i = pirates.length - 1; i >= 0; i--) {
const p = pirates[i];
// 计算指向玩家的方向向量
let dx = player.x - p.x;
let dy = player.y - p.y;
const dist = Math.hypot(dx, dy);
// 如果海盗已经非常接近玩家 (碰撞处理),先标记移除并造成伤害
if (dist < player.radius + p.radius) {
// 碰撞!造成伤害
damagePlayer(DAMAGE_PER_PIRATE);
// 移除该海盗 (爆炸效果可以不加,但为了简单直接移除)
pirates.splice(i, 1);
continue; // 跳过后续移动
}
// 否则向玩家移动
if (dist > 0.5) {
const moveX = (dx / dist) * p.speed * p.speedFactor;
const moveY = (dy / dist) * p.speed * p.speedFactor;
p.x += moveX;
p.y += moveY;
// 更新海盗的角度(用于绘制朝向)
p.angle = Math.atan2(dy, dx);
}
// 边界限制?海盗从外面进来,后面如果跑到外面去也保留吧,但为了美观稍微限制在画布内?不限制反而自然。
// 不限制它们,如果它们跑出去太远,但玩家在中心,一般不会。
}
// 4. 子弹移动 & 碰撞检测
for (let i = bullets.length - 1; i >= 0; i--) {
const b = bullets[i];
b.x += b.vx;
b.y += b.vy;
// 如果子弹超出画布太远则移除
if (b.x < -50 || b.x > CANVAS_W + 50 || b.y < -50 || b.y > CANVAS_H + 50) {
bullets.splice(i, 1);
continue;
}
let bulletUsed = false;
// 检查是否击中海盗
for (let j = pirates.length - 1; j >= 0; j--) {
const p = pirates[j];
const distToPirate = Math.hypot(b.x - p.x, b.y - p.y);
if (distToPirate < b.radius + p.radius) {
// 击中!
pirates.splice(j, 1);
bulletUsed = true;
score += 10; // 每击中一个海盗得10分
updateUI();
break; // 一颗子弹只打中一个海盗
}
}
if (bulletUsed) {
bullets.splice(i, 1);
}
}
// 5. 如果玩家生命值归零则游戏结束 (已处理)
if (player.hp <= 0) {
gameOver = true;
}
// 如果海盗数量为0且游戏未结束,为了不太无聊,可以补充生成,但生成计时器会做;初始和死亡时已有生成
// 不过如果场上没有海盗了,稍微加快生成?但保持自然,计时器继续工作。
}
// ----- 绘制所有内容 -----
function draw() {
ctx.clearRect(0, 0, CANVAS_W, CANVAS_H);
// 绘制海洋网格/波纹 (氛围)
ctx.strokeStyle = '#5f9eae30';
ctx.lineWidth = 1.5;
for (let i = 0; i < CANVAS_W; i += 45) {
ctx.beginPath();
ctx.moveTo(i, 0);
ctx.lineTo(i + 15, CANVAS_H);
ctx.strokeStyle = '#3f7f8f20';
ctx.stroke();
}
// 水位光晕
ctx.fillStyle = '#163a4660';
ctx.beginPath();
ctx.arc(150, 80, 120, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(650, 400, 150, 0, Math.PI * 2);
ctx.fillStyle = '#1a405060';
ctx.fill();
// 1. 绘制子弹 (发光的炮弹)
for (const b of bullets) {
ctx.shadowColor = '#f0e68c';
ctx.shadowBlur = 18;
ctx.beginPath();
ctx.arc(b.x, b.y, b.radius, 0, Math.PI * 2);
ctx.fillStyle = '#ffd966';
ctx.fill();
// 内部亮色
ctx.shadowBlur = 10;
ctx.beginPath();
ctx.arc(b.x - 1, b.y - 1, b.radius * 0.6, 0, Math.PI * 2);
ctx.fillStyle = '#fff8e7';
ctx.fill();
}
ctx.shadowBlur = 0; // 重置阴影
// 2. 绘制海盗 (骷髅海盗船/海盗旗形象)
for (const p of pirates) {
// 海盗船体 (圆形为主体)
ctx.shadowColor = '#000000';
ctx.shadowBlur = 12;
// 船身暗红色
ctx.beginPath();
ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
ctx.fillStyle = '#4a2e1e';
ctx.fill();
// 甲板
ctx.beginPath();
ctx.arc(p.x, p.y, p.radius * 0.8, 0, Math.PI * 2);
ctx.fillStyle = '#6b4a32';
ctx.fill();
// 海盗旗 (骷髅标志)
ctx.shadowBlur = 8;
// 旗杆
ctx.beginPath();
ctx.moveTo(p.x + 10, p.y - 20);
ctx.lineTo(p.x + 28, p.y - 32);
ctx.lineWidth = 4;
ctx.strokeStyle = '#c0a080';
ctx.stroke();
// 旗子 (黑底)
ctx.beginPath();
ctx.moveTo(p.x + 28, p.y - 32);
ctx.lineTo(p.x + 42, p.y - 28);
ctx.lineTo(p.x + 28, p.y - 14);
ctx.closePath();
ctx.fillStyle = '#1a1a1a';
ctx.fill();
// 骷髅图案简笔
ctx.beginPath();
ctx.arc(p.x + 32, p.y - 24, 4, 0, Math.PI * 2);
ctx.fillStyle = '#eee';
ctx.fill();
ctx.beginPath();
ctx.arc(p.x + 30, p.y - 25, 1.5, 0, Math.PI * 2);
ctx.fillStyle = '#000';
ctx.fill();
ctx.beginPath();
ctx.arc(p.x + 34, p.y - 25, 1.5, 0, Math.PI * 2);
ctx.fillStyle = '#000';
ctx.fill();
// 交叉骨头
ctx.beginPath();
ctx.moveTo(p.x + 26, p.y - 28);
ctx.lineTo(p.x + 32, p.y - 22);
ctx.moveTo(p.x + 32, p.y - 28);
ctx.lineTo(p.x + 26, p.y - 22);
ctx.lineWidth = 2;
ctx.strokeStyle = '#ddd';
ctx.stroke();
// 海盗眼睛 (发红光)
ctx.shadowBlur = 15;
ctx.shadowColor = '#ff0000';
ctx.beginPath();
ctx.arc(p.x - 6, p.y - 4, 5, 0, Math.PI * 2);
ctx.fillStyle = '#111';
ctx.fill();
ctx.beginPath();
ctx.arc(p.x + 8, p.y - 4, 5, 0, Math.PI * 2);
ctx.fillStyle = '#111';
ctx.fill();
ctx.beginPath();
ctx.arc(p.x - 7, p.y - 5, 2, 0, Math.PI * 2);
ctx.fillStyle = '#ff4444';
ctx.fill();
ctx.beginPath();
ctx.arc(p.x + 7, p.y - 5, 2, 0, Math.PI * 2);
ctx.fillStyle = '#ff4444';
ctx.fill();
// 眼罩带子
ctx.beginPath();
ctx.moveTo(p.x - 12, p.y - 9);
ctx.lineTo(p.x + 14, p.y - 1);
ctx.lineWidth = 2.5;
ctx.strokeStyle = '#222';
ctx.stroke();
// 嘴
ctx.beginPath();
ctx.arc(p.x, p.y + 8, 8, 0.2, Math.PI - 0.2);
ctx.lineWidth = 2.5;
ctx.strokeStyle = '#a55';
ctx.stroke();
// 重置阴影,避免影响后续
ctx.shadowBlur = 0;
ctx.shadowColor = 'transparent';
}
// 3. 绘制玩家 (炮台/船)
// 无敌闪烁效果:如果invincibleFrames > 0 且为奇数帧则半透明
let drawPlayer = true;
if (player.invincibleFrames > 0 && Math.floor(player.invincibleFrames / 4) % 2 === 0) {
// 闪烁时稍微透明,更明显
ctx.globalAlpha = 0.55;
} else {
ctx.globalAlpha = 1.0;
}
// 玩家船体
ctx.shadowColor = '#000000';
ctx.shadowBlur = 18;
ctx.beginPath();
ctx.arc(player.x, player.y, player.radius, 0, Math.PI * 2);
ctx.fillStyle = '#2a6d7c';
ctx.fill();
// 甲板
ctx.beginPath();
ctx.arc(player.x, player.y, player.radius * 0.8, 0, Math.PI * 2);
ctx.fillStyle = '#4a9eb0';
ctx.fill();
// 炮口 (指向鼠标)
let angle = Math.atan2(aimY - player.y, aimX - player.x);
if (!mouseInside) {
// 如果鼠标不在画布内,炮口默认朝上或者随机角度?就保持上次角度吧(默认会指向最后鼠标位置)
// 但如果还没移动过鼠标,aimX/Y是画布中心,指向中心方向无意义,那就指向右方
if (aimX === CANVAS_W/2 && aimY === CANVAS_H/2) {
angle = 0; // 向右
}
}
const barrelLength = player.radius * 1.2;
const barrelX = player.x + Math.cos(angle) * barrelLength;
const barrelY = player.y + Math.sin(angle) * barrelLength;
// 绘制炮管
ctx.beginPath();
ctx.moveTo(player.x + Math.cos(angle) * (player.radius * 0.6), player.y + Math.sin(angle) * (player.radius * 0.6));
ctx.lineTo(barrelX, barrelY);
ctx.lineWidth = 10;
ctx.strokeStyle = '#3e3e3e';
ctx.stroke();
ctx.lineWidth = 6;
ctx.strokeStyle = '#7a7a7a';
ctx.stroke();
// 炮口亮点
ctx.beginPath();
ctx.arc(barrelX, barrelY, 6, 0, Math.PI * 2);
ctx.fillStyle = '#bba76a';
ctx.shadowBlur = 20;
ctx.fill();
// 玩家眼睛 (可爱一点)
ctx.shadowBlur = 0;
ctx.beginPath();
ctx.arc(player.x - 7, player.y - 8, 4, 0, Math.PI * 2);
ctx.fillStyle = '#ffffff';
ctx.fill();
ctx.beginPath();
ctx.arc(player.x + 7, player.y - 8, 4, 0, Math.PI * 2);
ctx.fillStyle = '#ffffff';
ctx.fill();
ctx.beginPath();
ctx.arc(player.x - 7, player.y - 8, 2, 0, Math.PI * 2);
ctx.fillStyle = '#000000';
ctx.fill();
ctx.beginPath();
ctx.arc(player.x + 7, player.y - 8, 2, 0, Math.PI * 2);
ctx.fillStyle = '#000000';
ctx.fill();
ctx.globalAlpha = 1.0;
ctx.shadowBlur = 0;
// 4. 瞄准点 / 准星 (如果鼠标在canvas内)
if (mouseInside) {
ctx.beginPath();
ctx.arc(aimX, aimY, 10, 0, Math.PI * 2);
ctx.strokeStyle = '#f0f0f0cc';
ctx.lineWidth = 2;
ctx.setLineDash([5, 6]);
ctx.stroke();
ctx.setLineDash([]); // 重置实线
ctx.beginPath();
ctx.moveTo(aimX - 16, aimY);
ctx.lineTo(aimX - 6, aimY);
ctx.moveTo(aimX + 6, aimY);
ctx.lineTo(aimX + 16, aimY);
ctx.moveTo(aimX, aimY - 16);
ctx.lineTo(aimX, aimY - 6);
ctx.moveTo(aimX, aimY + 6);
ctx.lineTo(aimX, aimY + 16);
ctx.strokeStyle = '#ffd966';
ctx.lineWidth = 2.5;
ctx.stroke();
}
// 5. 游戏结束覆盖层
if (gameOver) {
ctx.fillStyle = '#000000aa';
ctx.fillRect(0, 0, CANVAS_W, CANVAS_H);
ctx.font = 'bold 58px "Segoe UI", system-ui, sans-serif';
ctx.fillStyle = '#e03a3a';
ctx.shadowColor = 'black';
ctx.shadowBlur = 20;
ctx.textAlign = 'center';
ctx.fillText('☠️ 沉船了 ☠️', CANVAS_W/2, CANVAS_H/2 - 30);
ctx.font = 'bold 32px "Segoe UI", system-ui, sans-serif';
ctx.fillStyle = '#ffe484';
ctx.fillText('点击"重新出海"', CANVAS_W/2, CANVAS_H/2 + 60);
ctx.shadowBlur = 0;
}
// 显示海盗数量 (debug或者提示)
// 可选,不显示也行。
}
// ----- 动画循环 -----
function gameLoop() {
if (!gameOver) {
update();
}
draw();
requestAnimationFrame(gameLoop);
}
// ----- 事件绑定 -----
function handleMouseMove(e) {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width; // canvas物理像素比
const scaleY = canvas.height / rect.height;
const rawX = (e.clientX - rect.left) * scaleX;
const rawY = (e.clientY - rect.top) * scaleY;
aimX = clamp(rawX, 0, CANVAS_W);
aimY = clamp(rawY, 0, CANVAS_H);
mouseInside = true;
}
function handleMouseLeave() {
mouseInside = false;
// 也可以把瞄准位置重置到玩家前方?但保留最后位置更自然
}
function handleCanvasClick(e) {
if (gameOver) return;
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const clickX = (e.clientX - rect.left) * scaleX;
const clickY = (e.clientY - rect.top) * scaleY;
// 更新瞄准位置再开火 (确保开火方向准确)
aimX = clamp(clickX, 0, CANVAS_W);
aimY = clamp(clickY, 0, CANVAS_H);
fireBullet(aimX, aimY);
}
// 触摸支持
function handleTouchStart(e) {
e.preventDefault(); // 防止滚动
const touch = e.touches[0];
if (!touch) return;
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const touchX = (touch.clientX - rect.left) * scaleX;
const touchY = (touch.clientY - rect.top) * scaleY;
aimX = clamp(touchX, 0, CANVAS_W);
aimY = clamp(touchY, 0, CANVAS_H);
mouseInside = true;
if (!gameOver) {
fireBullet(aimX, aimY);
}
}
function handleTouchMove(e) {
e.preventDefault();
const touch = e.touches[0];
if (!touch) return;
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const touchX = (touch.clientX - rect.left) * scaleX;
const touchY = (touch.clientY - rect.top) * scaleY;
aimX = clamp(touchX, 0, CANVAS_W);
aimY = clamp(touchY, 0, CANVAS_H);
mouseInside = true;
}
function handleTouchEnd(e) {
e.preventDefault();
// 不重置mouseInside,只是为了下次触摸还在
}
// ----- 初始化与注册事件 -----
canvas.addEventListener('mousemove', handleMouseMove);
canvas.addEventListener('mouseleave', handleMouseLeave);
canvas.addEventListener('click', handleCanvasClick);
canvas.addEventListener('touchstart', handleTouchStart, { passive: false });
canvas.addEventListener('touchmove', handleTouchMove, { passive: false });
canvas.addEventListener('touchend', handleTouchEnd);
restartBtn.addEventListener('click', () => {
resetGame();
});
// 启动游戏
resetGame();
// 初始鼠标位置为玩家右方,让炮口朝右
aimX = player.x + 100;
aimY = player.y;
gameLoop();
})();
</script>
</body>
</html>
玩法与操作说明
您可以点击画布上的任意位置来发射炮弹,目标是击中不断靠近的海盗船。
- 核心玩法:您控制画面中央的炮台,海盗会从屏幕边缘向您靠近,您需要点击鼠标或触摸屏幕来瞄准并发射炮弹,每击中一个海盗可获得10分。
- 生命与反馈:屏幕上方显示您的生命值,如果海盗碰到您的炮台,生命值会减少,同时炮台会短暂闪烁,生命值归零则游戏结束。
- 重新开始:游戏结束后,点击右下角的“重新出海”按钮即可开始新一局。
优化建议: 您可以根据喜好调整游戏难度,在代码开头的“游戏配置”区域,修改 PIRATE_SPEED(海盗速度)或 MAX_PIRATES(海盗数量)来改变挑战性。


