808 lines
32 KiB
HTML
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>
|