🎮ArcadeLab

🏃 简笔画小人跑酷 - 跳跃游戏

by RocketTiger92
991 lines38.7 KB
▶ Play
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>🏃 简笔画小人跑酷 - 跳跃游戏</title>
    <style>
        :root {
            --bg: #1a1a2e;
        }

        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            background: var(--bg);
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
            font-family: 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
            overflow: hidden;
            user-select: none;
            -webkit-user-select: none;
            -webkit-tap-highlight-color: transparent;
            cursor: pointer;
        }

        .game-wrapper {
            position: relative;
            border-radius: 20px;
            overflow: hidden;
            box-shadow:
                0 20px 60px rgba(0, 0, 0, 0.5),
                0 0 0 3px rgba(255, 255, 255, 0.08),
                0 0 0 6px rgba(0, 0, 0, 0.2),
                0 0 120px rgba(100, 180, 255, 0.15);
            transition: transform 0.15s ease, box-shadow 0.15s ease;
            max-width: 95vw;
            max-height: 90vh;
        }

        .game-wrapper:active {
            transform: scale(0.995);
            box-shadow:
                0 15px 45px rgba(0, 0, 0, 0.5),
                0 0 0 3px rgba(255, 255, 255, 0.06),
                0 0 0 6px rgba(0, 0, 0, 0.2),
                0 0 100px rgba(100, 180, 255, 0.1);
        }

        canvas {
            display: block;
            border-radius: 17px;
            max-width: 100%;
            height: auto;
        }

        .hint-bar {
            position: absolute;
            bottom: 16px;
            left: 50%;
            transform: translateX(-50%);
            background: rgba(0, 0, 0, 0.65);
            color: #fff;
            padding: 8px 20px;
            border-radius: 25px;
            font-size: 14px;
            letter-spacing: 0.5px;
            pointer-events: none;
            backdrop-filter: blur(8px);
            -webkit-backdrop-filter: blur(8px);
            border: 1px solid rgba(255, 255, 255, 0.2);
            transition: opacity 0.4s ease;
        }
    </style>
