🎮ArcadeLab

菲比丘比&弗糯糯跑酷 | 鸣潮二创

by ThunderKnight37
347 lines11.3 KB
▶ Play
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>菲比丘比&弗糯糯跑酷 | 鸣潮二创</title>
<style>
*{margin:0;padding:0;box-sizing:border-box;font-family:system-ui,sans-serif}
body{background:#1a1a2e;overflow:hidden;color:#fff;height:100vh}
#container{width:100%;height:100%;position:relative}
canvas{display:block;background:linear-gradient(#415a77,#778da9)}

/* 角色选择界面 */
.select-screen{
    position:absolute;inset:0;background:rgba(0,0,0,0.85);
    display:flex;flex-direction:column;align-items:center;justify-content:center;
    z-index:100;gap:24px
}
.select-title{font-size:28px;font-weight:bold;color:#ffe699}
.char-wrap{display:flex;gap:20px;flex-wrap:wrap;justify-content:center;padding:10px}
.char-card{
    width:160px;padding:16px;border-radius:14px;border:3px solid transparent;
    background:#2b2d42;cursor:pointer;text-align:center;transition:0.25s
}
.char-card:hover{transform:scale(1.05)}
.char-card.selected{border-color:#ffd166;background:#3a3d5c}
.char-avatar{width:80px;height:80px;border-radius:50%;margin:0 auto 10px}
.phoebe{background:radial-gradient(circle,#ffe8a8,#ffb964)}
.nuonuo{background:radial-gradient(circle,#c9d6ff,#7289da)}
.char-name{font-size:18px;margin-bottom:6px}
.char-desc{font-size:12px;color:#ccc;line-height:1.4}
.start-btn{
    padding:12px 32px;font-size:20px;border:none;border-radius:10px;
    background:#ff9f1c;color:#000;font-weight:bold;cursor:pointer;
    display:none
}

/* 游戏UI */
.game-ui{position:absolute;top:12px;left:12px;z-index:10}
.score-text,.high-text{font-size:20px;text-shadow:0 0 4px #000}
.tip{position:absolute;bottom:16px;left:50%;transform:translateX(-50%);opacity:0.8;font-size:14px}
.over-panel{
    position:absolute;inset:0;background:rgba(0,0,0,0.75);
    display:none;flex-direction:column;align-items:center;justify-content:center;
    gap:16px;z-index:20
}
.over-title{font-size:32px;color:#ff5a5f}
.restart-btn,back-btn{
    padding:10px 24px;font-size:18px;border-radius:8px;border:none;cursor:pointer;margin:4px
}
.restart-btn{background:#4cc9f0;color:#000}
.back-btn{background:#adb5bd;color:#000}
</style>
</head>
<body>
<div id="container">
    <canvas id="gameCanvas"></canvas>
    <div class="game-ui">
        <div class="score-text">分数:<span id="score">0</span></div>
        <div class="high-text">最高分:<span id="high">0</span></div>
    </div>
    <div class="tip">空格 / 点击屏幕跳跃</div>

    <!-- 角色选择 -->
    <div class="select-screen" id="selectScreen">
        <div class="select-title">选择你的主角</div>
        <div class="char-wrap">
            <div class="char-card" data-char="phoebe" id="card-phoebe">
                <div class="char-avatar phoebe"></div>
                <div class="char-name">菲比丘比</div>
                <div class="char-desc">菲比啾比~ 蹦蹦跳跳向前冲!</div>
            </div>
            <div class="char-card" data-char="nuonuo" id="card-nuonuo">
                <div class="char-avatar nuonuo"></div>
                <div class="char-name">弗糯糯</div>
                <div class="char-desc">呜...小心不要撞到障碍物</div>
            </div>
        </div>
        <button class="start-btn" id="startBtn">开始跑酷!</button>
    </div>

    <!-- 结束面板 -->
    <div class="over-panel" id="overPanel">
        <div class="over-title">游戏结束</div>
        <div>本次分数:<span id="finalScore">0</span></div>
        <div>最高分:<span id="finalHigh">0</span></div>
        <div>
            <button class="restart-btn" id="restartBtn">再来一局</button>
            <button class="back-btn" id="backBtn">返回选角</button>
        </div>
    </div>
</div>

<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
function resize(){
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
}
resize();
window.addEventListener('resize',resize);

// ========== 音频系统(WebAudio合成角色专属音效,无需外部音频文件) ==========
let audioCtx;
function initAudio(){
    audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
// 菲比丘比音效:轻快啾比声
function playPhoebeSound(){
    if(!audioCtx) return;
    const osc = audioCtx.createOscillator();
    const gain = audioCtx.createGain();
    osc.type = 'sine';
    osc.frequency.setValueAtTime(720, audioCtx.currentTime);
    osc.frequency.exponentialRampToValueAtTime(950, audioCtx.currentTime+0.12);
    gain.gain.setValueAtTime(0.4, audioCtx.currentTime);
    gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime+0.22);
    osc.connect(gain); gain.connect(audioCtx.destination);
    osc.start(); osc.stop(audioCtx.currentTime+0.22);
}
// 弗糯糯音效:低沉委屈软糯音
function playNuonuoSound(){
    if(!audioCtx) return;
    const osc = audioCtx.createOscillator();
    const gain = audioCtx.createGain();
    osc.type = 'triangle';
    osc.frequency.setValueAtTime(320, audioCtx.currentTime);
    osc.frequency.exponentialRampToValueAtTime(240, audioCtx.currentTime+0.25);
    gain.gain.setValueAtTime(0.35, audioCtx.currentTime);
    gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime+0.3);
    osc.connect(gain); gain.connect(audioCtx.destination);
    osc.start(); osc.stop(audioCtx.currentTime+0.3);
}
// 跳跃通用音效
function playJumpSfx(){
    if(!audioCtx) return;
    const osc = audioCtx.createOscillator();
    const gain = audioCtx.createGain();
    osc.type='square';
    osc.frequency.setValueAtTime(300,audioCtx.currentTime);
    osc.frequency.exponentialRampToValueAtTime(600,audioCtx.currentTime+0.08);
    gain.gain.setValueAtTime(0.25,audioCtx.currentTime);
    gain.gain.exponentialRampToValueAtTime(0.001,audioCtx.currentTime+0.12);
    osc.connect(gain); gain.connect(audioCtx.destination);
    osc.start(); osc.stop(audioCtx.currentTime+0.12);
}
// 碰撞失败音效
function playHitSfx(){
    if(!audioCtx) return;
    const osc = audioCtx.createOscillator();
    const gain = audioCtx.createGain();
    osc.type='sawtooth';
    osc.frequency.setValueAtTime(220,audioCtx.currentTime);
    osc.frequency.exponentialRampToValueAtTime(80,audioCtx.currentTime+0.3);
    gain.gain.setValueAtTime(0.4,audioCtx.currentTime);
    gain.gain.exponentialRampToValueAtTime(0.001,audioCtx.currentTime+0.35);
    osc.connect(gain); gain.connect(audioCtx.destination);
    osc.start(); osc.stop(audioCtx.currentTime+0.35);
}

// ========== 游戏变量 ==========
let selectedChar = null;
let score = 0;
let highScore = +localStorage.getItem('phoebeNuonuoRunHigh') || 0;
let gameRunning = false;
let player, obstacles = [];
let speed = 6;
const gravity = 0.6;
const jumpForce = -14;
let groundY;

// 玩家类
class Player{
    constructor(charType){
        this.w = 44; this.h = 44;
        this.x = 100;
        this.y = 0;
        this.vy = 0;
        this.isJumping = false;
        this.type = charType;
        this.color = charType==='phoebe' ? '#ffd073' : '#94a8e8';
    }
    jump(){
        if(!this.isJumping){
            this.vy = jumpForce;
            this.isJumping = true;
            playJumpSfx();
            // 跳跃时播放角色专属台词音效
            if(this.type==='phoebe') playPhoebeSound();
            else playNuonuoSound();
        }
    }
    update(){
        this.vy += gravity;
        this.y += this.vy;
        if(this.y >= groundY - this.h){
            this.y = groundY - this.h;
            this.vy = 0;
            this.isJumping = false;
        }
    }
    draw(){
        ctx.beginPath();
        ctx.arc(this.x+this.w/2, this.y+this.h/2, this.w/2, 0, Math.PI*2);
        ctx.fillStyle = this.color;
        ctx.fill();
        // 眼睛
        ctx.fillStyle="#222";
        ctx.beginPath();
        ctx.arc(this.x+14,this.y+16,4,0,Math.PI*2);
        ctx.arc(this.x+30,this.y+16,4,0,Math.PI*2);
        ctx.fill();
    }
}
class Obstacle{
    constructor(){
        this.w = 32; this.h = 50 + Math.random()*25;
        this.x = canvas.width + 20;
        this.y = groundY - this.h;
    }
    update(){
        this.x -= speed;
    }
    draw(){
        ctx.fillStyle='#444654';
        ctx.fillRect(this.x,this.y,this.w,this.h);
    }
    offScreen(){
        return this.x + this.w < 0;
    }
    checkHit(p){
        return p.x < this.x+this.w && p.x+p.w > this.x && p.y < this.y+this.h && p.y+p.h>this.y;
    }
}

// ========== 界面元素 ==========
const selectScreen = document.getElementById('selectScreen');
const overPanel = document.getElementById('overPanel');
const startBtn = document.getElementById('startBtn');
const restartBtn = document.getElementById('restartBtn');
const backBtn = document.getElementById('backBtn');
const scoreDom = document.getElementById('score');
const highDom = document.getElementById('high');
const finalScoreDom = document.getElementById('finalScore');
const finalHighDom = document.getElementById('finalHigh');
highDom.textContent = highScore;

// 选角点击
document.querySelectorAll('.char-card').forEach(card=>{
    card.onclick = ()=>{
        document.querySelectorAll('.char-card').forEach(c=>c.classList.remove('selected'));
        card.classList.add('selected');
        selectedChar = card.dataset.char;
        startBtn.style.display='block';
    }
})
startBtn.onclick = ()=>{
    initAudio();
    startGame();
}
restartBtn.onclick = startGame;
backBtn.onclick = ()=>{
    gameRunning = false;
    overPanel.style.display='none';
    selectScreen.style.display='flex';
}

function startGame(){
    selectScreen.style.display='none';
    overPanel.style.display='none';
    score = 0; speed = 6; obstacles = [];
    groundY = canvas.height - 80;
    player = new Player(selectedChar);
    gameRunning = true;
    requestAnimationFrame(gameLoop);
}
function gameOver(){
    gameRunning = false;
    playHitSfx();
    if(score>highScore){
        highScore = score;
        localStorage.setItem('phoebeNuonuoRunHigh',highScore);
    }
    finalScoreDom.textContent = score;
    finalHighDom.textContent = highScore;
    highDom.textContent = highScore;
    overPanel.style.display='flex';
}

// 输入监听
function doJump(){
    if(gameRunning && player) player.jump();
}
document.addEventListener('keydown',e=>{
    if(e.code==='Space'){
        e.preventDefault(); doJump();
    }
})
canvas.addEventListener('click',doJump);
canvas.addEventListener('touchstart',(e)=>{
    e.preventDefault(); doJump();
})

// 生成障碍物定时器
let spawnTimer = 0;
function spawnObstacle(){
    obstacles.push(new Obstacle());
}

function gameLoop(){
    if(!gameRunning) return;
    ctx.clearRect(0,0,canvas.width,canvas.height);
    // 地面
    ctx.fillStyle='#333542';
    ctx.fillRect(0,groundY,canvas.width,canvas.height-groundY);

    player.update();
    player.draw();

    spawnTimer++;
    const spawnRate = Math.max(70, 120 - speed*4);
    if(spawnTimer>spawnRate){
        spawnObstacle();
        spawnTimer=0;
    }
    for(let i=obstacles.length-1;i>=0;i--){
        const obs = obstacles[i];
        obs.update();
        obs.draw();
        if(obs.checkHit(player)){
            gameOver();
            return;
        }
        if(obs.offScreen()){
            obstacles.splice(i,1);
            score += 10;
            scoreDom.textContent = score;
            speed += 0.04; // 逐渐加速
        }
    }
    requestAnimationFrame(gameLoop);
}
</script>
</body>
</html>

Game Source: 菲比丘比&弗糯糯跑酷 | 鸣潮二创

Creator: ThunderKnight37

Libraries: none

Complexity: complex (347 lines, 11.3 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-thunderknight37" to link back to the original. Then publish at arcadelab.ai/publish.