📌 导读 :玩FPS游戏的小伙伴基本都用过AimLab瞄准训练工具,能够有效锻炼鼠标瞄准、手眼协调和反应速度。今天用纯前端技术(HTML+CSS+JS+Canvas)复刻一款轻量化、多功能的网页版Aim训练小游戏,无需安装、浏览器直接打开,包含5种训练模式、完整数据统计、动画特效,非常适合前端练手和日常瞄准训练!
✅ 项目类型:前端实战小项目
✅ 技术栈:HTML + CSS3 + JavaScript + Canvas
✅ 运行方式:本地保存HTML文件,浏览器直接打开运行
一、项目开发背景
AimLab是目前最热门的FPS瞄准训练工具,能够针对性提升玩家的反应速度、瞄准精度、跟枪能力,是游戏玩家的必备训练工具。
但原版AimLab需要下载安装,且功能繁杂。因此我基于前端技术,开发了一款轻量化、无广告、免安装的网页版瞄准训练器,完美复刻核心训练功能。
本项目不仅可以作为前端入门实战项目,练习Canvas动画、交互逻辑、数据计算、状态管理,还可以日常用来训练手速和反应力,一举两得。
二、项目核心功能介绍
项目内置5种差异化训练模式,覆盖新手入门、精准瞄准、极速反应、动态跟枪、高压多目标训练,适配不同训练需求。
1. 经典模式(新手入门)
限时60秒,靶子随机刷新、尺寸适中、刷新节奏平缓,适合新手熟悉鼠标手感,建立基础瞄准节奏,是入门必练模式。
2. 极速模式(反应力训练)
限时30秒,大幅加快靶子刷新速度和消失速度,高密度刷新目标,专门训练瞬时反应速度和快速定位能力,突破反应瓶颈。
3. 精准模式(微操训练)
缩小靶子尺寸,容错率极低,单靶分值更高。摒弃手速比拼,专注训练鼠标微操和精准定位,适配FPS爆头瞄准训练。
4. 动态模式(跟枪训练)
所有靶子会在屏幕内自由移动,触碰边界自动反弹,需要持续追踪目标瞄准,专门锻炼跟枪顺滑度,适配移动目标对战场景。
5. 生成模式(高压训练)
特色趣味模式!连续命中靶子后,原靶会分裂生成多个小型新靶,目标越来越多、场面越来越复杂,训练高压下的注意力分配和快速决策能力。
三、项目特色亮点
1. 完整的数据统计系统
实时统计核心训练数据,训练结束生成完整结算报表:
-
实时数据:得分、命中率、平均反应时间、最高连击、剩余时间
-
结算数据:命中数、总射击数、每分钟命中数、综合评级(S-F分级


2. 高颜值视觉动画特效
-
渐变毛玻璃UI界面,科技风深色主题,护眼高级
-
靶子渐变消失、倒计时环形进度效果
-
命中粒子特效、分数弹出、连击动画提示
-
自定义十字准心,还原游戏瞄准手感

3. 便捷操作快捷键
-
鼠标左键:射击瞄准
-
R键:一键重新开始训练
-
ESC键:退出训练,返回主界面

4. 自适应响应式布局
适配电脑、平板等不同设备屏幕,窗口缩放自动适配画布尺寸,不会出现布局错乱、画面变形问题。
四、项目效果展示
1. 游戏主界面
左侧为数据统计面板+模式选择区,右侧为全屏游戏画布,界面简洁清晰,功能分区明确,按钮悬浮、选中高亮交互流畅。
2. 倒计时准备界面
点击开始训练后,3秒倒计时预热,避免仓促开局,动画过渡顺滑,体验感拉满。
3. 游戏对局效果
对局中实时刷新数据,命中触发粒子特效和分数弹窗,高连击自动弹出连击提示,靶子自带倒计时衰减效果,氛围感十足。
4. 最终结算界面
训练结束后展示全套训练数据,根据总分自动判定S/A/B/C/D/F评级,清晰记录每一次训练成果,方便对比进步。
五、完整源码分享
将以下代码复制,保存为 aim-trainer.html 文件,直接用任意浏览器打开即可运行,无需任何环境配置。
javascript
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>射击训练场 - Aim Trainer</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
min-height: 100vh;
overflow: hidden;
color: #fff;
}
.container {
display: flex;
height: 100vh;
}
/* 侧边栏 */
.sidebar {
width: 250px;
background: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(10px);
padding: 20px;
border-right: 1px solid rgba(255, 255, 255, 0.1);
overflow-y: auto;
}
.logo {
text-align: center;
margin-bottom: 30px;
}
.logo h1 {
font-size: 24px;
background: linear-gradient(45deg, #ff6b6b, #feca57, #48dbfb);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-shadow: 0 0 30px rgba(255, 107, 107, 0.5);
}
.stats-panel {
background: rgba(255, 255, 255, 0.05);
border-radius: 15px;
padding: 15px;
margin-bottom: 20px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.stats-panel h3 {
font-size: 14px;
color: #888;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 15px;
}
.stat-row {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
padding: 8px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.stat-label {
color: #aaa;
font-size: 13px;
}
.stat-value {
color: #fff;
font-weight: bold;
font-size: 16px;
}
.stat-value.good {
color: #48dbfb;
}
.stat-value.excellent {
color: #1dd1a1;
}
.stat-value.poor {
color: #ff6b6b;
}
/* 模式选择 */
.mode-section h3 {
font-size: 14px;
color: #888;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 15px;
}
.mode-btn {
width: 100%;
padding: 12px 15px;
margin-bottom: 10px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 10px;
color: #fff;
cursor: pointer;
transition: all 0.3s ease;
text-align: left;
font-size: 14px;
}
.mode-btn:hover {
background: rgba(255, 255, 255, 0.1);
transform: translateX(5px);
}
.mode-btn.active {
background: linear-gradient(45deg, #ff6b6b, #feca57);
border-color: transparent;
box-shadow: 0 5px 20px rgba(255, 107, 107, 0.4);
}
.mode-btn .mode-name {
font-weight: bold;
display: block;
margin-bottom: 3px;
}
.mode-btn .mode-desc {
font-size: 11px;
color: #888;
}
/* 主游戏区域 */
.game-area {
flex: 1;
position: relative;
overflow: hidden;
}
#gameCanvas {
display: block;
cursor: crosshair;
}
/* 顶部信息栏 */
.top-bar {
position: absolute;
top: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 30px;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(10px);
padding: 15px 40px;
border-radius: 50px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.top-stat {
text-align: center;
}
.top-stat-label {
font-size: 11px;
color: #888;
text-transform: uppercase;
letter-spacing: 1px;
}
.top-stat-value {
font-size: 24px;
font-weight: bold;
color: #fff;
}
.top-stat-value.score {
color: #feca57;
}
.top-stat-value.accuracy {
color: #48dbfb;
}
.top-stat-value.combo {
color: #ff6b6b;
}
/* 开始界面 */
.start-screen {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(10px);
}
.start-screen h2 {
font-size: 48px;
margin-bottom: 20px;
background: linear-gradient(45deg, #ff6b6b, #feca57, #48dbfb);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.start-screen p {
color: #888;
margin-bottom: 40px;
font-size: 16px;
}
.btn-start {
padding: 18px 60px;
font-size: 20px;
font-weight: bold;
background: linear-gradient(45deg, #ff6b6b, #feca57);
border: none;
border-radius: 50px;
color: #000;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 10px 30px rgba(255, 107, 107, 0.4);
}
.btn-start:hover {
transform: scale(1.05);
box-shadow: 0 15px 40px rgba(255, 107, 107, 0.6);
}
.controls-info {
position: absolute;
bottom: 30px;
text-align: center;
color: #666;
font-size: 14px;
}
.controls-info kbd {
background: rgba(255, 255, 255, 0.1);
padding: 3px 8px;
border-radius: 5px;
margin: 0 3px;
border: 1px solid rgba(255, 255, 255, 0.2);
}
/* 结束界面 */
.end-screen {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: none;
flex-direction: column;
justify-content: center;
align-items: center;
background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(10px);
}
.end-screen h2 {
font-size: 42px;
margin-bottom: 10px;
color: #feca57;
}
.final-score {
font-size: 72px;
font-weight: bold;
background: linear-gradient(45deg, #ff6b6b, #feca57);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 30px;
}
.grade {
font-size: 120px;
margin-bottom: 30px;
}
.results-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
margin-bottom: 40px;
}
.result-card {
background: rgba(255, 255, 255, 0.05);
border-radius: 15px;
padding: 20px 30px;
text-align: center;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.result-card-value {
font-size: 32px;
font-weight: bold;
color: #fff;
margin-bottom: 5px;
}
.result-card-label {
font-size: 12px;
color: #888;
text-transform: uppercase;
}
.btn-restart {
padding: 15px 50px;
font-size: 18px;
font-weight: bold;
background: transparent;
border: 2px solid #ff6b6b;
border-radius: 50px;
color: #ff6b6b;
cursor: pointer;
transition: all 0.3s ease;
}
.btn-restart:hover {
background: #ff6b6b;
color: #000;
}
/* 十字准心 */
.crosshair {
position: fixed;
pointer-events: none;
z-index: 1000;
}
.crosshair::before,
.crosshair::after {
content: '';
position: absolute;
background: rgba(255, 255, 255, 0.8);
}
.crosshair::before {
width: 2px;
height: 20px;
left: 50%;
top: -10px;
transform: translateX(-50%);
}
.crosshair::after {
width: 20px;
height: 2px;
top: 50%;
left: -10px;
transform: translateY(-50%);
}
/* 命中特效 */
.hit-effect {
position: absolute;
pointer-events: none;
animation: hitAnim 0.3s ease-out forwards;
}
@keyframes hitAnim {
0% {
transform: scale(0);
opacity: 1;
}
100% {
transform: scale(2);
opacity: 0;
}
}
/* 连击提示 */
.combo-popup {
position: absolute;
font-size: 48px;
font-weight: bold;
color: #ff6b6b;
pointer-events: none;
animation: comboAnim 0.8s ease-out forwards;
text-shadow: 0 0 20px rgba(255, 107, 107, 0.8);
}
@keyframes comboAnim {
0% {
transform: scale(0.5) translateY(0);
opacity: 1;
}
50% {
transform: scale(1.2) translateY(-20px);
}
100% {
transform: scale(1) translateY(-50px);
opacity: 0;
}
}
/* 倒计时 */
.countdown {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 120px;
font-weight: bold;
color: #fff;
text-shadow: 0 0 50px rgba(255, 255, 255, 0.5);
animation: countdownAnim 1s ease-in-out;
}
@keyframes countdownAnim {
0% { transform: translate(-50%, -50%) scale(2); opacity: 0; }
50% { transform: translate(-50%, -50%) scale(1); opacity: 1; }
100% { transform: translate(-50%, -50%) scale(0.5); opacity: 0; }
}
/* 响应式 */
@media (max-width: 900px) {
.container {
flex-direction: column;
}
.sidebar {
width: 100%;
height: auto;
max-height: 200px;
}
.top-bar {
padding: 10px 20px;
gap: 15px;
}
.top-stat-value {
font-size: 18px;
}
}
</style>
</head>
<body>
<div class="crosshair"></div>
<div class="container">
<!-- 侧边栏 -->
<div class="sidebar">
<div class="logo">
<h1>🎯 AIM TRAINER</h1>
</div>
<div class="stats-panel">
<h3>当前数据</h3>
<div class="stat-row">
<span class="stat-label">得分</span>
<span class="stat-value score" id="scoreStat">0</span>
</div>
<div class="stat-row">
<span class="stat-label">命中率</span>
<span class="stat-value accuracy" id="accuracyStat">0%</span>
</div>
<div class="stat-row">
<span class="stat-label">平均反应时间</span>
<span class="stat-value" id="avgTimeStat">0ms</span>
</div>
<div class="stat-row">
<span class="stat-label">最高连击</span>
<span class="stat-value combo" id="maxComboStat">0x</span>
</div>
<div class="stat-row">
<span class="stat-label">剩余时间</span>
<span class="stat-value" id="timeStat">60s</span>
</div>
</div>
<div class="mode-section">
<h3>训练模式</h3>
<button class="mode-btn active" data-mode="classic">
<span class="mode-name">🎯 经典模式</span>
<span class="mode-desc">点击出现的靶子,限时60秒</span>
</button>
<button class="mode-btn" data-mode="speed">
<span class="mode-name">⚡ 极速模式</span>
<span class="mode-desc">靶子出现更快,只有30秒</span>
</button>
<button class="mode-btn" data-mode="precision">
<span class="mode-name">🎯 精准模式</span>
<span class="mode-desc">小靶子,高分值,考验精度</span>
</button>
<button class="mode-btn" data-mode="dynamic">
<span class="mode-name">🔄 动态模式</span>
<span class="mode-desc">靶子会移动,挑战追踪能力</span>
</button>
<button class="mode-btn" data-mode="spawn">
<span class="mode-name">💀 生成模式</span>
<span class="mode-desc">击中后分裂成更多小靶子</span>
</button>
</div>
</div>
<!-- 游戏区域 -->
<div class="game-area">
<canvas id="gameCanvas"></canvas>
<!-- 顶部信息栏 -->
<div class="top-bar">
<div class="top-stat">
<div class="top-stat-label">得分</div>
<div class="top-stat-value score" id="topScore">0</div>
</div>
<div class="top-stat">
<div class="top-stat-label">命中率</div>
<div class="top-stat-value accuracy" id="topAccuracy">0%</div>
</div>
<div class="top-stat">
<div class="top-stat-label">连击</div>
<div class="top-stat-value combo" id="topCombo">0x</div>
</div>
<div class="top-stat">
<div class="top-stat-label">反应时间</div>
<div class="top-stat-value" id="topReact">0ms</div>
</div>
</div>
<!-- 开始界面 -->
<div class="start-screen" id="startScreen">
<h2>射击训练场</h2>
<p>选择模式并开始训练你的瞄准技巧</p>
<button class="btn-start" id="btnStart">开始训练</button>
<div class="controls-info">
点击 <kbd>左键</kbd> 射击 | <kbd>R</kbd> 重新开始 | <kbd>ESC</kbd> 退出
</div>
</div>
<!-- 结束界面 -->
<div class="end-screen" id="endScreen">
<h2>训练完成!</h2>
<div class="grade" id="grade">S</div>
<div class="final-score" id="finalScore">0</div>
<div class="results-grid">
<div class="result-card">
<div class="result-card-value" id="resHits">0</div>
<div class="result-card-label">命中数</div>
</div>
<div class="result-card">
<div class="result-card-value" id="resAccuracy">0%</div>
<div class="result-card-label">命中率</div>
</div>
<div class="result-card">
<div class="result-card-value" id="resAvgTime">0ms</div>
<div class="result-card-label">平均反应</div>
</div>
<div class="result-card">
<div class="result-card-value" id="resMaxCombo">0x</div>
<div class="result-card-label">最高连击</div>
</div>
<div class="result-card">
<div class="result-card-value" id="resShots">0</div>
<div class="result-card-label">总射击数</div>
</div>
<div class="result-card">
<div class="result-card-value" id="resPPM">0</div>
<div class="result-card-label">每分钟命中</div>
</div>
</div>
<button class="btn-restart" id="btnRestart">再来一次</button>
</div>
</div>
</div>
<script>
// 游戏配置
const CONFIG = {
classic: {
timeLimit: 60,
spawnInterval: 800,
targetLifespan: 2000,
targetSize: 50,
maxTargets: 8
},
speed: {
timeLimit: 30,
spawnInterval: 400,
targetLifespan: 1200,
targetSize: 45,
maxTargets: 10
},
precision: {
timeLimit: 60,
spawnInterval: 1000,
targetLifespan: 1500,
targetSize: 30,
maxTargets: 6
},
dynamic: {
timeLimit: 60,
spawnInterval: 700,
targetLifespan: 2500,
targetSize: 45,
maxTargets: 8,
moving: true
},
spawn: {
timeLimit: 60,
spawnInterval: 600,
targetLifespan: 2000,
targetSize: 55,
maxTargets: 5,
spawnOnHit: true
}
};
// 游戏状态
let gameState = {
isRunning: false,
mode: 'classic',
score: 0,
hits: 0,
shots: 0,
misses: 0,
combo: 0,
maxCombo: 0,
reactionTimes: [],
timeLeft: 60,
targets: [],
particles: [],
lastSpawn: 0
};
// Canvas 设置
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
let width, height;
function resizeCanvas() {
width = canvas.width = window.innerWidth - 250;
height = canvas.height = window.innerHeight;
}
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
// 靶子类
class Target {
constructor(x, y, size, type = 'normal') {
this.x = x || Math.random() * (width - size * 2) + size;
this.y = y || Math.random() * (height - size * 2) + size;
this.size = size;
this.type = type;
this.spawnTime = Date.now();
this.lifespan = CONFIG[gameState.mode].targetLifespan;
this.hit = false;
if (CONFIG[gameState.mode].moving) {
this.vx = (Math.random() - 0.5) * 4;
this.vy = (Math.random() - 0.5) * 4;
} else {
this.vx = 0;
this.vy = 0;
}
}
update() {
if (this.hit) return;
this.x += this.vx;
this.y += this.vy;
// 边界反弹
if (this.x - this.size < 0 || this.x + this.size > width) {
this.vx *= -1;
this.x = Math.max(this.size, Math.min(width - this.size, this.x));
}
if (this.y - this.size < 0 || this.y + this.size > height) {
this.vy *= -1;
this.y = Math.max(this.size, Math.min(height - this.size, this.y));
}
}
draw() {
if (this.hit) return;
const elapsed = Date.now() - this.spawnTime;
const progress = elapsed / this.lifespan;
const alpha = 1 - progress;
// 外圈
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fillStyle = `rgba(255, 107, 107, ${alpha * 0.3})`;
ctx.fill();
// 中圈
ctx.beginPath();
ctx.arc(this.x, this.y, this.size * 0.7, 0, Math.PI * 2);
ctx.fillStyle = `rgba(254, 202, 87, ${alpha * 0.6})`;
ctx.fill();
// 内圈
ctx.beginPath();
ctx.arc(this.x, this.y, this.size * 0.4, 0, Math.PI * 2);
ctx.fillStyle = `rgba(72, 219, 251, ${alpha})`;
ctx.fill();
// 中心点
ctx.beginPath();
ctx.arc(this.x, this.y, this.size * 0.15, 0, Math.PI * 2);
ctx.fillStyle = `rgba(255, 255, 255, ${alpha})`;
ctx.fill();
// 倒计时环
if (this.lifespan > 0) {
ctx.beginPath();
ctx.arc(this.x, this.y, this.size + 5, -Math.PI/2, -Math.PI/2 + Math.PI * 2 * (1 - progress));
ctx.strokeStyle = `rgba(255, 255, 255, ${alpha * 0.5})`;
ctx.lineWidth = 3;
ctx.stroke();
}
}
isExpired() {
return Date.now() - this.spawnTime > this.lifespan;
}
containsPoint(px, py) {
const dist = Math.sqrt((px - this.x) ** 2 + (py - this.y) ** 2);
return dist <= this.size;
}
}
// 粒子类
class Particle {
constructor(x, y, color) {
this.x = x;
this.y = y;
this.vx = (Math.random() - 0.5) * 10;
this.vy = (Math.random() - 0.5) * 10;
this.life = 1;
this.color = color;
this.size = Math.random() * 5 + 2;
}
update() {
this.x += this.vx;
this.y += this.vy;
this.vy += 0.2; // 重力
this.life -= 0.02;
}
draw() {
ctx.save();
ctx.globalAlpha = this.life;
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fillStyle = this.color;
ctx.fill();
ctx.restore();
}
isDead() {
return this.life <= 0;
}
}
// 创建命中特效
function createHitEffect(x, y, score) {
// 粒子
for (let i = 0; i < 15; i++) {
gameState.particles.push(new Particle(x, y, '#48dbfb'));
}
// 分数弹出
showScorePopup(x, y, score);
}
// 显示分数弹出
function showScorePopup(x, y, score) {
const popup = document.createElement('div');
popup.className = 'hit-effect';
popup.style.left = x + 'px';
popup.style.top = y + 'px';
popup.style.color = score >= 100 ? '#feca57' : '#48dbfb';
popup.style.fontSize = score >= 100 ? '28px' : '22px';
popup.style.position = 'absolute';
popup.style.pointerEvents = 'none';
popup.style.fontWeight = 'bold';
popup.textContent = `+${score}`;
document.querySelector('.game-area').appendChild(popup);
setTimeout(() => popup.remove(), 500);
}
// 显示连击
function showCombo(combo) {
if (combo < 3) return;
const popup = document.createElement('div');
popup.className = 'combo-popup';
popup.style.left = Math.random() * (width - 200) + 100 + 'px';
popup.style.top = height / 2 + 'px';
popup.textContent = `${combo}x COMBO!`;
document.querySelector('.game-area').appendChild(popup);
setTimeout(() => popup.remove(), 800);
}
// 生成新靶子
function spawnTarget() {
const config = CONFIG[gameState.mode];
if (gameState.targets.length < config.maxTargets) {
gameState.targets.push(new Target(null, null, config.targetSize));
}
}
// 处理点击
function handleClick(e) {
if (!gameState.isRunning) return;
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
gameState.shots++;
let hit = false;
for (let i = gameState.targets.length - 1; i >= 0; i--) {
const target = gameState.targets[i];
if (!target.hit && target.containsPoint(x, y)) {
target.hit = true;
hit = true;
// 计算反应时间
const reactionTime = Date.now() - target.spawnTime;
gameState.reactionTimes.push(reactionTime);
// 计算分数
const baseScore = 10;
const timeBonus = Math.max(0, 500 - reactionTime) / 10;
const comboMultiplier = Math.min(gameState.combo * 0.1 + 1, 3);
const score = Math.round((baseScore + timeBonus) * comboMultiplier);
gameState.score += score;
gameState.hits++;
gameState.combo++;
if (gameState.combo > gameState.maxCombo) {
gameState.maxCombo = gameState.combo;
}
createHitEffect(target.x, target.y, score);
showCombo(gameState.combo);
// 生成模式:分裂
if (CONFIG[gameState.mode].spawnOnHit && gameState.combo > 1) {
const numSpawns = Math.min(3, gameState.combo);
for (let j = 0; j < numSpawns; j++) {
const angle = (j / numSpawns) * Math.PI * 2;
const offsetX = Math.cos(angle) * 80;
const offsetY = Math.sin(angle) * 80;
const newSize = target.size * 0.6;
if (newSize > 15) {
gameState.targets.push(new Target(
target.x + offsetX,
target.y + offsetY,
newSize
));
}
}
}
break;
}
}
if (!hit) {
gameState.misses++;
gameState.combo = 0;
}
updateUI();
}
// 更新UI
function updateUI() {
const accuracy = gameState.shots > 0 ? Math.round((gameState.hits / gameState.shots) * 100) : 0;
const avgReaction = gameState.reactionTimes.length > 0
? Math.round(gameState.reactionTimes.reduce((a, b) => a + b, 0) / gameState.reactionTimes.length)
: 0;
// 侧边栏
document.getElementById('scoreStat').textContent = gameState.score;
document.getElementById('accuracyStat').textContent = accuracy + '%';
document.getElementById('avgTimeStat').textContent = avgReaction + 'ms';
document.getElementById('maxComboStat').textContent = gameState.maxCombo + 'x';
document.getElementById('timeStat').textContent = gameState.timeLeft + 's';
// 顶部栏
document.getElementById('topScore').textContent = gameState.score;
document.getElementById('topAccuracy').textContent = accuracy + '%';
document.getElementById('topCombo').textContent = gameState.combo + 'x';
document.getElementById('topReact').textContent = avgReaction + 'ms';
}
// 更新统计数据
function updateStats() {
const accuracy = gameState.shots > 0 ? Math.round((gameState.hits / gameState.shots) * 100) : 0;
const avgReaction = gameState.reactionTimes.length > 0
? Math.round(gameState.reactionTimes.reduce((a, b) => a + b, 0) / gameState.reactionTimes.length)
: 0;
document.getElementById('scoreStat').textContent = gameState.score;
document.getElementById('accuracyStat').textContent = accuracy + '%';
document.getElementById('accuracyStat').className = 'stat-value accuracy ' +
(accuracy >= 70 ? 'excellent' : accuracy >= 50 ? 'good' : 'poor');
document.getElementById('avgTimeStat').textContent = avgReaction + 'ms';
document.getElementById('maxComboStat').textContent = gameState.maxCombo + 'x';
document.getElementById('timeStat').textContent = gameState.timeLeft + 's';
}
// 游戏循环
let lastTime = 0;
function gameLoop(timestamp) {
if (!gameState.isRunning) return;
const delta = timestamp - lastTime;
lastTime = timestamp;
// 清除画布
ctx.fillStyle = 'rgba(26, 26, 46, 0.3)';
ctx.fillRect(0, 0, width, height);
// 生成靶子
if (timestamp - gameState.lastSpawn > CONFIG[gameState.mode].spawnInterval) {
spawnTarget();
gameState.lastSpawn = timestamp;
}
// 更新和绘制靶子
gameState.targets = gameState.targets.filter(t => !t.isExpired() && !t.hit);
gameState.targets.forEach(t => {
t.update();
t.draw();
});
// 更新和绘制粒子
gameState.particles = gameState.particles.filter(p => !p.isDead());
gameState.particles.forEach(p => {
p.update();
p.draw();
});
requestAnimationFrame(gameLoop);
}
// 开始游戏
function startGame() {
gameState = {
...gameState,
isRunning: true,
score: 0,
hits: 0,
shots: 0,
misses: 0,
combo: 0,
maxCombo: 0,
reactionTimes: [],
timeLeft: CONFIG[gameState.mode].timeLimit,
targets: [],
particles: [],
lastSpawn: 0
};
document.getElementById('startScreen').style.display = 'none';
document.getElementById('endScreen').style.display = 'none';
// 倒计时
let count = 3;
const countdown = setInterval(() => {
if (count > 0) {
showCountdown(count);
count--;
} else {
clearInterval(countdown);
gameLoop(0);
startTimer();
}
}, 1000);
}
// 显示倒计时
function showCountdown(num) {
const existing = document.querySelector('.countdown');
if (existing) existing.remove();
const div = document.createElement('div');
div.className = 'countdown';
div.textContent = num;
document.querySelector('.game-area').appendChild(div);
setTimeout(() => div.remove(), 900);
}
// 计时器
function startTimer() {
const timer = setInterval(() => {
if (!gameState.isRunning) {
clearInterval(timer);
return;
}
gameState.timeLeft--;
updateStats();
if (gameState.timeLeft <= 0) {
clearInterval(timer);
endGame();
}
}, 1000);
}
// 结束游戏
function endGame() {
gameState.isRunning = false;
const accuracy = gameState.shots > 0 ? Math.round((gameState.hits / gameState.shots) * 100) : 0;
const avgReaction = gameState.reactionTimes.length > 0
? Math.round(gameState.reactionTimes.reduce((a, b) => a + b, 0) / gameState.reactionTimes.length)
: 0;
const ppm = Math.round((gameState.hits / CONFIG[gameState.mode].timeLimit) * 60);
// 计算评级
let grade = 'F';
if (gameState.score >= 5000) grade = 'S';
else if (gameState.score >= 4000) grade = 'A';
else if (gameState.score >= 3000) grade = 'B';
else if (gameState.score >= 2000) grade = 'C';
else if (gameState.score >= 1000) grade = 'D';
document.getElementById('grade').textContent = grade;
document.getElementById('finalScore').textContent = gameState.score;
document.getElementById('resHits').textContent = gameState.hits;
document.getElementById('resAccuracy').textContent = accuracy + '%';
document.getElementById('resAvgTime').textContent = avgReaction + 'ms';
document.getElementById('resMaxCombo').textContent = gameState.maxCombo + 'x';
document.getElementById('resShots').textContent = gameState.shots;
document.getElementById('resPPM').textContent = ppm;
document.getElementById('endScreen').style.display = 'flex';
}
// 事件监听
canvas.addEventListener('click', handleClick);
document.getElementById('btnStart').addEventListener('click', startGame);
document.getElementById('btnRestart').addEventListener('click', startGame);
// 模式选择
document.querySelectorAll('.mode-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.mode-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
gameState.mode = btn.dataset.mode;
});
});
// 键盘快捷键
document.addEventListener('keydown', (e) => {
if (e.key === 'r' || e.key === 'R') {
startGame();
}
if (e.key === 'Escape') {
if (gameState.isRunning) {
gameState.isRunning = false;
document.getElementById('startScreen').style.display = 'flex';
}
}
});
// 初始绘制
ctx.fillStyle = '#1a1a2e';
ctx.fillRect(0, 0, width, height);
</script>
</body>
</html>