Files
Ubuntu 32b43f85bd a
2026-07-03 16:58:28 +08:00

808 lines
32 KiB
HTML

<!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>🫧 泡泡龙大作战 | Bubble Shooter</title>
<script src="https://game-cdn.poki.com/scripts/v2/poki-sdk.js"></script>
<script src="https://code.playcanvas.com/playcanvas-stable.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
width: 100%;
height: 100%;
overflow: hidden;
background: #0a0a1a;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
touch-action: none;
}
#game-container {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
canvas {
display: block;
box-shadow: 0 0 40px rgba(0, 100, 255, 0.3);
}
</style>
</head>
<body>
<div id="game-container">
<canvas id="application"></canvas>
</div>
<script>
// ============================================
// Poki SDK 管理器
// ============================================
class PokiManager {
constructor() {
this.initialized = false;
this.audioEnabled = true;
}
async init() {
try {
await PokiSDK.init();
console.log('✅ Poki SDK 初始化成功');
this.initialized = true;
PokiSDK.gameLoadingFinished();
// 禁用页面滚动
window.addEventListener('keydown', (e) => {
if (['ArrowDown', 'ArrowUp', ' '].includes(e.key)) e.preventDefault();
});
window.addEventListener('wheel', (e) => e.preventDefault(), { passive: false });
} catch (err) {
console.warn('⚠️ Poki SDK 初始化失败:', err);
this.initialized = false;
}
}
gameplayStart() {
if (this.initialized) PokiSDK.gameplayStart();
}
gameplayStop() {
if (this.initialized) PokiSDK.gameplayStop();
}
async showAd() {
if (!this.initialized) return;
try {
await PokiSDK.commercialBreak(() => { this.audioEnabled = false; });
this.audioEnabled = true;
} catch (err) {
this.audioEnabled = true;
}
}
}
// ============================================
// 泡泡龙游戏 - PlayCanvas版本
// ============================================
class BubbleShooterGame {
constructor() {
this.width = 480;
this.height = 640;
// 初始化 PlayCanvas
this.canvas = document.getElementById('application');
this.app = new pc.Application(this.canvas, {
mouse: new pc.Mouse(this.canvas),
touch: new pc.TouchDevice(this.canvas),
elementInput: new pc.ElementInput(this.canvas)
});
this.app.start();
this.app.setCanvasFillMode(pc.FILLMODE_NONE);
this.resize();
window.addEventListener('resize', () => this.resize());
// Poki
this.poki = new PokiManager();
// 游戏常量
this.BUBBLE_RADIUS = 20;
this.GRID_COLS = 12;
this.GRID_ROWS = 16;
this.COLORS = [
new pc.Color(1, 0.42, 0.42), // 红 #ff6b6b
new pc.Color(0.31, 0.8, 0.77), // 青 #4ecdc4
new pc.Color(0.27, 0.72, 0.82), // 蓝 #45b7d1
new pc.Color(0.59, 0.81, 0.71), // 绿 #96ceb4
new pc.Color(1, 0.92, 0.65), // 黄 #ffeaa7
new pc.Color(0.87, 0.9, 0.91), // 白 #dfe6e9
new pc.Color(0.99, 0.47, 0.66) // 粉 #fd79a8
];
// 游戏状态
this.grid = [];
this.score = 0;
this.clearedCount = 0;
this.combo = 0;
this.shots = 0;
this.gameState = 'loading'; // loading, menu, playing, gameover
// 泡泡状态
this.currentBubble = null;
this.nextBubbleColor = null;
this.projectile = null;
this.aimAngle = -Math.PI / 2;
this.isAiming = false;
// 实体引用
this.bubbleEntities = [];
this.uiEntities = [];
this.init();
}
resize() {
const container = document.getElementById('game-container');
const aspect = this.width / this.height;
const containerAspect = container.clientWidth / container.clientHeight;
let canvasWidth, canvasHeight;
if (containerAspect > aspect) {
canvasHeight = container.clientHeight;
canvasWidth = canvasHeight * aspect;
} else {
canvasWidth = container.clientWidth;
canvasHeight = canvasWidth / aspect;
}
this.canvas.style.width = canvasWidth + 'px';
this.canvas.style.height = canvasHeight + 'px';
this.app.resizeCanvas(canvasWidth, canvasHeight);
}
async init() {
this.createScene();
this.createMaterials();
await this.poki.init();
this.initGrid();
this.loadBubble();
this.createUI();
this.setupInput();
this.startGame();
this.app.on('update', (dt) => this.update(dt));
}
createScene() {
// 相机 - 正交投影,适配2D游戏
this.camera = new pc.Entity('camera');
this.camera.addComponent('camera', {
clearColor: new pc.Color(0.04, 0.04, 0.1),
projection: pc.PROJECTION_ORTHOGRAPHIC,
orthoHeight: this.height / 2
});
this.camera.setPosition(0, 0, 10);
this.app.root.addChild(this.camera);
// 灯光
const light = new pc.Entity('light');
light.addComponent('light', {
type: 'directional',
color: new pc.Color(1, 1, 1),
intensity: 1
});
light.setEulerAngles(45, 30, 0);
this.app.root.addChild(light);
// 游戏根节点
this.gameRoot = new pc.Entity('gameRoot');
this.app.root.addChild(this.gameRoot);
// UI根节点
this.uiRoot = new pc.Entity('uiRoot');
this.app.root.addChild(this.uiRoot);
}
createMaterials() {
this.materials = [];
for (let i = 0; i < this.COLORS.length; i++) {
const mat = new pc.StandardMaterial();
mat.diffuse = this.COLORS[i];
mat.shininess = 80;
mat.metalness = 0.3;
mat.useMetalness = true;
mat.update();
this.materials.push(mat);
}
// 白色材质(用于高光)
this.whiteMat = new pc.StandardMaterial();
this.whiteMat.diffuse = new pc.Color(1, 1, 1);
this.whiteMat.emissive = new pc.Color(0.8, 0.8, 0.8);
this.whiteMat.update();
}
initGrid() {
this.grid = [];
this.bubbleEntities = [];
for (let row = 0; row < this.GRID_ROWS; row++) {
this.grid[row] = [];
for (let col = 0; col < this.GRID_COLS; col++) {
if (row % 2 === 1 && col === this.GRID_COLS - 1) {
this.grid[row][col] = null;
continue;
}
if (row < 5) {
const colorIdx = Math.floor(Math.random() * this.COLORS.length);
this.grid[row][col] = {
colorIdx: colorIdx,
row: row,
col: col
};
this.createBubbleEntity(row, col, colorIdx);
} else {
this.grid[row][col] = null;
}
}
}
}
createBubbleEntity(row, col, colorIdx) {
const pos = this.getBubblePosition(row, col);
// 泡泡主体
const bubble = new pc.Entity(`bubble_${row}_${col}`);
bubble.addComponent('render', { type: 'sphere' });
bubble.setLocalScale(this.BUBBLE_RADIUS * 1.8, this.BUBBLE_RADIUS * 1.8, this.BUBBLE_RADIUS * 1.8);
bubble.setPosition(pos.x, pos.y, 0);
bubble.render.material = this.materials[colorIdx];
this.gameRoot.addChild(bubble);
// 高光
const highlight = new pc.Entity('highlight');
highlight.addComponent('render', { type: 'sphere' });
highlight.setLocalScale(0.3, 0.3, 0.3);
highlight.setPosition(-0.3, 0.3, 0.8);
highlight.render.material = this.whiteMat;
bubble.addChild(highlight);
this.bubbleEntities.push(bubble);
return bubble;
}
getBubblePosition(row, col) {
const offset = (row % 2) * this.BUBBLE_RADIUS;
const x = col * (this.BUBBLE_RADIUS * 2) + this.BUBBLE_RADIUS + offset - this.width / 2;
const y = this.height / 2 - (row * (this.BUBBLE_RADIUS * Math.sqrt(3)) + this.BUBBLE_RADIUS) - 40;
return { x, y };
}
loadBubble() {
if (this.nextBubbleColor === null) {
this.nextBubbleColor = Math.floor(Math.random() * this.COLORS.length);
}
this.currentBubble = {
colorIdx: this.nextBubbleColor,
x: 0,
y: -this.height / 2 + 60
};
this.nextBubbleColor = Math.floor(Math.random() * this.COLORS.length);
this.updateNextBubbleUI();
}
createUI() {
// 分数文本
this.scoreText = this.createText('💎 得分: 0', -this.width/2 + 80, this.height/2 - 30, 20);
this.uiRoot.addChild(this.scoreText);
// 消除数
this.clearedText = this.createText('🎯 0', 0, this.height/2 - 30, 20);
this.uiRoot.addChild(this.clearedText);
// 连击
this.comboText = this.createText('🔥 连击: 0', this.width/2 - 80, this.height/2 - 30, 20);
this.uiRoot.addChild(this.comboText);
// 下一个泡泡提示
this.nextLabel = this.createText('下一个:', -40, -this.height/2 + 30, 16);
this.uiRoot.addChild(this.nextLabel);
// 下一个泡泡显示
this.nextBubbleEntity = new pc.Entity('nextBubble');
this.nextBubbleEntity.addComponent('render', { type: 'sphere' });
this.nextBubbleEntity.setLocalScale(25, 25, 25);
this.nextBubbleEntity.setPosition(30, -this.height/2 + 60, 0);
this.uiRoot.addChild(this.nextBubbleEntity);
// 下一个泡泡高光
const highlight = new pc.Entity('highlight');
highlight.addComponent('render', { type: 'sphere' });
highlight.setLocalScale(0.3, 0.3, 0.3);
highlight.setPosition(-0.3, 0.3, 0.8);
highlight.render.material = this.whiteMat;
this.nextBubbleEntity.addChild(highlight);
// 瞄准线
this.aimLine = new pc.Entity('aimLine');
this.aimLine.addComponent('render', { type: 'cylinder' });
this.aimLine.setLocalScale(2, 50, 2);
this.aimLine.setPosition(0, -this.height/2 + 110, 0);
const lineMat = new pc.StandardMaterial();
lineMat.diffuse = new pc.Color(1, 1, 1);
lineMat.opacity = 0.3;
lineMat.blendType = pc.BLEND_NORMAL;
lineMat.update();
this.aimLine.render.material = lineMat;
this.uiRoot.addChild(this.aimLine);
// 危险线
const dangerY = this.getBubblePosition(this.GRID_ROWS - 2, 0).y;
const dangerLine = new pc.Entity('dangerLine');
dangerLine.addComponent('render', { type: 'cylinder' });
dangerLine.setLocalScale(480, 2, 2);
dangerLine.setPosition(0, dangerY, 0);
const dangerMat = new pc.StandardMaterial();
dangerMat.diffuse = new pc.Color(1, 0.3, 0.3);
dangerMat.emissive = new pc.Color(0.5, 0.1, 0.1);
dangerMat.opacity = 0.5;
dangerMat.blendType = pc.BLEND_NORMAL;
dangerMat.update();
dangerLine.render.material = dangerMat;
this.uiRoot.addChild(dangerLine);
}
createText(text, x, y, fontSize) {
const entity = new pc.Entity('text');
entity.setPosition(x, y, 5);
entity.addComponent('element', {
type: 'text',
text: text,
fontSize: fontSize,
color: new pc.Color(1, 1, 1),
anchor: new pc.Vec4(0.5, 0.5, 0.5, 0.5),
pivot: new pc.Vec2(0.5, 0.5),
fontAsset: null
});
return entity;
}
updateNextBubbleUI() {
this.nextBubbleEntity.render.material = this.materials[this.nextBubbleColor];
}
setupInput() {
// 鼠标/触摸输入
this.app.mouse.on(pc.EVENT_MOUSEDOWN, (e) => {
if (this.gameState !== 'playing' || this.projectile) return;
this.isAiming = true;
this.updateAim(e.x, e.y);
});
this.app.mouse.on(pc.EVENT_MOUSEMOVE, (e) => {
if (!this.isAiming || this.projectile) return;
this.updateAim(e.x, e.y);
});
this.app.mouse.on(pc.EVENT_MOUSEUP, () => {
if (!this.isAiming || this.projectile || this.gameState !== 'playing') return;
this.isAiming = false;
this.shoot();
});
// 触摸支持
this.app.touch.on(pc.EVENT_TOUCHSTART, (e) => {
if (this.gameState !== 'playing' || this.projectile) return;
this.isAiming = true;
const touch = e.touches[0];
this.updateAim(touch.x, touch.y);
e.event.preventDefault();
});
this.app.touch.on(pc.EVENT_TOUCHMOVE, (e) => {
if (!this.isAiming || this.projectile) return;
const touch = e.touches[0];
this.updateAim(touch.x, touch.y);
e.event.preventDefault();
});
this.app.touch.on(pc.EVENT_TOUCHEND, (e) => {
if (!this.isAiming || this.projectile || this.gameState !== 'playing') return;
this.isAiming = false;
this.shoot();
e.event.preventDefault();
});
}
updateAim(screenX, screenY) {
// 屏幕坐标转世界坐标
const worldPos = this.screenToWorld(screenX, screenY);
const dx = worldPos.x - this.currentBubble.x;
const dy = worldPos.y - this.currentBubble.y;
this.aimAngle = Math.atan2(dy, dx);
// 限制角度(不能向下射)
if (this.aimAngle > -0.2) this.aimAngle = -0.2;
if (this.aimAngle < -Math.PI + 0.2) this.aimAngle = -Math.PI + 0.2;
// 更新瞄准线
this.aimLine.setEulerAngles(0, 0, this.aimAngle * 180 / Math.PI - 90);
}
screenToWorld(screenX, screenY) {
const rect = this.canvas.getBoundingClientRect();
const x = (screenX - rect.left) / rect.width * this.width - this.width / 2;
const y = this.height / 2 - (screenY - rect.top) / rect.height * this.height;
return { x, y };
}
shoot() {
const speed = 15;
this.projectile = {
x: this.currentBubble.x,
y: this.currentBubble.y,
vx: Math.cos(this.aimAngle) * speed,
vy: Math.sin(this.aimAngle) * speed,
colorIdx: this.currentBubble.colorIdx,
radius: this.BUBBLE_RADIUS
};
// 创建飞行泡泡实体
this.projectileEntity = new pc.Entity('projectile');
this.projectileEntity.addComponent('render', { type: 'sphere' });
this.projectileEntity.setLocalScale(this.BUBBLE_RADIUS * 1.8, this.BUBBLE_RADIUS * 1.8, this.BUBBLE_RADIUS * 1.8);
this.projectileEntity.setPosition(this.projectile.x, this.projectile.y, 0);
this.projectileEntity.render.material = this.materials[this.projectile.colorIdx];
this.gameRoot.addChild(this.projectileEntity);
// 高光
const highlight = new pc.Entity('highlight');
highlight.addComponent('render', { type: 'sphere' });
highlight.setLocalScale(0.3, 0.3, 0.3);
highlight.setPosition(-0.3, 0.3, 0.8);
highlight.render.material = this.whiteMat;
this.projectileEntity.addChild(highlight);
}
update(dt) {
if (this.projectile) {
this.updateProjectile();
}
}
updateProjectile() {
const p = this.projectile;
p.x += p.vx;
p.y += p.vy;
// 墙壁反弹
const leftBound = -this.width / 2 + p.radius;
const rightBound = this.width / 2 - p.radius;
if (p.x < leftBound || p.x > rightBound) {
p.vx = -p.vx;
p.x = Math.max(leftBound, Math.min(rightBound, p.x));
}
// 顶部碰撞
const topBound = this.height / 2 - 40 - p.radius;
if (p.y > topBound) {
this.attachBubble();
return;
}
// 检测与网格泡泡的碰撞
for (let row = 0; row < this.GRID_ROWS; row++) {
for (let col = 0; col < this.GRID_COLS; col++) {
const bubble = this.grid[row][col];
if (bubble) {
const pos = this.getBubblePosition(row, col);
const dist = Math.hypot(p.x - pos.x, p.y - pos.y);
if (dist < p.radius + this.BUBBLE_RADIUS - 2) {
this.attachBubble();
return;
}
}
}
}
// 更新实体位置
this.projectileEntity.setPosition(p.x, p.y, 0);
}
attachBubble() {
const p = this.projectile;
// 找到最近的网格位置
let bestRow = 0, bestCol = 0;
let minDist = Infinity;
for (let row = 0; row < this.GRID_ROWS; row++) {
for (let col = 0; col < this.GRID_COLS; col++) {
if (row % 2 === 1 && col === this.GRID_COLS - 1) continue;
if (this.grid[row][col]) continue;
const pos = this.getBubblePosition(row, col);
const dist = Math.hypot(p.x - pos.x, p.y - pos.y);
if (dist < minDist) {
minDist = dist;
bestRow = row;
bestCol = col;
}
}
}
// 放置泡泡
this.grid[bestRow][bestCol] = {
colorIdx: p.colorIdx,
row: bestRow,
col: bestCol
};
// 销毁飞行泡泡,创建网格泡泡
this.projectileEntity.destroy();
this.projectile = null;
this.createBubbleEntity(bestRow, bestCol, p.colorIdx);
// 检查匹配
const matches = this.findMatches(bestRow, bestCol, p.colorIdx);
if (matches.length >= 3) {
this.removeBubbles(matches);
this.score += matches.length * 10 * (this.combo + 1);
this.clearedCount += matches.length;
this.combo++;
// 消除悬挂的泡泡
const floating = this.findFloatingBubbles();
if (floating.length > 0) {
this.removeBubbles(floating);
this.score += floating.length * 20;
this.clearedCount += floating.length;
}
} else {
this.combo = 0;
this.shots++;
if (this.shots % 6 === 0) {
this.addRow();
}
}
this.loadBubble();
this.updateUI();
this.checkGameOver();
}
findMatches(startRow, startCol, colorIdx) {
const matches = [];
const visited = new Set();
const queue = [[startRow, startCol]];
while (queue.length > 0) {
const [row, col] = queue.shift();
const key = `${row},${col}`;
if (visited.has(key)) continue;
visited.add(key);
const bubble = this.grid[row]?.[col];
if (!bubble || bubble.colorIdx !== colorIdx) continue;
matches.push(bubble);
const neighbors = this.getNeighbors(row, col);
for (const [nr, nc] of neighbors) {
if (!visited.has(`${nr},${nc}`)) {
queue.push([nr, nc]);
}
}
}
return matches;
}
getNeighbors(row, col) {
const isOdd = row % 2 === 1;
const offsets = isOdd ? [
[-1, 0], [-1, 1], [0, -1], [0, 1], [1, 0], [1, 1]
] : [
[-1, -1], [-1, 0], [0, -1], [0, 1], [1, -1], [1, 0]
];
return offsets.map(([dr, dc]) => [row + dr, col + dc])
.filter(([r, c]) => r >= 0 && r < this.GRID_ROWS && c >= 0 && c < this.GRID_COLS);
}
findFloatingBubbles() {
const connected = new Set();
const queue = [];
for (let col = 0; col < this.GRID_COLS; col++) {
if (this.grid[0][col]) {
queue.push([0, col]);
connected.add(`0,${col}`);
}
}
while (queue.length > 0) {
const [row, col] = queue.shift();
const neighbors = this.getNeighbors(row, col);
for (const [nr, nc] of neighbors) {
const key = `${nr},${nc}`;
if (!connected.has(key) && this.grid[nr]?.[nc]) {
connected.add(key);
queue.push([nr, nc]);
}
}
}
const floating = [];
for (let row = 0; row < this.GRID_ROWS; row++) {
for (let col = 0; col < this.GRID_COLS; col++) {
if (this.grid[row]?.[col] && !connected.has(`${row},${col}`)) {
floating.push(this.grid[row][col]);
}
}
}
return floating;
}
removeBubbles(bubbles) {
for (const bubble of bubbles) {
// 找到并销毁实体
const entityName = `bubble_${bubble.row}_${bubble.col}`;
const entity = this.gameRoot.findByName(entityName);
if (entity) {
entity.destroy();
}
this.grid[bubble.row][bubble.col] = null;
}
}
addRow() {
// 从下往上移动
for (let row = this.GRID_ROWS - 1; row > 0; row--) {
for (let col = 0; col < this.GRID_COLS; col++) {
this.grid[row][col] = this.grid[row - 1]?.[col];
if (this.grid[row][col]) {
this.grid[row][col].row = row;
}
}
}
// 顶部添加新行
for (let col = 0; col < this.GRID_COLS; col++) {
this.grid[0][col] = {
colorIdx: Math.floor(Math.random() * this.COLORS.length),
row: 0,
col: col
};
}
// 重新创建所有实体
this.refreshBubbleEntities();
}
refreshBubbleEntities() {
// 清除所有泡泡实体
for (const entity of this.bubbleEntities) {
if (entity && !entity.destroyed) {
entity.destroy();
}
}
this.bubbleEntities = [];
// 重新创建
for (let row = 0; row < this.GRID_ROWS; row++) {
for (let col = 0; col < this.GRID_COLS; col++) {
if (this.grid[row]?.[col]) {
this.createBubbleEntity(row, col, this.grid[row][col].colorIdx);
}
}
}
}
checkGameOver() {
for (let col = 0; col < this.GRID_COLS; col++) {
if (this.grid[this.GRID_ROWS - 2]?.[col]) {
this.gameOver();
return;
}
}
}
gameOver() {
this.gameState = 'gameover';
this.poki.gameplayStop();
this.poki.showAd();
// 显示游戏结束UI
this.showGameOverUI();
}
showGameOverUI() {
const title = this.score > 1000 ? '🎉 太棒了!' : '游戏结束';
this.gameOverPanel = new pc.Entity('gameOverPanel');
this.gameOverPanel.setPosition(0, 0, 10);
this.uiRoot.addChild(this.gameOverPanel);
// 背景
const bg = new pc.Entity('bg');
bg.addComponent('render', { type: 'plane' });
bg.setLocalScale(300, 1, 200);
const bgMat = new pc.StandardMaterial();
bgMat.diffuse = new pc.Color(0.1, 0.15, 0.3);
bgMat.opacity = 0.95;
bgMat.blendType = pc.BLEND_NORMAL;
bgMat.update();
bg.render.material = bgMat;
this.gameOverPanel.addChild(bg);
// 标题
const titleText = this.createText(title, 0, 40, 36);
this.gameOverPanel.addChild(titleText);
// 分数
const scoreText = this.createText(`最终得分: ${this.score}`, 0, -10, 24);
this.gameOverPanel.addChild(scoreText);
// 提示
const hintText = this.createText('点击重新开始', 0, -60, 16);
hintText.element.color = new pc.Color(0.7, 0.8, 1);
this.gameOverPanel.addChild(hintText);
// 点击重启
this.app.mouse.on(pc.EVENT_MOUSEDOWN, () => this.restart());
this.app.touch.on(pc.EVENT_TOUCHSTART, () => this.restart());
}
restart() {
if (this.gameOverPanel) {
this.gameOverPanel.destroy();
}
this.grid = [];
this.score = 0;
this.clearedCount = 0;
this.combo = 0;
this.shots = 0;
this.projectile = null;
this.isAiming = false;
this.refreshBubbleEntities();
this.initGrid();
this.loadBubble();
this.updateUI();
this.startGame();
}
updateUI() {
this.scoreText.element.text = `💎 得分: ${this.score}`;
this.clearedText.element.text = `🎯 ${this.clearedCount}`;
this.comboText.element.text = `🔥 连击: ${this.combo}`;
}
startGame() {
this.gameState = 'playing';
this.poki.gameplayStart();
}
}
// 启动游戏
const game = new BubbleShooterGame();
</script>
<!-- 登录认证 -->
<script src="../auth.js?v=1"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
GAME_AUTH.init('bubble-shooter', '泡泡龙');
});
</script>
</body>
</html>