Files
cocos-hehe/index.html
leiyuwei f7509f9c4d feat: 合成大作战 Cocos Creator merge game
8x12 棋盘合成小游戏,不同颜色方块代表不同等级
- 点击选中方块,点击相邻同等级方块合成升级
- 自动生成新方块,支持链式合成
- Cocos Creator 3.x 项目 + 独立 HTML 可玩版本

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 23:51:35 +08:00

449 lines
12 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>合成大作战</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; user-select: none; -webkit-user-select: none; }
body {
background: #191923;
display: flex;
justify-content: center;
align-items: center;
min-height: 100dvh;
font-family: 'Segoe UI', 'PingFang SC', sans-serif;
touch-action: manipulation;
}
#gameContainer {
position: relative;
width: 480px;
max-width: 100vw;
aspect-ratio: 480 / 860;
}
canvas {
display: block;
width: 100%;
height: 100%;
border-radius: 12px;
background: #191923;
touch-action: none;
}
@media (max-width: 500px) {
#gameContainer { width: 100vw; }
}
</style>
</head>
<body>
<div id="gameContainer">
<canvas id="gameCanvas"></canvas>
</div>
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
// ============ 配置 ============
const ROWS = 12;
const COLS = 8;
const CELL = 52;
const GAP = 6;
const PAD = 10;
const BOARD_W = COLS * (CELL + GAP) + GAP;
const BOARD_H = ROWS * (CELL + GAP) + GAP;
const UI_TOP = 80;
const CANVAS_W = 480;
const CANVAS_H = 860;
canvas.width = CANVAS_W;
canvas.height = CANVAS_H;
const BLOCK_COLORS = [
'#A8E6CF', // Lv1 草
'#4CAF50', // Lv2 木
'#2196F3', // Lv3 水
'#9C27B0', // Lv4 风
'#FF9800', // Lv5 火
'#F44336', // Lv6 雷
'#E91E63', // Lv7 光
'#FFD700', // Lv8 神
];
const LEVEL_NAMES = ['草','木','水','风','火','雷','光','神'];
const SPAWN_INTERVAL = 3500; // ms
const COLORS_TEXT = '#ffffff';
const COLORS_BG = '#191923';
// ============ 游戏状态 ============
let grid = [];
let score = 0;
let gameOver = false;
let selected = null; // {row, col}
let lastSpawn = 0;
let animating = false;
let boardX, boardY;
function initBoard() {
grid = [];
for (let r = 0; r < ROWS; r++) {
grid[r] = [];
for (let c = 0; c < COLS; c++) {
grid[r][c] = null;
}
}
}
// ============ 渲染 ============
function resize() {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
boardX = (CANVAS_W - BOARD_W) / 2;
boardY = UI_TOP;
}
function draw() {
ctx.clearRect(0, 0, CANVAS_W, CANVAS_H);
// 背景装饰
ctx.fillStyle = '#1a1a2e';
ctx.fillRect(0, 0, CANVAS_W, CANVAS_H);
// 顶栏背景
ctx.fillStyle = '#2a2a3e';
ctx.beginPath();
roundRect(ctx, boardX - 10, 8, BOARD_W + 20, 60, 10);
ctx.fill();
// 标题
ctx.fillStyle = '#888';
ctx.font = '16px "PingFang SC", sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('合成大作战', CANVAS_W / 2, 28);
// 分数
ctx.fillStyle = '#fff';
ctx.font = 'bold 26px "PingFang SC", sans-serif';
ctx.fillText(`得分: ${score}`, CANVAS_W / 2, 56);
// 棋盘背景
ctx.fillStyle = '#22223a';
ctx.beginPath();
roundRect(ctx, boardX - 6, boardY - 6, BOARD_W + 12, BOARD_H + 12, 8);
ctx.fill();
// 格子背景
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
const x = boardX + GAP + c * (CELL + GAP);
const y = boardY + GAP + r * (CELL + GAP);
ctx.fillStyle = '#2a2a42';
ctx.beginPath();
roundRect(ctx, x, y, CELL, CELL, 6);
ctx.fill();
}
}
// 方块
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
const block = grid[r][c];
if (block) drawBlock(r, c, block);
}
}
// 选中高亮
if (selected) {
const { row, col } = selected;
const x = boardX + GAP + col * (CELL + GAP);
const y = boardY + GAP + row * (CELL + GAP);
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 3;
ctx.beginPath();
roundRect(ctx, x - 2, y - 2, CELL + 4, CELL + 4, 8);
ctx.stroke();
}
// 游戏结束遮罩
if (gameOver) {
ctx.fillStyle = 'rgba(0,0,0,0.7)';
ctx.fillRect(0, 0, CANVAS_W, CANVAS_H);
ctx.fillStyle = '#ffc832';
ctx.font = 'bold 40px "PingFang SC", sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('游戏结束', CANVAS_W / 2, CANVAS_H / 2 - 60);
ctx.fillStyle = '#fff';
ctx.font = '28px "PingFang SC", sans-serif';
ctx.fillText(`最终得分: ${score}`, CANVAS_W / 2, CANVAS_H / 2);
// 重新开始按钮
const bx = CANVAS_W / 2 - 90, by = CANVAS_H / 2 + 50, bw = 180, bh = 54;
ctx.fillStyle = '#4CAF50';
ctx.beginPath();
roundRect(ctx, bx, by, bw, bh, 12);
ctx.fill();
ctx.fillStyle = '#fff';
ctx.font = '22px "PingFang SC", sans-serif';
ctx.fillText('重新开始', CANVAS_W / 2, by + bh / 2);
}
}
function roundRect(ctx, x, y, w, h, r) {
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
ctx.lineTo(x + w, y + h - r);
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
ctx.lineTo(x + r, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
ctx.lineTo(x, y + r);
ctx.quadraticCurveTo(x, y, x + r, y);
ctx.closePath();
}
function drawBlock(r, c, block) {
const x = boardX + GAP + c * (CELL + GAP);
const y = boardY + GAP + r * (CELL + GAP);
const lvl = block.level;
const color = BLOCK_COLORS[Math.min(lvl - 1, BLOCK_COLORS.length - 1)];
// 方块主体
ctx.fillStyle = color;
ctx.beginPath();
roundRect(ctx, x, y, CELL, CELL, 8);
ctx.fill();
// 高光
ctx.fillStyle = 'rgba(255,255,255,0.2)';
ctx.beginPath();
roundRect(ctx, x + 4, y + 4, CELL - 8, CELL / 3, 6);
ctx.fill();
// 等级文字
ctx.fillStyle = lvl >= 7 ? '#333' : '#fff';
ctx.font = `bold ${20 + lvl}px "PingFang SC", sans-serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(`${lvl}`, x + CELL / 2, y + CELL / 2 - 4);
// 等级名称小字
ctx.fillStyle = 'rgba(255,255,255,0.6)';
ctx.font = '10px "PingFang SC", sans-serif';
const name = LEVEL_NAMES[Math.min(lvl - 1, LEVEL_NAMES.length - 1)];
ctx.fillText(name, x + CELL / 2, y + CELL / 2 + 16);
}
// ============ 游戏逻辑 ============
function getRandomLevel() {
const roll = Math.random();
if (roll < 0.45) return 1;
if (roll < 0.75) return 2;
if (roll < 0.90) return 3;
if (roll < 0.97) return 4;
return 5;
}
function findEmptyCells() {
const cells = [];
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
if (!grid[r][c]) cells.push({row: r, col: c});
}
}
return cells;
}
function spawnBlock() {
const empty = findEmptyCells();
if (empty.length === 0) {
gameOver = true;
return null;
}
const cell = empty[Math.floor(Math.random() * empty.length)];
grid[cell.row][cell.col] = { level: getRandomLevel() };
return cell;
}
function isAdjacent(r1, c1, r2, c2) {
const dr = Math.abs(r1 - r2);
const dc = Math.abs(c1 - c2);
return (dr === 1 && dc === 0) || (dr === 0 && dc === 1);
}
function getAdjacentBlocks(row, col) {
const result = [];
const dirs = [[-1,0],[1,0],[0,-1],[0,1]];
for (const [dr, dc] of dirs) {
const r = row + dr, c = col + dc;
if (r >= 0 && r < ROWS && c >= 0 && c < COLS && grid[r][c]) {
result.push({row: r, col: c, block: grid[r][c]});
}
}
return result;
}
function doMerge(r1, c1, r2, c2) {
const lvl = Math.min(grid[r1][c1].level + 1, 8);
score += lvl * 10;
grid[r2][c2] = null;
grid[r1][c1] = { level: lvl };
selected = null;
// 链式合并检测
setTimeout(() => {
const adj = getAdjacentBlocks(r1, c1);
for (const a of adj) {
if (a.block.level === lvl) {
doMerge(r1, c1, a.row, a.col);
return;
}
}
}, 200);
}
function tryMerge(r1, c1, r2, c2) {
if (!grid[r1][c1] || !grid[r2][c2]) return false;
if (grid[r1][c1].level !== grid[r2][c2].level) return false;
if (!isAdjacent(r1, c1, r2, c2)) return false;
doMerge(r1, c1, r2, c2);
return true;
}
function moveBlock(fromR, fromC, toR, toC) {
if (grid[toR][toC]) return false;
grid[toR][toC] = grid[fromR][fromC];
grid[fromR][fromC] = null;
return true;
}
// ============ 输入处理 ============
function getGridPos(clientX, clientY) {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const mx = (clientX - rect.left) * scaleX;
const my = (clientY - rect.top) * scaleY;
const gx = mx - boardX;
const gy = my - boardY;
const col = Math.floor((gx - GAP) / (CELL + GAP));
const row = Math.floor((gy - GAP) / (CELL + GAP));
if (row < 0 || row >= ROWS || col < 0 || col >= COLS) return null;
return {row, col};
}
function isRestartHit(clientX, clientY) {
if (!gameOver) return false;
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const mx = (clientX - rect.left) * scaleX;
const my = (clientY - rect.top) * scaleY;
const bx = CANVAS_W / 2 - 90;
const by = CANVAS_H / 2 + 50;
const bw = 180, bh = 54;
return mx >= bx && mx <= bx + bw && my >= by && my <= by + bh;
}
function handleTap(clientX, clientY) {
if (gameOver) {
if (isRestartHit(clientX, clientY)) {
restartGame();
}
return;
}
const pos = getGridPos(clientX, clientY);
if (!pos) { selected = null; return; }
const {row, col} = pos;
const tapped = grid[row][col];
if (tapped) {
if (selected) {
const s = selected;
if (tryMerge(s.row, s.col, row, col)) return;
selected = {row, col};
} else {
selected = {row, col};
}
} else {
if (selected) {
const s = selected;
if (s.row !== row || s.col !== col) {
// 尝试移动
if (isAdjacent(s.row, s.col, row, col)) {
if (moveBlock(s.row, s.col, row, col)) {
selected = null;
// 检查移动后是否有合并
setTimeout(() => {
const adj = getAdjacentBlocks(row, col);
for (const a of adj) {
if (a.block.level === grid[row][col].level) {
tryMerge(row, col, a.row, a.col);
return;
}
}
}, 150);
return;
}
}
}
selected = null;
} else {
selected = null;
}
}
}
// ============ 游戏循环 ============
let lastTime = 0;
function gameLoop(timestamp) {
if (!lastTime) lastTime = timestamp;
if (!gameOver) {
if (timestamp - lastSpawn > SPAWN_INTERVAL) {
lastSpawn = timestamp;
spawnBlock();
}
}
draw();
requestAnimationFrame(gameLoop);
}
function restartGame() {
initBoard();
score = 0;
gameOver = false;
selected = null;
lastSpawn = performance.now();
// 初始生成3个
for (let i = 0; i < 3; i++) spawnBlock();
}
// ============ 事件绑定 ============
canvas.addEventListener('click', (e) => {
handleTap(e.clientX, e.clientY);
});
canvas.addEventListener('touchstart', (e) => {
e.preventDefault();
const touch = e.changedTouches[0];
handleTap(touch.clientX, touch.clientY);
}, {passive: false});
// ============ 启动 ============
resize();
initBoard();
lastSpawn = performance.now();
for (let i = 0; i < 3; i++) spawnBlock();
requestAnimationFrame(gameLoop);
</script>
</body>
</html>