Files
nanocode/happy-games/games/blocky-blast/index.html
T
Ubuntu 32b43f85bd a
2026-07-03 16:58:28 +08:00

693 lines
27 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>🧩 Blocky Blast | 方块消除</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;
}
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);
}
}
gameplayStart() {
if (this.initialized) PokiSDK.gameplayStart();
}
gameplayStop() {
if (this.initialized) PokiSDK.gameplayStop();
}
async showAd() {
if (!this.initialized) return;
try {
await PokiSDK.commercialBreak();
} catch (err) {}
}
}
// ============================================
// Blocky Blast - PlayCanvas版本
// ============================================
class BlockyBlastGame {
constructor() {
this.width = 480;
this.height = 640;
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());
this.poki = new PokiManager();
// 游戏配置
this.GRID_SIZE = 8;
this.CELL_SIZE = 48;
this.COLORS = [
new pc.Color(1, 0.42, 0.42), // 红
new pc.Color(0.31, 0.8, 0.77), // 青
new pc.Color(0.27, 0.72, 0.82), // 蓝
new pc.Color(0.59, 0.81, 0.71), // 绿
new pc.Color(1, 0.92, 0.65), // 黄
];
// 游戏状态
this.grid = [];
this.score = 0;
this.combo = 0;
this.gameState = 'loading';
this.dragging = false;
this.selectedPiece = null;
this.pieces = [];
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.spawnPieces();
this.createUI();
this.setupInput();
this.startGame();
this.app.on('update', (dt) => this.update(dt));
}
createScene() {
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);
this.uiRoot = new pc.Entity('uiRoot');
this.app.root.addChild(this.uiRoot);
// 游戏板背景
const boardSize = this.GRID_SIZE * this.CELL_SIZE;
const board = new pc.Entity('board');
board.addComponent('render', { type: 'plane' });
board.setLocalScale(boardSize + 10, 1, boardSize + 10);
board.setPosition(0, 0, -1);
const boardMat = new pc.StandardMaterial();
boardMat.diffuse = new pc.Color(0.08, 0.1, 0.15);
boardMat.update();
board.render.material = boardMat;
this.gameRoot.addChild(board);
// 网格线
this.createGridLines();
}
createGridLines() {
const boardSize = this.GRID_SIZE * this.CELL_SIZE;
const halfSize = boardSize / 2;
const lineMat = new pc.StandardMaterial();
lineMat.diffuse = new pc.Color(0.2, 0.25, 0.35);
lineMat.update();
for (let i = 0; i <= this.GRID_SIZE; i++) {
const pos = -halfSize + i * this.CELL_SIZE;
// 横线
const hLine = new pc.Entity('hline');
hLine.addComponent('render', { type: 'box' });
hLine.setLocalScale(boardSize, 1, 1);
hLine.setPosition(0, pos, -0.5);
hLine.render.material = lineMat;
this.gameRoot.addChild(hLine);
// 竖线
const vLine = new pc.Entity('vline');
vLine.addComponent('render', { type: 'box' });
vLine.setLocalScale(1, boardSize, 1);
vLine.setPosition(pos, 0, -0.5);
vLine.render.material = lineMat;
this.gameRoot.addChild(vLine);
}
}
createMaterials() {
this.materials = [];
for (const color of this.COLORS) {
const mat = new pc.StandardMaterial();
mat.diffuse = color;
mat.shininess = 60;
mat.update();
this.materials.push(mat);
}
this.highlightMat = new pc.StandardMaterial();
this.highlightMat.diffuse = new pc.Color(1, 1, 1);
this.highlightMat.emissive = new pc.Color(0.3, 0.3, 0.3);
this.highlightMat.opacity = 0.3;
this.highlightMat.blendType = pc.BLEND_NORMAL;
this.highlightMat.update();
}
initGrid() {
this.grid = [];
for (let y = 0; y < this.GRID_SIZE; y++) {
this.grid[y] = [];
for (let x = 0; x < this.GRID_SIZE; x++) {
this.grid[y][x] = null;
}
}
}
spawnPieces() {
// 清理旧方块
for (const piece of this.pieces) {
piece.entity.destroy();
}
this.pieces = [];
this.initGrid();
// 生成3个随机方块
const shapes = this.getShapes();
for (let i = 0; i < 3; i++) {
this.createPiece(shapes[i], i);
}
}
getShapes() {
const shapes = [
// 单格
[[1]],
// 双格
[[1, 1]],
// 三格直线
[[1, 1, 1]],
// 三格L形
[[1, 0], [1, 1]],
// 四格直线
[[1, 1, 1, 1]],
// 四格方块
[[1, 1], [1, 1]],
// 四格T形
[[1, 1, 1], [0, 1, 0]],
// 四格Z形
[[1, 1, 0], [0, 1, 1]],
// 五格十字
[[0, 1, 0], [1, 1, 1], [0, 1, 0]],
];
// 随机打乱并取前3个
return shapes.sort(() => Math.random() - 0.5).slice(0, 3);
}
createPiece(shape, index) {
const pieceGroup = new pc.Entity('piece' + index);
const cells = [];
const colorIdx = Math.floor(Math.random() * this.COLORS.length);
for (let y = 0; y < shape.length; y++) {
for (let x = 0; x < shape[y].length; x++) {
if (shape[y][x]) {
const cell = new pc.Entity('cell');
cell.addComponent('render', { type: 'box' });
cell.setLocalScale(this.CELL_SIZE - 4, this.CELL_SIZE - 4, 10);
cell.setPosition(
(x - shape[0].length / 2) * this.CELL_SIZE,
-(y - shape.length / 2) * this.CELL_SIZE,
0
);
cell.render.material = this.materials[colorIdx];
pieceGroup.addChild(cell);
cells.push({ x, y, entity: cell });
}
}
}
// 初始位置(底部候选区)
const startX = -160 + index * 160;
const startY = -this.height / 2 + 80;
pieceGroup.setPosition(startX, startY, 5);
this.uiRoot.addChild(pieceGroup);
this.pieces.push({
entity: pieceGroup,
shape: shape,
colorIdx: colorIdx,
cells: cells,
index: index,
originalPos: new pc.Vec3(startX, startY, 5)
});
}
createUI() {
// 分数
this.scoreText = this.createText(`💎 得分: 0`, 0, this.height/2 - 40, 28);
this.uiRoot.addChild(this.scoreText);
// 连击
this.comboText = this.createText('', 0, this.height/2 - 80, 20);
this.uiRoot.addChild(this.comboText);
}
createText(text, x, y, fontSize) {
const entity = new pc.Entity('text');
entity.setPosition(x, y, 10);
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)
});
return entity;
}
setupInput() {
this.app.mouse.on(pc.EVENT_MOUSEDOWN, (e) => this.onInputStart(e.x, e.y));
this.app.mouse.on(pc.EVENT_MOUSEMOVE, (e) => this.onInputMove(e.x, e.y));
this.app.mouse.on(pc.EVENT_MOUSEUP, () => this.onInputEnd());
this.app.touch.on(pc.EVENT_TOUCHSTART, (e) => {
const touch = e.touches[0];
this.onInputStart(touch.x, touch.y);
});
this.app.touch.on(pc.EVENT_TOUCHMOVE, (e) => {
const touch = e.touches[0];
this.onInputMove(touch.x, touch.y);
});
this.app.touch.on(pc.EVENT_TOUCHEND, () => this.onInputEnd());
}
onInputStart(screenX, screenY) {
if (this.gameState !== 'playing') return;
const worldPos = this.screenToWorld(screenX, screenY);
// 检查是否点击了某个方块
for (const piece of this.pieces) {
const pos = piece.entity.getPosition();
const dx = worldPos.x - pos.x;
const dy = worldPos.y - pos.y;
if (Math.abs(dx) < 60 && Math.abs(dy) < 60) {
this.dragging = true;
this.selectedPiece = piece;
piece.entity.setPosition(worldPos.x, worldPos.y, 10);
return;
}
}
}
onInputMove(screenX, screenY) {
if (!this.dragging || !this.selectedPiece) return;
const worldPos = this.screenToWorld(screenX, screenY);
this.selectedPiece.entity.setPosition(worldPos.x, worldPos.y, 10);
}
onInputEnd() {
if (!this.dragging || !this.selectedPiece) return;
const pos = this.selectedPiece.entity.getPosition();
// 检查是否可以放置到网格
const gridPos = this.worldToGrid(pos.x, pos.y);
if (this.canPlace(this.selectedPiece, gridPos.x, gridPos.y)) {
this.placePiece(this.selectedPiece, gridPos.x, gridPos.y);
} else {
// 返回原位置
this.selectedPiece.entity.setPosition(this.selectedPiece.originalPos);
this.selectedPiece.entity.setLocalEulerAngles(0, 0, 0);
}
this.dragging = false;
this.selectedPiece = null;
}
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 };
}
worldToGrid(worldX, worldY) {
const boardSize = this.GRID_SIZE * this.CELL_SIZE;
const halfSize = boardSize / 2;
const x = Math.floor((worldX + halfSize) / this.CELL_SIZE);
const y = Math.floor((halfSize - worldY) / this.CELL_SIZE);
return { x, y };
}
canPlace(piece, gridX, gridY) {
const shape = piece.shape;
for (let y = 0; y < shape.length; y++) {
for (let x = 0; x < shape[y].length; x++) {
if (shape[y][x]) {
const targetX = gridX + x;
const targetY = gridY + y;
if (targetX < 0 || targetX >= this.GRID_SIZE ||
targetY < 0 || targetY >= this.GRID_SIZE ||
this.grid[targetY][targetX] !== null) {
return false;
}
}
}
}
return true;
}
placePiece(piece, gridX, gridY) {
const shape = piece.shape;
const boardSize = this.GRID_SIZE * this.CELL_SIZE;
const halfSize = boardSize / 2;
// 放置到网格
for (let y = 0; y < shape.length; y++) {
for (let x = 0; x < shape[y].length; x++) {
if (shape[y][x]) {
const targetX = gridX + x;
const targetY = gridY + y;
this.grid[targetY][targetX] = piece.colorIdx;
// 创建静态方块
const cell = new pc.Entity('placed');
cell.addComponent('render', { type: 'box' });
cell.setLocalScale(this.CELL_SIZE - 4, this.CELL_SIZE - 4, 10);
cell.setPosition(
-halfSize + targetX * this.CELL_SIZE + this.CELL_SIZE / 2,
halfSize - targetY * this.CELL_SIZE - this.CELL_SIZE / 2,
0
);
cell.render.material = this.materials[piece.colorIdx];
this.gameRoot.addChild(cell);
}
}
}
// 移除拖动的方块
piece.entity.destroy();
this.pieces = this.pieces.filter(p => p !== piece);
// 计分
const cellCount = shape.flat().filter(c => c).length;
this.score += cellCount * 10;
// 检查消除
this.checkLines();
// 检查是否还有可放置的位置
if (this.pieces.length === 0) {
this.spawnPieces();
} else if (!this.hasValidMoves()) {
this.gameOver();
}
this.updateUI();
}
checkLines() {
const linesToClear = [];
// 检查行
for (let y = 0; y < this.GRID_SIZE; y++) {
if (this.grid[y].every(cell => cell !== null)) {
linesToClear.push({ type: 'row', index: y });
}
}
// 检查列
for (let x = 0; x < this.GRID_SIZE; x++) {
if (this.grid.every(row => row[x] !== null)) {
linesToClear.push({ type: 'col', index: x });
}
}
// 执行消除
if (linesToClear.length > 0) {
this.combo++;
const baseScore = linesToClear.length * this.GRID_SIZE * 10;
const comboBonus = this.combo * 50;
this.score += baseScore + comboBonus;
// 清除行/列
for (const line of linesToClear) {
if (line.type === 'row') {
for (let x = 0; x < this.GRID_SIZE; x++) {
this.grid[line.index][x] = null;
}
} else {
for (let y = 0; y < this.GRID_SIZE; y++) {
this.grid[y][line.index] = null;
}
}
}
// 重建网格显示
this.rebuildGrid();
// 显示连击
this.comboText.element.text = `🔥 ${this.combo}连击! +${comboBonus}`;
setTimeout(() => {
this.comboText.element.text = '';
}, 1500);
} else {
this.combo = 0;
}
}
rebuildGrid() {
// 清除所有已放置的方块实体
const placed = this.gameRoot.findByName('placed');
while (placed) {
placed.destroy();
}
// 根据grid重新创建
const boardSize = this.GRID_SIZE * this.CELL_SIZE;
const halfSize = boardSize / 2;
for (let y = 0; y < this.GRID_SIZE; y++) {
for (let x = 0; x < this.GRID_SIZE; x++) {
if (this.grid[y][x] !== null) {
const cell = new pc.Entity('placed');
cell.addComponent('render', { type: 'box' });
cell.setLocalScale(this.CELL_SIZE - 4, this.CELL_SIZE - 4, 10);
cell.setPosition(
-halfSize + x * this.CELL_SIZE + this.CELL_SIZE / 2,
halfSize - y * this.CELL_SIZE - this.CELL_SIZE / 2,
0
);
cell.render.material = this.materials[this.grid[y][x]];
this.gameRoot.addChild(cell);
}
}
}
}
hasValidMoves() {
for (const piece of this.pieces) {
for (let y = 0; y < this.GRID_SIZE; y++) {
for (let x = 0; x < this.GRID_SIZE; x++) {
if (this.canPlace(piece, x, y)) {
return true;
}
}
}
}
return false;
}
gameOver() {
this.gameState = 'gameover';
this.poki.gameplayStop();
// 显示结束面板
this.showGameOverUI();
}
showGameOverUI() {
const panel = new pc.Entity('gameOverPanel');
panel.setPosition(0, 0, 20);
this.uiRoot.addChild(panel);
// 背景
const bg = new pc.Entity('bg');
bg.addComponent('render', { type: 'plane' });
bg.setLocalScale(300, 1, 250);
const bgMat = new pc.StandardMaterial();
bgMat.diffuse = new pc.Color(0.1, 0.12, 0.2);
bgMat.opacity = 0.95;
bgMat.blendType = pc.BLEND_NORMAL;
bgMat.update();
bg.render.material = bgMat;
panel.addChild(bg);
// 标题
const title = this.createText('游戏结束', 0, 60, 36);
panel.addChild(title);
// 分数
const score = this.createText(`最终得分: ${this.score}`, 0, 0, 24);
panel.addChild(score);
// 提示
const hint = this.createText('点击重新开始', 0, -60, 16);
hint.element.color = new pc.Color(0.7, 0.8, 1);
panel.addChild(hint);
this.gameOverPanel = panel;
// 点击重启
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.score = 0;
this.combo = 0;
this.grid = [];
this.initGrid();
this.rebuildGrid();
this.spawnPieces();
this.updateUI();
this.startGame();
}
updateUI() {
this.scoreText.element.text = `💎 得分: ${this.score}`;
}
startGame() {
this.gameState = 'playing';
this.poki.gameplayStart();
}
update(dt) {
// 方块浮动动画
const time = Date.now() * 0.002;
for (const piece of this.pieces) {
if (piece !== this.selectedPiece) {
const pos = piece.originalPos.clone();
pos.y += Math.sin(time + piece.index) * 3;
piece.entity.setPosition(pos);
}
}
}
}
const game = new BlockyBlastGame();
</script>
<!-- 登录认证 -->
<script src="../auth.js?v=1"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
GAME_AUTH.init('blocky-blast', '方块拼图');
});
</script>
</body>
</html>