泡泡速爆|休闲解压小游戏
by CrystalPenguin76558 lines18.9 KB
<!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.