🎮ArcadeLab

泡泡速爆|休闲解压小游戏

by CrystalPenguin76
558 lines18.9 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;-webkit-tap-highlight-color:transparent;}
html,body{height:100%;overflow:hidden;font-family:system-ui,sans-serif;color:#fff;}
#gameWrap{width:100vw;height:100vh;display:flex;flex-direction:column;}
#topBar{height:80px;padding:12px 16px;display:flex;justify-content:space-between;align-items:center;background:#0f1a42;gap:8px;flex-shrink:0;position:relative;z-index:10;}
.infoItem{text-align:center;flex:1;}
.infoLabel{font-size:14px;opacity:0.8;display:block;}
.infoVal{font-size:24px;font-weight:bold;margin-top:4px;}
#gameCanvas{flex:1;width:100%;display:block;background:linear-gradient(180deg,#102060 0%,#050c28 100%);position:relative;}
/* 中央连击文字 */
.comboCenterText{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);pointer-events:none;font-weight:bold;opacity:0.85;text-shadow:0 0 14px #fff;z-index:5;}
/* 新手引导遮罩 */
#guideMask{position:fixed;inset:0;background:rgba(0,0,0,0.75);display:none;align-items:center;justify-content:center;z-index:98;padding:24px;text-align:center;}
.guideBox{background:rgba(40,70,160,0.9);border-radius:20px;padding:32px 24px;border:2px solid #70b8ff;max-width:320px;width:100%;}
.guideText{font-size:20px;line-height:1.6;color:#fff;}
.guideTip{margin-top:16px;font-size:15px;opacity:0.75;}
/* 结算弹窗 */
#modalMask{position:fixed;inset:0;background:rgba(0,0,0,0.88);display:none;align-items:center;justify-content:center;padding:20px;z-index:99;}
#modalBox{background:#122055;width:100%;max-width:360px;border-radius:24px;padding:36px 28px;text-align:center;border:2px solid #4499ff;}
.modalTitle{font-size:28px;margin-bottom:28px;color:#ffdd44;}
.modalRow{margin:16px 0;font-size:19px;}
.modalStrong{font-size:28px;font-weight:bold;color:#88ddff;}
.modalBtnGroup{margin-top:32px;display:flex;flex-direction:column;gap:16px;}
.modalBtn{padding:16px 0;font-size:21px;border:none;border-radius:99px;font-weight:bold;width:100%;}
#restartBtn{background:#4488ff;color:#fff;}
#shareBtn{background:#22bb77;color:#fff;}
.modalBtn:active{opacity:0.8;}
</style>
</head>
<body>
<div id="gameWrap">
    <div id="topBar">
        <div class="infoItem">
            <span class="infoLabel">剩余时间</span>
            <span class="infoVal" id="timeText">60</span>
        </div>
        <div class="infoItem">
            <span class="infoLabel">得分</span>
            <span class="infoVal" id="scoreText">0</span>
        </div>
        <div class="infoItem">
            <span class="infoLabel">最高连击</span>
            <span class="infoVal" id="maxComboText">0</span>
        </div>
    </div>
    <canvas id="gameCanvas"></canvas>
    <div class="comboCenterText" id="centerCombo"></div>
</div>

<!-- 新手引导 -->
<div id="guideMask">
    <div class="guideBox">
        <div class="guideText">休闲解压小游戏<br>点击泡泡轻松得分,连击更舒服~</div>
        <div class="guideTip">点击任意位置关闭提示</div>
    </div>
</div>

<!-- 结算弹窗 -->
<div id="modalMask">
    <div id="modalBox">
        <h2 class="modalTitle">本局结束,放松一下</h2>
        <div class="modalRow">本局得分:<span class="modalStrong" id="endScore">0</span></div>
        <div class="modalRow">最高连击:<span class="modalStrong" id="endMaxCombo">0</span></div>
        <div class="modalRow">历史最高分:<span class="modalStrong" id="bestScore">0</span></div>
        <div class="modalBtnGroup">
            <button class="modalBtn" id="restartBtn">再来一局放松</button>
            <button class="modalBtn" id="shareBtn">分享我的分数</button>
        </div>
    </div>
</div>

<script>
// ========== 音频系统 分层音效 ==========
let audioCtx = null;
function initAudio(){
    if(!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
// 普通啵啵爆破音
function playPopSound(){
    initAudio();
    const osc = audioCtx.createOscillator();
    const gain = audioCtx.createGain();
    osc.connect(gain);
    gain.connect(audioCtx.destination);
    const now = audioCtx.currentTime;
    osc.type = 'sine';
    osc.frequency.setValueAtTime(620, now);
    osc.frequency.exponentialRampToValueAtTime(100, now+0.13);
    gain.gain.setValueAtTime(0.28, now);
    gain.gain.exponentialRampToValueAtTime(0.001, now+0.13);
    osc.start(now);
    osc.stop(now+0.13);
}
// 炸弹低沉嗡声(削弱音量,不刺耳)
function playBombSound(){
    initAudio();
    const osc = audioCtx.createOscillator();
    const gain = audioCtx.createGain();
    osc.connect(gain);
    gain.connect(audioCtx.destination);
    const now = audioCtx.currentTime;
    osc.type = 'sawtooth';
    osc.frequency.setValueAtTime(160, now);
    osc.frequency.exponentialRampToValueAtTime(35, now+0.35);
    gain.gain.setValueAtTime(0.25, now);
    gain.gain.exponentialRampToValueAtTime(0.001, now+0.35);
    osc.start(now);
    osc.stop(now+0.35);
}
// 连击5/10/20 轻柔叮声
function playComboTipSound(){
    initAudio();
    const osc = audioCtx.createOscillator();
    const gain = audioCtx.createGain();
    osc.connect(gain);
    gain.connect(audioCtx.destination);
    const now = audioCtx.currentTime;
    osc.type = 'sine';
    osc.frequency.setValueAtTime(1100, now);
    osc.frequency.setValueAtTime(1400, now+0.08);
    gain.gain.setValueAtTime(0.18, now);
    gain.gain.exponentialRampToValueAtTime(0.001, now+0.2);
    osc.start(now);
    osc.stop(now+0.2);
}

// ========== 画布初始化 ==========
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const topBar = document.getElementById('topBar');
const centerComboEl = document.getElementById('centerCombo');
const guideMask = document.getElementById('guideMask');
function resizeCanvas(){
    canvas.width = window.innerWidth;
    canvas.height = canvas.offsetHeight;
}
resizeCanvas();
window.addEventListener('resize', resizeCanvas);

// ========== 全局游戏配置|纯休闲无盈利相关 ==========
const game = {
    timeLeft:60,
    score:0,
    combo:0,
    maxCombo:0,
    lastTapTime:0,
    comboTimeout:1200, // 休闲优化:断连时长放宽至1.2秒,新手友好
    gameRunning:false,
    freezeActive:false,
    freezeEndTs:0,
    bubbleSpeedBase:0.9, // 降低基础速度,节奏舒缓
    speedAdd:0.0005, // 减速加速幅度,不会越玩越紧张
    maxBubbleCount:20,
    spawnTimer:null,
    countTimer:null,
    bubbles:[],
    particles:[],
    floatTexts:[],
    freezeItems:[],
    lastFreezeScore:0,
    freezeSpawnStep:15, // 休闲优化:每15分生成冰冻,更容易清屏
    bestScore: Number(localStorage.getItem('bubbleBest')) || 0,
    hasShowGuide: Boolean(localStorage.getItem('bubbleGuideShow')),
    // 预留金币扩展:如需金币系统直接修改初始值,无氪金逻辑
    coin: 9999 // 初始金币拉满,纯休闲无消耗、无充值入口
};

// DOM元素绑定
const timeText = document.getElementById('timeText');
const scoreText = document.getElementById('scoreText');
const maxComboText = document.getElementById('maxComboText');
const modalMask = document.getElementById('modalMask');
const endScoreEl = document.getElementById('endScore');
const endMaxComboEl = document.getElementById('endMaxCombo');
const bestScoreEl = document.getElementById('bestScore');
const restartBtn = document.getElementById('restartBtn');
const shareBtn = document.getElementById('shareBtn');

// ========== 粒子特效类(彩色爆炸小点) ==========
class Particle{
    constructor(x,y,color){
        this.x = x;
        this.y = y;
        this.r = 2 + Math.random()*4;
        const angle = Math.random() * Math.PI * 2;
        const speed = 3 + Math.random()*4.5;
        this.vx = Math.cos(angle) * speed;
        this.vy = Math.sin(angle) * speed;
        this.life = 55;
        this.color = color;
    }
    update(){
        this.x += this.vx;
        this.y += this.vy;
        this.life--;
        this.vy += 0.06;
        this.vx *= 0.98;
    }
    draw(){
        ctx.globalAlpha = this.life / 55;
        ctx.beginPath();
        ctx.arc(this.x,this.y,this.r,0,Math.PI*2);
        ctx.fillStyle = this.color;
        ctx.fill();
        ctx.globalAlpha = 1;
    }
}

// ========== 得分上浮文字 ==========
class FloatText{
    constructor(x,y,text){
        this.x = x;
        this.y = y;
        this.text = text;
        this.life = 50;
        this.vy = -1.6;
        this.size = 24;
    }
    update(){
        this.y += this.vy;
        this.life--;
        this.size *= 0.97;
    }
    draw(){
        ctx.globalAlpha = this.life / 50;
        ctx.font = `bold ${this.size}px sans-serif`;
        ctx.fillStyle = '#fff866';
        ctx.textAlign = 'center';
        ctx.fillText(this.text, this.x, this.y);
        ctx.globalAlpha = 1;
    }
}

// ========== 冰冻道具类 ==========
class FreezeItem{
    constructor(){
        this.r = 22;
        this.x = this.r + Math.random()*(canvas.width - this.r*2);
        this.y = 120 + Math.random()*(canvas.height*0.6);
        this.life = 360;
    }
    draw(){
        ctx.beginPath();
        ctx.arc(this.x,this.y,this.r,0,Math.PI*2);
        ctx.fillStyle = 'rgba(100,220,255,0.75)';
        ctx.fill();
        ctx.strokeStyle = '#fff';
        ctx.lineWidth = 2;
        ctx.stroke();
        ctx.fillStyle = '#002244';
        ctx.font = 'bold 24px sans-serif';
        ctx.textAlign = 'center';
        ctx.textBaseline = 'middle';
        ctx.fillText('❄', this.x, this.y);
    }
    hit(px,py){
        const dx = px - this.x;
        const dy = py - this.y;
        return dx*dx + dy*dy <= this.r*this.r;
    }
}

// ========== 泡泡主类:渐变半透+脉动+S摆动 ==========
class Bubble{
    constructor(){
        this.baseR = 25 + Math.random()*15;
        this.r = this.baseR;
        this.x = this.baseR + Math.random()*(canvas.width - this.baseR*2);
        this.originX = this.x;
        this.y = canvas.height + this.baseR;
        this.speed = game.bubbleSpeedBase;
        this.wavePhase = Math.random() * Math.PI * 2;
        this.pulsePhase = Math.random() * Math.PI * 2;
        // 类型概率:炸弹8% 金色10% 普通82%
        const rand = Math.random();
        if(rand < 0.08){
            this.type = 'bomb';
            this.baseColor1 = 'rgba(40,40,40,0.7)';
            this.baseColor2 = 'rgba(80,80,80,0.5)';
        }else if(rand < 0.18){
            this.type = 'gold';
            this.baseColor1 = 'rgba(255,220,40,0.75)';
            this.baseColor2 = 'rgba(255,240,150,0.55)';
        }else{
            this.type = 'normal';
            const hue = Math.random()*360;
            this.baseColor1 = `hsla(${hue},78%,62%,0.7)`;
            this.baseColor2 = `hsla(${hue+30},82%,72%,0.4)`;
        }
        this.stroke = 'rgba(255,255,255,0.85)';
    }
    update(deltaSpeed){
        if(game.freezeActive) return;
        this.wavePhase += 0.018;
        this.x = this.originX + Math.sin(this.wavePhase) * 14;
        this.pulsePhase += 0.035;
        this.r = this.baseR + Math.sin(this.pulsePhase) * 3;
        this.speed += deltaSpeed;
        this.y -= this.speed;
    }
    draw(){
        ctx.beginPath();
        ctx.arc(this.x, this.y, this.r, 0, Math.PI*2);
        const grad = ctx.createRadialGradient(this.x,this.y,0,this.x,this.y,this.r);
        grad.addColorStop(0, this.baseColor2);
        grad.addColorStop(1, this.baseColor1);
        ctx.fillStyle = grad;
        ctx.fill();
        ctx.lineWidth = 2;
        ctx.strokeStyle = this.stroke;
        ctx.stroke();
        if(this.type === 'gold'){
            ctx.fillStyle = '#fff';
            ctx.font = `bold ${this.r*0.85}px sans-serif`;
            ctx.textAlign = 'center';
            ctx.textBaseline = 'middle';
            ctx.fillText('+5', this.x, this.y);
        }else if(this.type === 'bomb'){
            ctx.fillStyle = '#ff4444';
            ctx.font = `bold ${this.r*0.85}px sans-serif`;
            ctx.textAlign = 'center';
            ctx.textBaseline = 'middle';
            ctx.fillText('-1s', this.x, this.y); // 休闲削弱:炸弹仅扣1秒
        }
    }
    hit(touchX, touchY){
        const dx = touchX - this.x;
        const dy = touchY - this.y;
        return dx*dx + dy*dy <= this.r*this.r;
    }
}

// ========== 生成控制 ==========
function getSpawnDelay(){
    if(game.timeLeft > 30) return 1500;
    if(game.timeLeft > 10) return 1000;
    return 600;
}
function spawnBubble(){
    if(!game.gameRunning) return;
    if(game.bubbles.length >= game.maxBubbleCount) return;
    game.bubbles.push(new Bubble());
}
function trySpawnFreeze(){
    const threshold = game.lastFreezeScore + game.freezeSpawnStep;
    if(game.score >= threshold){
        game.freezeItems.push(new FreezeItem());
        game.lastFreezeScore = game.score;
    }
}
function spawnExplodeParticles(x,y,color,count=12){
    for(let i=0;i<count;i++){
        game.particles.push(new Particle(x,y,color));
    }
}

// ========== 点击交互核心 ==========
function handleTap(x,y){
    if(!game.gameRunning) return;
    initAudio();
    const nowTs = Date.now();
    // 冰冻道具检测
    for(let i = game.freezeItems.length - 1; i >= 0; i--){
        const item = game.freezeItems[i];
        if(item.hit(x,y)){
            game.freezeActive = true;
            game.freezeEndTs = nowTs + 2000;
            game.freezeItems.splice(i,1);
            return;
        }
    }
    let hitBubble = null;
    let hitIndex = -1;
    for(let i=game.bubbles.length-1; i>=0; i--){
        const b = game.bubbles[i];
        if(b.hit(x,y)){
            hitBubble = b;
            hitIndex = i;
            break;
        }
    }
    if(!hitBubble){
        game.combo = 0;
        centerComboEl.textContent = '';
        return;
    }
    // 宽松连击判定
    if(nowTs - game.lastTapTime <= game.comboTimeout){
        game.combo += 1;
        if(game.combo ===5 || game.combo ===10 || game.combo ===20){
            playComboTipSound();
        }
    }else{
        game.combo = 1;
    }
    game.lastTapTime = nowTs;
    if(game.combo > game.maxCombo) game.maxCombo = game.combo;
    maxComboText.textContent = game.maxCombo;
    centerComboEl.textContent = `连击 x${game.combo}`;
    centerComboEl.style.fontSize = `${38 + game.combo*2.2}px`;
    spawnExplodeParticles(hitBubble.x, hitBubble.y, hitBubble.baseColor1);
    const basePoint = hitBubble.type === 'gold' ? 5 : 1;
    const addComboPoint = game.combo;
    const totalAdd = basePoint + addComboPoint;
    game.score += totalAdd;
    scoreText.textContent = game.score;
    game.floatTexts.push(new FloatText(hitBubble.x, hitBubble.y, `+${totalAdd}`));
    // 炸弹仅扣1秒,惩罚弱化
    if(hitBubble.type === 'bomb'){
        playBombSound();
        game.timeLeft -= 1;
        if(game.timeLeft < 0) game.timeLeft = 0;
        timeText.textContent = game.timeLeft;
    }else{
        playPopSound();
    }
    game.bubbles.splice(hitIndex,1);
    trySpawnFreeze();
}

// ========== 输入绑定 ==========
canvas.addEventListener('touchstart', e=>{
    e.preventDefault();
    const t = e.touches[0];
    const offsetY = topBar.offsetHeight;
    handleTap(t.clientX, t.clientY - offsetY);
},{passive:false});
canvas.addEventListener('click', e=>{
    const offsetY = topBar.offsetHeight;
    handleTap(e.clientX, e.clientY - offsetY);
});
guideMask.addEventListener('click', ()=>{
    guideMask.style.display = 'none';
    localStorage.setItem('bubbleGuideShow','1');
    game.hasShowGuide = true;
});

// ========== 倒计时定时器 ==========
function gameTimerTick(){
    if(!game.gameRunning) return;
    game.timeLeft -= 1;
    timeText.textContent = game.timeLeft;
    clearInterval(game.spawnTimer);
    game.spawnTimer = setInterval(spawnBubble, getSpawnDelay());
    if(game.timeLeft <= 0){
        endGame();
    }
}

// ========== 游戏主渲染循环 ==========
let lastTime = 0;
function gameLoop(ts){
    requestAnimationFrame(gameLoop);
    if(!game.gameRunning) return;
    const delta = ts - lastTime;
    lastTime = ts;
    ctx.clearRect(0,0,canvas.width,canvas.height);
    if(game.freezeActive && Date.now() > game.freezeEndTs){
        game.freezeActive = false;
    }
    let touchTop = false;
    for(let i=game.bubbles.length-1; i>=0; i--){
        const b = game.bubbles[i];
        b.update(game.speedAdd * delta);
        b.draw();
        if(b.y - b.r <= 0) touchTop = true;
    }
    if(touchTop){
        endGame();
    }
    for(let i=game.particles.length-1; i>=0; i--){
        const p = game.particles[i];
        p.update();
        p.draw();
        if(p.life <= 0) game.particles.splice(i,1);
    }
    for(let i=game.floatTexts.length-1; i>=0; i--){
        const ft = game.floatTexts[i];
        ft.update();
        ft.draw();
        if(ft.life <= 0) game.floatTexts.splice(i,1);
    }
    for(let i=game.freezeItems.length-1; i>=0; i--){
        const item = game.freezeItems[i];
        item.draw();
        item.life--;
        if(item.life <= 0) game.freezeItems.splice(i,1);
    }
}

// ========== 游戏结束逻辑 ==========
function endGame(){
    game.gameRunning = false;
    clearInterval(game.spawnTimer);
    clearInterval(game.countTimer);
    if(game.score > game.bestScore){
        game.bestScore = game.score;
        localStorage.setItem('bubbleBest', game.bestScore);
    }
    endScoreEl.textContent = game.score;
    endMaxComboEl.textContent = game.maxCombo;
    bestScoreEl.textContent = game.bestScore;
    modalMask.style.display = 'flex';
    centerComboEl.textContent = '';
}

// ========== 分享按钮:复制文本 ==========
shareBtn.addEventListener('click', async ()=>{
    const copyText = `我在休闲泡泡速爆中得了${game.score}分,轻松解压,你也来试试?`;
    try{
        await navigator.clipboard.writeText(copyText);
        alert('文案已复制,分享给好友一起放松!');
    }catch(err){
        alert('复制失败,手动复制:'+copyText);
    }
});

// ========== 开局重置 ==========
function startGame(){
    game.timeLeft = 60;
    game.score = 0;
    game.combo = 0;
    game.maxCombo = 0;
    game.lastTapTime = 0;
    game.freezeActive = false;
    game.particles = [];
    game.floatTexts = [];
    game.freezeItems = [];
    game.bubbles = [];
    game.lastFreezeScore = 0;
    game.gameRunning = true;
    timeText.textContent = game.timeLeft;
    scoreText.textContent = game.score;
    maxComboText.textContent = game.maxCombo;
    centerComboEl.textContent = '';
    modalMask.style.display = 'none';
    if(!game.hasShowGuide){
        guideMask.style.display = 'flex';
    }
    clearInterval(game.spawnTimer);
    game.spawnTimer = setInterval(spawnBubble, getSpawnDelay());
    clearInterval(game.countTimer);
    game.countTimer = setInterval(gameTimerTick, 1000);
    lastTime = performance.now();
    requestAnimationFrame(gameLoop);
}

restartBtn.addEventListener('click', startGame);
window.addEventListener('load', startGame);
</script>
</body>
</html>

Game Source: 泡泡速爆|休闲解压小游戏

Creator: CrystalPenguin76

Libraries: none

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