制作类似aimlab的测试手速反应力的小游戏

📌 导读 :玩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>
相关推荐
小小de风呀1 小时前
de风——【从零开始学C++】(七):string类详解
开发语言·c++·算法
江屿风1 小时前
【c++笔记】类和对象流食般投喂(中)
开发语言·c++·笔记
Data_Journal1 小时前
Node.js网络爬取指南——简单易上手!
大数据·linux·服务器·前端·javascript
csbysj20201 小时前
C 语言输入与输出(I/O)详解
开发语言
Huangjin007_1 小时前
【C++ STL篇(八)】set容器——零基础入门与核心用法精讲
开发语言·c++·学习
c#上位机1 小时前
C#项目中打包文件的三种方式
开发语言·c#
hehelm1 小时前
C++ 特殊类设计
开发语言·c++
吃好睡好便好1 小时前
在Matlab中绘制圆锥三维曲面图
开发语言·人工智能·学习·算法·matlab·信息可视化
摇滚侠1 小时前
并发编程 Java 面试题 真正的 offer 偏方 Java 基础 Java 高级
java·开发语言