</head>
<body>

    <div class="game-wrapper" id="gameWrapper">
        <canvas id="gameCanvas"></canvas>
        <div class="hint-bar" id="hintBar">⌨️ 空格键 或 🖱️ 点击屏幕跳跃</div>
    </div>

    <script>
        (function() {
            const canvas = document.getElementById('gameCanvas');
            const ctx = canvas.getContext('2d');
            const wrapper = document.getElementById('gameWrapper');
            const hintBar = document.getElementById('hintBar');

            // --- 画布尺寸 ---
            const WIDTH = 800;
            const HEIGHT = 420;
            canvas.width = WIDTH;
            canvas.height = HEIGHT;

            // --- 响应式缩放 ---
            function resizeCanvas() {
                const scale = Math.min(
                    (window.innerWidth * 0.95) / WIDTH,
                    (window.innerHeight * 0.88) / HEIGHT,
                    1.0
                );
                canvas.style.width = WIDTH * scale + 'px';
                canvas.style.height = HEIGHT * scale + 'px';
            }
            resizeCanvas();
            window.addEventListener('resize', resizeCanvas);

            // --- 游戏常量 ---
            const GROUND_Y = 340; // 地面线(脚底位置)
            const PLAYER_X = 130; // 玩家水平位置
            const GRAVITY_PER_SEC = 1800; // 重力加速度 px/s² (60fps下每帧0.5px)
            const JUMP_VELOCITY = -600; // 跳跃初速度 px/s (60fps下约-10.3px/帧)
            const OBSTACLE_SPEED = 220; // 障碍物速度 px/s
            const SCORE_PER_SECOND = 10; // 每秒得分
            const MIN_SPAWN_INTERVAL = 0.5; // 最小生成间隔(秒)
            const MAX_SPAWN_INTERVAL = 1.5; // 最大生成间隔(秒)

            // --- 游戏状态 ---
            const STATE = { WAITING: 'waiting', PLAYING: 'playing', GAME_OVER: 'gameOver' };
            let gameState = STATE.WAITING;
            let score = 0;
            let scoreAccumulator = 0;
            let playerY = GROUND_Y; // 玩家脚底Y坐标
            let playerVelocityY = 0;
            let isOnGround = true;
            let obstacles = [];
            let spawnTimer = 0;
            let particles = [];
            let clouds = [];
            let screenShake = 0;
            let gameOverAlpha = 0;
            let restartCooldown = 0;
            let lastTime = performance.now();

            // --- 音效系统 (Web Audio API) ---
            let audioCtx = null;

            function getAudioContext() {
                if (!audioCtx) {
                    try {
                        audioCtx = new(window.AudioContext || window.webkitAudioContext)();
                    } catch (e) {
                        audioCtx = null;
                    }
                }
                if (audioCtx && audioCtx.state === 'suspended') {
                    audioCtx.resume();
                }
                return audioCtx;
            }

            function playSound(freq, duration, type = 'square', vol = 0.08, freqEnd = null) {
                const ctx = getAudioContext();
                if (!ctx) return;
                try {
                    const t = ctx.currentTime;
                    const oscillator = ctx.createOscillator();
                    const gainNode = ctx.createGain();
                    oscillator.type = type;
                    oscillator.frequency.setValueAtTime(freq, t);
                    if (freqEnd) {
                        oscillator.frequency.linearRampToValueAtTime(freqEnd, t + duration);
                    }
                    gainNode.gain.setValueAtTime(vol, t);
                    gainNode.gain.exponentialRampToValueAtTime(0.001, t + duration);
                    oscillator.connect(gainNode);
                    gainNode.connect(ctx.destination);
                    oscillator.start(t);
                    oscillator.stop(t + duration);
                } catch (e) {
                    // 静默处理
                }
            }

            function sfxJump() {
                playSound(420, 0.1, 'square', 0.06, 680);
                setTimeout(() => playSound(520, 0.07, 'square', 0.04, 750), 40);
            }

            function sfxHit() {
                playSound(60, 0.35, 'sawtooth', 0.12, 25);
                playSound(90, 0.25, 'triangle', 0.08, 40);
            }

            function sfxScore() {
                playSound(880, 0.06, 'sine', 0.03, 1100);
            }

            // --- 初始化云朵 ---
            function initClouds() {
                clouds = [];
                for (let i = 0; i < 5; i++) {
                    clouds.push({
                        x: Math.random() * WIDTH,
                        y: 25 + Math.random() * 130,
                        w: 60 + Math.random() * 100,
                        h: 25 + Math.random() * 35,
                        speed: 15 + Math.random() * 35,
                        opacity: 0.35 + Math.random() * 0.45,
                    });
                }
            }
            initClouds();

            // --- 粒子系统 ---
            function spawnParticles(x, y, count, color, spreadX = 3, spreadY = 4, life = 0.5) {
                for (let i = 0; i < count; i++) {
                    particles.push({
                        x: x,
                        y: y,
                        vx: (Math.random() - 0.5) * spreadX * 2,
                        vy: -Math.random() * spreadY * 8 - 2,
                        life: life + Math.random() * 0.3,
                        maxLife: life + 0.3,
                        color: color,
                        size: 1.5 + Math.random() * 3.5,
                    });
                }
            }

            function updateParticles(dt) {
                for (let i = particles.length - 1; i >= 0; i--) {
                    const p = particles[i];
                    p.x += p.vx * dt;
                    p.y += p.vy * dt;
                    p.vy += 400 * dt; // 粒子重力
                    p.life -= dt;
                    if (p.life <= 0) particles.splice(i, 1);
                }
            }

            function drawParticles(ctx) {
                for (const p of particles) {
                    const alpha = Math.max(0, p.life / p.maxLife);
                    ctx.fillStyle = p.color.replace('1)', `${alpha})`).replace('rgb',
                        'rgba');
                    ctx.beginPath();
                    ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
                    ctx.fill();
                }
            }

            // --- 重置游戏 ---
            function resetGame() {
                playerY = GROUND_Y;
                playerVelocityY = 0;
                isOnGround = true;
                obstacles = [];
                particles = [];
                score = 0;
                scoreAccumulator = 0;
                spawnTimer = 0.6;
                screenShake = 0;
                gameOverAlpha = 0;
                restartCooldown = 0;
                lastTime = performance.now();
                gameState = STATE.PLAYING;
                hintBar.style.opacity = '0';
            }

            // --- 生成障碍物 ---
            function spawnObstacle() {
                const heightOptions = [
                    { h: 32, w: 16, label: 'tiny' },
                    { h: 42, w: 18, label: 'small' },
                    { h: 52, w: 20, label: 'medium' },
                    { h: 62, w: 22, label: 'large' },
                    { h: 74, w: 24, label: 'xlarge' },
                ];
                const choice = heightOptions[Math.floor(Math.random() * heightOptions.length)];
                obstacles.push({
                    x: WIDTH + 10,
                    y: GROUND_Y - choice.h,
                    width: choice.w,
                    height: choice.h,
                    label: choice.label,
                    passed: false,
                });
            }

            // --- 碰撞检测 ---
            function checkCollision(playerBox, obs) {
                const obsBox = {
                    x: obs.x - obs.width / 2,
                    y: obs.y,
                    w: obs.width,
                    h: obs.height,
                };
                // 缩小一点碰撞框,更公平
                const margin = 5;
                const px = playerBox.x + margin;
                const py = playerBox.y + margin;
                const pw = playerBox.w - margin * 2;
                const ph = playerBox.h - margin * 2;
                const ox = obsBox.x + margin * 0.6;
                const oy = obsBox.y + margin * 0.4;
                const ow = obsBox.w - margin * 1.2;
                const oh = obsBox.h - margin * 0.8;

                return (
                    px < ox + ow &&
                    px + pw > ox &&
                    py < oy + oh &&
                    py + ph > oy
                );
            }

            function getPlayerCollisionBox() {
                const headRadius = 13;
                const totalHeight = 58;
                const footY = playerY;
                const topY = footY - totalHeight;
                const boxWidth = 28;
                return {
                    x: PLAYER_X - boxWidth / 2,
                    y: topY,
                    w: boxWidth,
                    h: totalHeight,
                };
            }

            // --- 绘制函数 ---
            function drawSky(ctx) {
                const gradient = ctx.createLinearGradient(0, 0, 0, GROUND_Y);
                gradient.addColorStop(0, '#b8dff5');
                gradient.addColorStop(0.45, '#d4eaf7');
                gradient.addColorStop(0.85, '#e8f4fb');
                gradient.addColorStop(1, '#f0f7fc');
                ctx.fillStyle = gradient;
                ctx.fillRect(0, 0, WIDTH, GROUND_Y);

                // 太阳光晕
                const sunX = WIDTH * 0.78;
                const sunY = 65;
                const sunGrad = ctx.createRadialGradient(sunX, sunY, 20, sunX, sunY, 180);
                sunGrad.addColorStop(0, 'rgba(255,252,235,0.9)');
                sunGrad.addColorStop(0.25, 'rgba(255,248,220,0.5)');
                sunGrad.addColorStop(0.6, 'rgba(255,240,200,0.08)');
                sunGrad.addColorStop(1, 'rgba(255,235,180,0)');
                ctx.fillStyle = sunGrad;
                ctx.fillRect(0, 0, WIDTH, GROUND_Y);
            }

            function drawClouds(ctx) {
                for (const cloud of clouds) {
                    ctx.fillStyle = `rgba(255,255,255,${cloud.opacity})`;
                    const cx = cloud.x;
                    const cy = cloud.y;
                    const w = cloud.w;
                    const h = cloud.h;
                    // 多个椭圆组成云朵
                    ctx.beginPath();
                    ctx.ellipse(cx, cy, w * 0.5, h * 0.45, 0, 0, Math.PI * 2);
                    ctx.fill();
                    ctx.beginPath();
                    ctx.ellipse(cx - w * 0.28, cy + h * 0.08, w * 0.35, h * 0.38, 0, 0, Math.PI * 2);
                    ctx.fill();
                    ctx.beginPath();
                    ctx.ellipse(cx + w * 0.3, cy + h * 0.05, w * 0.38, h * 0.4, 0, 0, Math.PI * 2);
                    ctx.fill();
                    ctx.beginPath();
                    ctx.ellipse(cx - w * 0.1, cy - h * 0.2, w * 0.33, h * 0.42, 0, 0, Math.PI * 2);
                    ctx.fill();
                    ctx.beginPath();
                    ctx.ellipse(cx + w * 0.12, cy - h * 0.15, w * 0.3, h * 0.4, 0, 0, Math.PI * 2);
                    ctx.fill();
                }
            }

            function drawGround(ctx) {
                // 草地主体
                const grassGrad = ctx.createLinearGradient(0, GROUND_Y, 0, HEIGHT);
                grassGrad.addColorStop(0, '#7bc34d');
                grassGrad.addColorStop(0.15, '#6aaf3c');
                grassGrad.addColorStop(0.5, '#558b2f');
                grassGrad.addColorStop(1, '#3e6b1f');
                ctx.fillStyle = grassGrad;
                ctx.fillRect(0, GROUND_Y, WIDTH, HEIGHT - GROUND_Y);

                // 地面线
                ctx.strokeStyle = '#5c9e2f';
                ctx.lineWidth = 2.5;
                ctx.beginPath();
                ctx.moveTo(0, GROUND_Y);
                ctx.lineTo(WIDTH, GROUND_Y);
                ctx.stroke();

                // 地面线高光
                ctx.strokeStyle = 'rgba(255,255,255,0.25)';
                ctx.lineWidth = 1;
                ctx.beginPath();
                ctx.moveTo(0, GROUND_Y + 1);
                ctx.lineTo(WIDTH, GROUND_Y + 1);
                ctx.stroke();

                // 草丛纹理
                const grassBlades = 90;
                for (let i = 0; i < grassBlades; i++) {
                    const gx = (i * WIDTH / grassBlades + Math.sin(i * 7.3) * 8 + WIDTH * 0.03) % WIDTH;
                    const gy = GROUND_Y + 3 + Math.abs(Math.cos(i * 4.7)) * 14;
                    const gh = 8 + Math.abs(Math.sin(i * 3.1)) * 16;
                    const sway = Math.sin(i * 2.3 + performance.now() * 0.001) * 3;
                    ctx.strokeStyle = `rgba(${100 + Math.random()*40},${140 + Math.random()*50},${40 + Math.random()*35},0.55)`;
                    ctx.lineWidth = 1.2 + Math.random() * 0.8;
                    ctx.beginPath();
                    ctx.moveTo(gx, gy);
                    ctx.quadraticCurveTo(gx + sway, gy - gh * 0.6, gx + sway * 1.4, gy - gh);
                    ctx.stroke();
                }

                // 小花朵点缀
                for (let i = 0; i < 15; i++) {
                    const fx = (i * 53 + 17) % WIDTH;
                    const fy = GROUND_Y + 5 + (i * 31 % 28);
                    ctx.fillStyle = 'rgba(255,255,255,0.7)';
                    ctx.beginPath();
                    ctx.arc(fx, fy, 2.2, 0, Math.PI * 2);
                    ctx.fill();
                    ctx.fillStyle = 'rgba(255,240,150,0.8)';
                    ctx.beginPath();
                    ctx.arc(fx, fy, 1.1, 0, Math.PI * 2);
                    ctx.fill();
                }
            }

            function drawStickFigure(ctx, footY, isJumping) {
                const headRadius = 13;
                const headY = footY - 58 + headRadius;
                const neckY = headY + headRadius;
                const bodyBottom = footY - 16;
                const bodyTop = neckY;
                const bodyMid = (bodyTop + bodyBottom) / 2;
                const armY = bodyTop + 5;

                ctx.save();

                // 跳跃时的轻微旋转
                if (isJumping) {
                    const rotateAngle = -0.08;
                    ctx.translate(PLAYER_X, footY);
                    ctx.rotate(rotateAngle);
                    ctx.translate(-PLAYER_X, -footY);
                }

                // 阴影
                if (isOnGround && !isJumping) {
                    ctx.fillStyle = 'rgba(0,0,0,0.2)';
                    ctx.beginPath();
                    ctx.ellipse(PLAYER_X, footY + 1, 12, 4, 0, 0, Math.PI * 2);
                    ctx.fill();
                }

                // 腿部
                const legSpread = isJumping ? 7 : 10;
                ctx.strokeStyle = '#2c2c2c';
                ctx.lineWidth = 3.5;
                ctx.lineCap = 'round';
                // 左腿
                ctx.beginPath();
                ctx.moveTo(PLAYER_X, bodyBottom);
                ctx.lineTo(PLAYER_X - legSpread, footY);
                ctx.stroke();
                // 右腿
                ctx.beginPath();
                ctx.moveTo(PLAYER_X, bodyBottom);
                ctx.lineTo(PLAYER_X + legSpread, footY);
                ctx.stroke();

                // 身体
                ctx.strokeStyle = '#2c2c2c';
                ctx.lineWidth = 3.8;
                ctx.beginPath();
                ctx.moveTo(PLAYER_X, bodyTop);
                ctx.lineTo(PLAYER_X, bodyBottom);
                ctx.stroke();

                // 手臂
                const armLength = 16;
                const armAngle = isJumping ? -0.6 : 0.25;
                ctx.lineWidth = 3;
                // 左臂
                ctx.beginPath();
                ctx.moveTo(PLAYER_X, armY);
                ctx.lineTo(PLAYER_X - Math.cos(armAngle) * armLength, armY - Math.sin(armAngle) * armLength);
                ctx.stroke();
                // 右臂
                ctx.beginPath();
                ctx.moveTo(PLAYER_X, armY);
                ctx.lineTo(PLAYER_X + Math.cos(armAngle) * armLength, armY - Math.sin(armAngle) * armLength);
                ctx.stroke();

                // 鞋子
                ctx.fillStyle = '#3a3a3a';
                ctx.beginPath();
                ctx.arc(PLAYER_X - legSpread, footY - 1, 3.5, 0, Math.PI * 2);
                ctx.fill();
                ctx.beginPath();
                ctx.arc(PLAYER_X + legSpread, footY - 1, 3.5, 0, Math.PI * 2);
                ctx.fill();

                // 头部
                const headGrad = ctx.createRadialGradient(PLAYER_X - 2, headY - 3, 2, PLAYER_X, headY, headRadius);
                headGrad.addColorStop(0, '#fdf5e6');
                headGrad.addColorStop(0.7, '#f0d9b5');
                headGrad.addColorStop(1, '#d4a574');
                ctx.fillStyle = headGrad;
                ctx.beginPath();
                ctx.arc(PLAYER_X, headY, headRadius, 0, Math.PI * 2);
                ctx.fill();
                ctx.strokeStyle = '#2c2c2c';
                ctx.lineWidth = 2.2;
                ctx.stroke();

                // 眼睛
                ctx.fillStyle = '#1a1a1a';
                ctx.beginPath();
                ctx.arc(PLAYER_X - 4, headY - 3, 2.2, 0, Math.PI * 2);
                ctx.fill();
                ctx.beginPath();
                ctx.arc(PLAYER_X + 4, headY - 3, 2.2, 0, Math.PI * 2);
                ctx.fill();

                // 微笑
                ctx.strokeStyle = '#5a3a2a';
                ctx.lineWidth = 1.3;
                ctx.beginPath();
                ctx.arc(PLAYER_X, headY + 2, 5, 0.15 * Math.PI, 0.85 * Math.PI);
                ctx.stroke();

                // 跳跃时的汗滴
                if (isJumping && playerVelocityY < -100) {
                    ctx.fillStyle = 'rgba(150,200,255,0.7)';
                    ctx.beginPath();
                    ctx.arc(PLAYER_X + 16, headY - 6, 2.5, 0, Math.PI * 2);
                    ctx.fill();
                }

                ctx.restore();
            }

            function drawCactus(ctx, obs) {
                const cx = obs.x;
                const baseY = obs.y + obs.height;
                const h = obs.height;
                const w = obs.width;
                const halfW = w / 2;

                ctx.save();

                // 主体阴影
                ctx.fillStyle = 'rgba(0,0,0,0.15)';
                ctx.beginPath();
                ctx.roundRect(cx - halfW + 3, obs.y + 3, w, h, halfW * 0.8);
                ctx.fill();

                // 主体
                const bodyGrad = ctx.createLinearGradient(cx - halfW, 0, cx + halfW, 0);
                bodyGrad.addColorStop(0, '#4a8c3f');
                bodyGrad.addColorStop(0.35, '#5da04f');
                bodyGrad.addColorStop(0.65, '#4d9442');
                bodyGrad.addColorStop(1, '#35702b');
                ctx.fillStyle = bodyGrad;
                ctx.beginPath();
                ctx.roundRect(cx - halfW, obs.y, w, h, halfW * 0.8);
                ctx.fill();
                ctx.strokeStyle = '#2d5a22';
                ctx.lineWidth = 1.6;
                ctx.stroke();

                // 纹理竖线
                ctx.strokeStyle = 'rgba(255,255,255,0.1)';
                ctx.lineWidth = 1;
                for (let lx = cx - halfW * 0.4; lx <= cx + halfW * 0.4; lx += halfW * 0.5) {
                    ctx.beginPath();
                    ctx.moveTo(lx, obs.y + 4);
                    ctx.lineTo(lx, obs.y + h - 4);
                    ctx.stroke();
                }

                // 分支(较高的仙人掌才有)
                if (h >= 42) {
                    const branchY = obs.y + h * 0.35;
                    const branchLen = w * 0.7;
                    const branchH = h * 0.28;
                    // 左分支
                    drawCactusBranch(ctx, cx - halfW, branchY, -branchLen, branchH, bodyGrad);
                    // 右分支
                    if (h >= 52) {
                        const branchY2 = obs.y + h * 0.5;
                        drawCactusBranch(ctx, cx + halfW, branchY2, branchLen, branchH * 0.85, bodyGrad);
                    }
                }

                // 顶部小刺
                ctx.fillStyle = '#d4e8c0';
                for (let i = -1; i <= 1; i++) {
                    ctx.beginPath();
                    ctx.arc(cx + i * halfW * 0.5, obs.y - 1, 2, 0, Math.PI * 2);
                    ctx.fill();
                }

                // 小花(大型仙人掌)
                if (h >= 62) {
                    const flowerX = cx;
                    const flowerY = obs.y - 3;
                    ctx.fillStyle = '#ff6b8a';
                    ctx.beginPath();
                    ctx.arc(flowerX, flowerY, 4.5, 0, Math.PI * 2);
                    ctx.fill();
                    ctx.fillStyle = '#ffb347';
                    ctx.beginPath();
                    ctx.arc(flowerX, flowerY, 2.2, 0, Math.PI * 2);
                    ctx.fill();
                    // 花瓣
                    for (let p = 0; p < 5; p++) {
                        const angle = (p / 5) * Math.PI * 2;
                        ctx.fillStyle = '#ff8da8';
                        ctx.beginPath();
                        ctx.arc(flowerX + Math.cos(angle) * 4, flowerY + Math.sin(angle) * 4, 2.8, 0, Math.PI * 2);
                        ctx.fill();
                    }
                }

                ctx.restore();
            }

            function drawCactusBranch(ctx, startX, startY, len, h, bodyGrad) {
                ctx.fillStyle = '#4a8c3f';
                ctx.beginPath();
                ctx.roundRect(startX + (len > 0 ? 0 : len), startY, Math.abs(len), h, 7);
                ctx.fill();
                ctx.strokeStyle = '#2d5a22';
                ctx.lineWidth = 1.3;
                ctx.stroke();
                // 小臂延伸
                const tipX = startX + len;
                const tipY = startY + h / 2;
                ctx.fillStyle = '#5da04f';
                ctx.beginPath();
                ctx.roundRect(tipX - 4, tipY - h * 0.6, 8, h * 1.2, 5);
                ctx.fill();
                ctx.strokeStyle = '#2d5a22';
                ctx.lineWidth = 1;
                ctx.stroke();
            }

            function drawGameOverOverlay(ctx) {
                if (gameOverAlpha <= 0) return;

                // 暗色遮罩
                ctx.fillStyle = `rgba(0,0,0,${Math.min(0.55, gameOverAlpha)})`;
                ctx.fillRect(0, 0, WIDTH, HEIGHT);

                // 主面板
                const panelW = 340;
                const panelH = 190;
                const panelX = WIDTH / 2 - panelW / 2;
                const panelY = HEIGHT / 2 - panelH / 2 - 10;
                const alpha = Math.min(1, gameOverAlpha * 2);

                ctx.save();
                ctx.globalAlpha = alpha;

                // 面板背景
                const panelGrad = ctx.createLinearGradient(panelX, panelY, panelX, panelY + panelH);
                panelGrad.addColorStop(0, 'rgba(40,40,50,0.94)');
                panelGrad.addColorStop(1, 'rgba(25,25,35,0.96)');
                ctx.fillStyle = panelGrad;
                ctx.beginPath();
                ctx.roundRect(panelX, panelY, panelW, panelH, 20);
                ctx.fill();
                ctx.strokeStyle = 'rgba(255,255,255,0.3)';
                ctx.lineWidth = 2;
                ctx.stroke();

                // 标题
                ctx.fillStyle = '#ff6b6b';
                ctx.font = 'bold 34px "PingFang SC","Microsoft YaHei","Segoe UI",sans-serif';
                ctx.textAlign = 'center';
                ctx.fillText('游戏结束', WIDTH / 2, panelY + 52);

                // 分数
                ctx.fillStyle = '#ffffff';
                ctx.font = 'bold 22px "PingFang SC","Microsoft YaHei","Segoe UI",sans-serif';
                ctx.fillText(`🏆 最终得分:${Math.floor(score)}`, WIDTH / 2, panelY + 95);

                // 提示
                ctx.fillStyle = '#ffd700';
                ctx.font = '17px "PingFang SC","Microsoft YaHei","Segoe UI",sans-serif';
                const pulse = 0.7 + 0.3 * Math.sin(performance.now() * 0.005);
                ctx.globalAlpha = alpha * pulse;
                ctx.fillText('按 空格键 重新开始', WIDTH / 2, panelY + 140);

                ctx.restore();
            }

            function drawUI(ctx) {
                // 左上角分数
                const scoreX = 22;
                const scoreY = 38;

                // 分数背景
                ctx.fillStyle = 'rgba(0,0,0,0.45)';
                ctx.beginPath();
                ctx.roundRect(scoreX - 8, scoreY - 26, 155, 40, 22);
                ctx.fill();
                ctx.strokeStyle = 'rgba(255,255,255,0.35)';
                ctx.lineWidth = 1.5;
                ctx.stroke();

                // 分数文字
                ctx.fillStyle = '#ffffff';
                ctx.font = 'bold 18px "PingFang SC","Microsoft YaHei","Segoe UI",sans-serif';
                ctx.textAlign = 'left';
                ctx.fillText(`⭐ ${Math.floor(score)} 分`, scoreX + 6, scoreY + 1);
            }

            // --- 更新逻辑 ---
            function update(dt) {
                // 限制最大步长防止跳帧问题
                const clampedDt = Math.min(dt, 0.1);

                // 更新云朵
                for (const cloud of clouds) {
                    cloud.x -= cloud.speed * clampedDt;
                    if (cloud.x + cloud.w < -20) {
                        cloud.x = WIDTH + 20;
                        cloud.y = 25 + Math.random() * 130;
                    }
                }

                // 更新粒子
                updateParticles(clampedDt);

                // 屏幕震动衰减
                if (screenShake > 0) {
                    screenShake = Math.max(0, screenShake - clampedDt * 12);
                }

                // 游戏结束冷却
                if (restartCooldown > 0) {
                    restartCooldown = Math.max(0, restartCooldown - clampedDt);
                }

                if (gameState === STATE.PLAYING) {
                    // 计分
                    scoreAccumulator += clampedDt;
                    const prevScoreInt = Math.floor(score);
                    score = scoreAccumulator * SCORE_PER_SECOND;
                    if (Math.floor(score) > prevScoreInt && Math.floor(score) % 10 === 0 && Math.floor(score) > 0) {
                        sfxScore();
                    }

                    // 玩家物理
                    if (!isOnGround) {
                        playerVelocityY += GRAVITY_PER_SEC * clampedDt;
                        playerY += playerVelocityY * clampedDt;

                        // 落地检测
                        if (playerY >= GROUND_Y) {
                            playerY = GROUND_Y;
                            playerVelocityY = 0;
                            isOnGround = true;
                            // 落地粒子
                            spawnParticles(PLAYER_X, GROUND_Y, 8, 'rgb(180,200,160)', 6, 3, 0.4);
                        }
                    }

                    // 生成障碍物
                    spawnTimer -= clampedDt;
                    if (spawnTimer <= 0) {
                        spawnObstacle();
                        spawnTimer = MIN_SPAWN_INTERVAL + Math.random() * (MAX_SPAWN_INTERVAL - MIN_SPAWN_INTERVAL);
                    }

                    // 更新障碍物
                    for (const obs of obstacles) {
                        obs.x -= OBSTACLE_SPEED * clampedDt;
                    }

                    // 移除屏幕外的障碍物
                    while (obstacles.length > 0 && obstacles[0].x < -60) {
                        obstacles.shift();
                    }

                    // 碰撞检测
                    const playerBox = getPlayerCollisionBox();
                    for (const obs of obstacles) {
                        if (checkCollision(playerBox, obs)) {
                            // 游戏结束
                            gameState = STATE.GAME_OVER;
                            gameOverAlpha = 0;
                            screenShake = 0.5;
                            restartCooldown = 0.5;
                            sfxHit();
                            spawnParticles(PLAYER_X, playerY - 20, 25, 'rgb(255,100,80)', 8, 6, 0.7);
                            spawnParticles(PLAYER_X, playerY - 20, 15, 'rgb(255,200,50)', 6, 5, 0.5);
                            hintBar.style.opacity = '1';
                            hintBar.textContent = '💥 游戏结束!按 空格键 重新开始';
                            break;
                        }
                    }
                }

                // 游戏结束动画
                if (gameState === STATE.GAME_OVER && gameOverAlpha < 1.5) {
                    gameOverAlpha += clampedDt * 1.8;
                }
            }

            // --- 渲染 ---
            function render(ctx) {
                ctx.clearRect(0, 0, WIDTH, HEIGHT);

                // 屏幕震动偏移
                let shakeX = 0;
                let shakeY = 0;
                if (screenShake > 0) {
                    shakeX = (Math.random() - 0.5) * screenShake * 18;
                    shakeY = (Math.random() - 0.5) * screenShake * 14;
                }

                ctx.save();
                ctx.translate(shakeX, shakeY);

                drawSky(ctx);
                drawClouds(ctx);
                drawGround(ctx);

                // 绘制障碍物
                for (const obs of obstacles) {
                    drawCactus(ctx, obs);
                }

                // 绘制玩家
                const isJumping = !isOnGround || playerY < GROUND_Y;
                drawStickFigure(ctx, playerY, isJumping);

                // 绘制粒子
                drawParticles(ctx);

                ctx.restore();

                // UI(不受震动影响)
                drawUI(ctx);
                drawGameOverOverlay(ctx);

                // 等待状态提示
                if (gameState === STATE.WAITING) {
                    const pulse = 0.6 + 0.4 * Math.sin(performance.now() * 0.004);
                    ctx.fillStyle = `rgba(0,0,0,0.5)`;
                    ctx.font = 'bold 20px "PingFang SC","Microsoft YaHei","Segoe UI",sans-serif';
                    ctx.textAlign = 'center';
                    ctx.fillText('👆 点击屏幕 或按 空格键 开始游戏', WIDTH / 2, HEIGHT / 2 - 15);
                    ctx.fillStyle = `rgba(255,255,255,${0.6 + 0.4 * Math.sin(performance.now()*0.005)})`;
                    ctx.font = '15px "PingFang SC","Microsoft YaHei","Segoe UI",sans-serif';
                    ctx.fillText('跳过仙人掌,存活越久分数越高!', WIDTH / 2, HEIGHT / 2 + 25);
                }
            }

            // --- 游戏循环 ---
            function gameLoop(timestamp) {
                const dt = (timestamp - lastTime) / 1000; // 转换为秒
                lastTime = timestamp;

                if (dt > 0 && dt < 0.5) {
                    update(dt);
                }
                render(ctx);

                requestAnimationFrame(gameLoop);
            }

            // --- 输入处理 ---
            function handleJump() {
                if (gameState === STATE.WAITING) {
                    resetGame();
                    gameState = STATE.PLAYING;
                    hintBar.style.opacity = '0';
                    // 初始跳跃
                    if (isOnGround) {
                        playerVelocityY = JUMP_VELOCITY;
                        isOnGround = false;
                        sfxJump();
                        spawnParticles(PLAYER_X, GROUND_Y, 6, 'rgb(200,220,240)', 5, 4, 0.35);
                    }
                    return;
                }

                if (gameState === STATE.PLAYING) {
                    if (isOnGround) {
                        playerVelocityY = JUMP_VELOCITY;
                        isOnGround = false;
                        sfxJump();
                        spawnParticles(PLAYER_X, GROUND_Y, 6, 'rgb(200,220,240)', 5, 4, 0.35);
                    }
                    return;
                }

                if (gameState === STATE.GAME_OVER && restartCooldown <= 0) {
                    resetGame();
                    hintBar.style.opacity = '0';
                    return;
                }
            }

            function handleRestart() {
                if (gameState === STATE.GAME_OVER && restartCooldown <= 0) {
                    resetGame();
                    hintBar.style.opacity = '0';
                }
            }

            // 键盘事件
            window.addEventListener('keydown', (e) => {
                if (e.code === 'Space' || e.code === 'KeyW' || e.code === 'ArrowUp') {
                    e.preventDefault();
                    if (gameState === STATE.GAME_OVER) {
                        handleRestart();
                    } else {
                        handleJump();
                    }
                }
            });

            // 鼠标点击
            canvas.addEventListener('click', (e) => {
                e.preventDefault();
                if (gameState === STATE.GAME_OVER) {
                    handleRestart();
                } else {
                    handleJump();
                }
            });

            // 触摸事件(移动端)
            canvas.addEventListener('touchstart', (e) => {
                e.preventDefault();
                if (gameState === STATE.GAME_OVER) {
                    handleRestart();
                } else {
                    handleJump();
                }
            }, { passive: false });

            // 双击防止缩放
            canvas.addEventListener('dblclick', (e) => {
                e.preventDefault();
            });

            // --- 初始绘制 ---
            function drawInitialFrame() {
                ctx.clearRect(0, 0, WIDTH, HEIGHT);
                drawSky(ctx);
                drawClouds(ctx);
                drawGround(ctx);
                drawStickFigure(ctx, GROUND_Y, false);
                drawUI(ctx);
                // 等待提示
                ctx.fillStyle = 'rgba(0,0,0,0.5)';
                ctx.font = 'bold 20px "PingFang SC","Microsoft YaHei","Segoe UI",sans-serif';
                ctx.textAlign = 'center';
                ctx.fillText('👆 点击屏幕 或按 空格键 开始游戏', WIDTH / 2, HEIGHT / 2 - 15);
                ctx.fillStyle = 'rgba(255,255,255,0.7)';
                ctx.font = '15px "PingFang SC","Microsoft YaHei","Segoe UI",sans-serif';
                ctx.fillText('跳过仙人掌,存活越久分数越高!', WIDTH / 2, HEIGHT / 2 + 25);
            }

            // --- 启动 ---
            drawInitialFrame();
            hintBar.style.opacity = '1';
            hintBar.textContent = '⌨️ 空格键 或 🖱️ 点击屏幕跳跃';
            lastTime = performance.now();
            requestAnimationFrame(gameLoop);

            console.log('🏃 简笔画小人跑酷游戏已就绪!');
            console.log('   - 按空格键或点击屏幕跳跃');
            console.log('   - 躲避仙人掌障碍物');
            console.log('   - 每存活1秒获得10分');
            console.log('   - 游戏结束后按空格键重新开始');
            console.log('   🎮 祝你玩得开心!');
        })();
    </script>
</body>
</html>

Game Source: 🏃 简笔画小人跑酷 - 跳跃游戏

Creator: RocketTiger92

Libraries: none

Complexity: complex (991 lines, 38.7 KB)

The full source code is displayed above on this page.

Remix Instructions

To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: game-rockettiger92" to link back to the original. Then publish at arcadelab.ai/publish.