🎮ArcadeLab

溶洞像素跳跃

by MysticMaker96
369 lines8.7 KB
▶ Play
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>溶洞像素跳跃</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{background:#181412;color:#fff;font-family:monospace;overflow:hidden}
canvas{display:block;margin:0 auto;background:#2c2420;image-rendering:pixelated;}
.info{position:absolute;top:8px;left:12px;font-size:18px;text-shadow:1px 1px #000}
</style>
</head>
<body>
<div class="info">
💰金币:<span id="coinCount">0</span> &nbsp;&nbsp; ⭐分数:<span id="score">0</span>
</div>
<canvas id="game" width="480" height="320"></canvas>

<script>
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const coinDom = document.getElementById('coinCount');
const scoreDom = document.getElementById('score');

function drawPixel(x,y,w,h,color){
    ctx.fillStyle = color;
    ctx.fillRect(Math.floor(x),Math.floor(y),w,h);
}

// 玩家:紫色像素小人,结构更清楚
const player = {
    x: 60,
    y: 200,
    w: 14,
    h: 22,
    vx: 2.6,
    vy: 0,
    jumpPower: -16,
    gravity: 0.48,
    onGround: false,
    pixelSize: 3,
    runPhase: 0,
    flip: 1,
    coinNum: 0,
    score: 0
};

const groundY = 260;
let spikes = [];
let coins = [];

// 尖刺:加高,保证高于小人
function spawnSpike(){
    const h = 44 + Math.random() * 16;
    spikes.push({
        x: canvas.width,
        y: groundY - h,
        w: 14,
        h: h,
        scored: false
    });
}

// 普通金币
function spawnCoin(){
    coins.push({
        x: canvas.width,
        y: groundY - 36 - Math.random()*55,
        r: 5,
        collected: false
    });
}

// 大片金币区域
function spawnCoinWave(){
    const count = 5 + Math.floor(Math.random()*4);
    const startY = groundY - 40 - Math.random()*40;
    for(let i=0;i<count;i++){
        coins.push({
            x: canvas.width + i*22,
            y: startY + Math.sin(i*0.6)*10,
            r: 5,
            collected: false
        });
    }
}

// 点击画布任意位置跳跃
canvas.addEventListener('click', ()=>{
    if(player.onGround){
        player.vy = player.jumpPower;
        player.onGround = false;
    }
});

canvas.addEventListener('touchstart',e=>{
    e.preventDefault();
    if(player.onGround){
        player.vy = player.jumpPower;
        player.onGround = false;
    }
});

let spawnTimer = 0;
const spawnRate = 110;
const moveSpeed = 3;
let coinWaveTimer = 0;
const coinWaveRate = 320;

function resetGame(){
    player.x = 60;
    player.y = 200;
    player.vy = 0;
    player.onGround = false;
    player.coinNum = 0;
    player.score = 0;
    player.runPhase = 0;
    spikes = [];
    coins = [];
    spawnTimer = 0;
    coinWaveTimer = 0;
    updateUI();
}

function updateUI(){
    coinDom.textContent = player.coinNum;
    scoreDom.textContent = Math.floor(player.score);
}

function rectOverlap(a,b){
    return a.x < b.x + b.w && a.x+a.w > b.x && a.y < b.y + b.h && a.y+a.h > b.y;
}

function circleRect(c, r, rect){
    let cx = Math.max(rect.x, Math.min(c.x, rect.x+rect.w));
    let cy = Math.max(rect.y, Math.min(c.y, rect.y+rect.h));
    let dx = c.x - cx;
    let dy = c.y - cy;
    return dx*dx + dy*dy < r*r;
}

function update(){
    spawnTimer++;
    coinWaveTimer++;

    if(spawnTimer >= spawnRate){
        spawnTimer = 0;
        if(Math.random()<0.8) spawnSpike();
        if(Math.random()<0.28) spawnCoin();
    }

    if(coinWaveTimer >= coinWaveRate){
        coinWaveTimer = 0;
        spawnCoinWave();
    }

    player.vy += player.gravity;
    player.y += player.vy;

    if(player.y + player.h*player.pixelSize >= groundY){
        player.y = groundY - player.h*player.pixelSize;
        player.vy = 0;
        player.onGround = true;
    }else{
        player.onGround = false;
    }

    // 跑步相位只在地面累积
    if(player.onGround){
        player.runPhase += 0.35;
    }

    // 移动尖刺和金币
    for(let i=spikes.length-1;i>=0;i--){
        spikes[i].x -= moveSpeed;
        if(spikes[i].x + spikes[i].w < 0) spikes.splice(i,1);
    }

    for(let i=coins.length-1;i>=0;i--){
        coins[i].x -= moveSpeed;
        if(coins[i].x + coins[i].r < 0) coins.splice(i,1);
    }

    // 碰撞尖刺
    const playerBox = {
        x: player.x+2,
        y: player.y+2,
        w: player.w*player.pixelSize-4,
        h: player.h*player.pixelSize-4
    };

    for(let s of spikes){
        if(rectOverlap(playerBox, s)){
            resetGame();
            return;
        }

        if(!s.scored && s.x + s.w/2 < player.x){
            s.scored = true;
            player.score += 1;
            updateUI();
        }
    }

    // 吃金币
    for(let i=coins.length-1;i>=0;i--){
        const c = coins[i];
        if(!c.collected && circleRect(c, c.r, playerBox)){
            c.collected = true;
            player.coinNum++;
            player.score += 10;
            coins.splice(i,1);
            updateUI();
        }
    }

    player.score += 0.02;
}

function drawPlayer(){
    const px = player.pixelSize;

    const cx = player.x + player.w*px/2;
    const cy = player.y;

    ctx.save();
    ctx.translate(cx, cy);
    ctx.scale(player.flip,1);
    ctx.translate(-player.w*px/2,0);

    let leftLegAngle, rightLegAngle;
    let leftArmAngle, rightArmAngle;

    if(player.onGround){
        leftLegAngle = Math.sin(player.runPhase) * 0.35;
        rightLegAngle = Math.sin(player.runPhase + Math.PI) * 0.35;
        leftArmAngle = Math.sin(player.runPhase + Math.PI) * 0.3;
        rightArmAngle = Math.sin(player.runPhase) * 0.3;
    }else{
        const t = Math.min(1, -player.vy / 16);
        leftLegAngle = -0.4 + t * 0.2;
        rightLegAngle = -0.4 + t * 0.2;
        leftArmAngle = 0.9 - t * 0.3;
        rightArmAngle = 0.9 - t * 0.3;
    }

    // 头
    drawPixel(4*px, 0, 6*px, 4*px, '#ffd0b0');
    drawPixel(5*px, 1*px, 2*px, 1*px, '#000000');
    drawPixel(7*px, 1*px, 2*px, 1*px, '#000000');
    drawPixel(4*px, 2*px, 1*px, 1*px, '#ffffff');
    drawPixel(9*px, 2*px, 1*px, 1*px, '#ffffff');

    // 身子
    drawPixel(4*px, 4*px, 6*px, 5*px, '#9b59b6');
    drawPixel(4*px, 7*px, 6*px, 2*px, '#8e44ad');
    drawPixel(5*px, 5*px, 2*px, 1*px, '#b066ff');

    // 左腿
    ctx.save();
    ctx.translate(5*px, 9*px);
    ctx.rotate(leftLegAngle);
    drawPixel(0, 0, 2*px, 4*px, '#7d3fbf');
    ctx.restore();

    // 右腿
    ctx.save();
    ctx.translate(7*px, 9*px);
    ctx.rotate(rightLegAngle);
    drawPixel(0, 0, 2*px, 4*px, '#7d3fbf');
    ctx.restore();

    // 左臂
    ctx.save();
    ctx.translate(3*px, 5*px);
    ctx.rotate(leftArmAngle);
    drawPixel(0, 0, 2*px, 3*px, '#8e44ad');
    ctx.restore();

    // 右臂
    ctx.save();
    ctx.translate(10*px, 5*px);
    ctx.rotate(rightArmAngle);
    drawPixel(0, 0, 2*px, 3*px, '#8e44ad');
    ctx.restore();

    ctx.restore();

    // ===== 筋斗云,画在小人脚下 =====
    const feetY = player.y + player.h * px;
    const cloudWobble = Math.sin(player.runPhase * 0.6) * 1.2;
    const cloudX = cx;
    const cloudY = feetY + 2;

    ctx.fillStyle = '#ffffff';
    // 云朵轮廓
    drawPixel(cloudX - 21, cloudY + cloudWobble, 10, 5, '#fff');
    drawPixel(cloudX - 12, cloudY - 2 + cloudWobble, 24, 7, '#fff');
    drawPixel(cloudX + 9, cloudY + cloudWobble, 10, 5, '#fff');

    // 浅蓝光晕
    ctx.fillStyle = 'rgba(200, 230, 255, 0.45)';
    drawPixel(cloudX - 16, cloudY + 4 + cloudWobble, 32, 3, '#c8e6ff');

    // 跑动尾迹
    if (player.onGround) {
        ctx.fillStyle = 'rgba(255,255,255,0.35)';
        drawPixel(cloudX - 30, cloudY + 2 + cloudWobble, 5, 2, '#fff');
        drawPixel(cloudX - 36, cloudY + 4 + cloudWobble, 3, 2, '#fff');
    }
}

function drawSpikes(){
    for(let s of spikes){
        ctx.fillStyle='#70482c';
        ctx.fillRect(s.x, s.y, s.w, s.h);

        ctx.beginPath();
        ctx.moveTo(s.x, s.y);
        ctx.lineTo(s.x + s.w/2, s.y - 10);
        ctx.lineTo(s.x + s.w, s.y);
        ctx.fill();

        ctx.fillStyle='#543420';
        ctx.fillRect(s.x+2, s.y+4, 3, s.h-4);
        ctx.fillRect(s.x+8, s.y+10, 3, s.h-10);
    }
}

function drawCoins(){
    for(let c of coins){
        ctx.fillStyle='#ffd700';
        ctx.beginPath();
        ctx.arc(c.x, c.y, c.r, 0, Math.PI*2);
        ctx.fill();

        ctx.fillStyle='#ffb800';
        ctx.beginPath();
        ctx.arc(c.x-1, c.y-1, c.r*0.45, 0, Math.PI*2);
        ctx.fill();
    }
}

function draw(){
    ctx.clearRect(0,0,canvas.width,canvas.height);

    ctx.fillStyle='#4b382b';
    ctx.fillRect(0, groundY, canvas.width, canvas.height-groundY);

    ctx.fillStyle='#3a2a20';
    for(let i=0;i<canvas.width;i+=16){
        ctx.fillRect(i, groundY, 4, 4);
    }

    drawSpikes();
    drawCoins();
    drawPlayer();
}

function loop(){
    update();
    draw();
    requestAnimationFrame(loop);
}
loop();
</script>
</body>
</html>

Game Source: 溶洞像素跳跃

Creator: MysticMaker96

Libraries: none

Complexity: complex (369 lines, 8.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-mysticmaker96-mw4q56gu" to link back to the original. Then publish at arcadelab.ai/publish.