686 lines
25 KiB
HTML
686 lines
25 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>🎮 俄罗斯方块 | 最终版</title>
|
|
<link rel="icon" href="favicon.ico" type="image/x-icon">
|
|
|
|
<!-- PlayCanvas 1.66.0 - 和贪吃蛇完全一样 -->
|
|
<script src="https://code.playcanvas.com/playcanvas-1.66.0.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;
|
|
}
|
|
#application {
|
|
width: 100vw;
|
|
height: 100vh;
|
|
display: block;
|
|
}
|
|
|
|
/* UI层 */
|
|
#ui {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
pointer-events: none;
|
|
z-index: 100;
|
|
}
|
|
|
|
#score {
|
|
position: absolute;
|
|
top: 20px;
|
|
left: 20px;
|
|
color: white;
|
|
font-size: 24px;
|
|
font-weight: bold;
|
|
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
|
|
}
|
|
|
|
#level {
|
|
position: absolute;
|
|
top: 50px;
|
|
left: 20px;
|
|
color: #aaa;
|
|
font-size: 14px;
|
|
}
|
|
|
|
#lines {
|
|
position: absolute;
|
|
top: 70px;
|
|
left: 20px;
|
|
color: #aaa;
|
|
font-size: 14px;
|
|
}
|
|
|
|
#menu {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
text-align: center;
|
|
pointer-events: auto;
|
|
}
|
|
|
|
#menu h1 {
|
|
color: #00d4ff;
|
|
font-size: 48px;
|
|
margin-bottom: 20px;
|
|
text-shadow: 0 0 20px rgba(0,212,255,0.5);
|
|
}
|
|
|
|
.btn {
|
|
padding: 15px 40px;
|
|
font-size: 20px;
|
|
background: linear-gradient(135deg, #667eea, #764ba2);
|
|
color: white;
|
|
border: none;
|
|
border-radius: 30px;
|
|
cursor: pointer;
|
|
margin: 10px;
|
|
transition: transform 0.2s, box-shadow 0.2s;
|
|
}
|
|
|
|
.btn:hover {
|
|
transform: scale(1.05);
|
|
box-shadow: 0 10px 30px rgba(102,126,234,0.4);
|
|
}
|
|
|
|
#gameOver {
|
|
display: none;
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
text-align: center;
|
|
background: rgba(0,0,0,0.8);
|
|
padding: 40px;
|
|
border-radius: 20px;
|
|
pointer-events: auto;
|
|
}
|
|
|
|
#gameOver h2 {
|
|
color: #ff6b6b;
|
|
font-size: 36px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
#finalScore {
|
|
color: white;
|
|
font-size: 24px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
/* 虚拟控制按钮 */
|
|
.controls-container {
|
|
position: absolute;
|
|
bottom: 30px;
|
|
left: 0;
|
|
width: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 15px;
|
|
pointer-events: auto;
|
|
}
|
|
|
|
.control-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
width: 100%;
|
|
max-width: 500px;
|
|
}
|
|
|
|
.control-button {
|
|
width: 65px;
|
|
height: 65px;
|
|
background: linear-gradient(145deg, #1e293b 0%, #0f172a 100%);
|
|
border-radius: 50%;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
font-size: 28px;
|
|
color: #fff;
|
|
cursor: pointer;
|
|
user-select: none;
|
|
border: 2px solid rgba(255, 255, 255, 0.1);
|
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
|
|
transition: all 0.1s ease;
|
|
}
|
|
|
|
.control-row:nth-child(2) {
|
|
justify-content: center;
|
|
gap: 5px;
|
|
}
|
|
|
|
.loading {
|
|
position: fixed;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
color: white;
|
|
font-size: 18px;
|
|
text-align: center;
|
|
}
|
|
|
|
.debug-info {
|
|
position: fixed;
|
|
bottom: 10px;
|
|
left: 10px;
|
|
background: rgba(0, 0, 0, 0.8);
|
|
color: #68d391;
|
|
padding: 10px;
|
|
border-radius: 5px;
|
|
font-family: monospace;
|
|
font-size: 12px;
|
|
max-width: 300px;
|
|
max-height: 200px;
|
|
overflow-y: auto;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="loading" id="loading">🎮 加载俄罗斯方块...</div>
|
|
|
|
<canvas id="application" width="540" height="720"></canvas>
|
|
|
|
<div id="ui">
|
|
<div id="score">分数: 0</div>
|
|
<div id="level">等级: 0</div>
|
|
<div id="lines">消行: 0</div>
|
|
|
|
<div id="menu">
|
|
<h1>🎮 俄罗斯方块</h1>
|
|
<button class="btn" onclick="startGame()">▶️ 开始游戏</button>
|
|
<button class="btn" onclick="showInstructions()">📖 游戏说明</button>
|
|
</div>
|
|
|
|
<div id="gameOver">
|
|
<h2>游戏结束!</h2>
|
|
<div id="finalScore">最终得分: 0</div>
|
|
<button class="btn" onclick="restartGame()">🔄 重新开始</button>
|
|
<button class="btn" onclick="backToMenu()">🏠 返回菜单</button>
|
|
</div>
|
|
|
|
<div class="controls-container">
|
|
<!-- 第一行:方向控制按钮 -->
|
|
<div class="control-row">
|
|
<div class="control-button left" id="left-btn">←</div>
|
|
<div class="control-button down" id="down-btn">↓</div>
|
|
<div class="control-button right" id="right-btn">→</div>
|
|
</div>
|
|
|
|
<!-- 第二行:旋转按钮 -->
|
|
<div class="control-row">
|
|
<div class="control-button rotate" id="rotate-btn">↻ 旋转</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="debug-info" id="debug-info">
|
|
<div>调试信息:</div>
|
|
</div>
|
|
|
|
<script>
|
|
// ==================== 调试日志 ====================
|
|
function debugLog(message) {
|
|
const debugEl = document.getElementById('debug-info');
|
|
const logEntry = document.createElement('div');
|
|
logEntry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
|
|
debugEl.appendChild(logEntry);
|
|
debugEl.scrollTop = debugEl.scrollHeight;
|
|
console.log(message);
|
|
}
|
|
|
|
// ==================== PlayCanvas 游戏 ====================
|
|
function initGame() {
|
|
debugLog('🎮 初始化 PlayCanvas...');
|
|
|
|
const canvas = document.getElementById('application');
|
|
if (!canvas) {
|
|
debugLog('❌ Canvas 元素未找到!');
|
|
alert('游戏加载失败:Canvas 未找到');
|
|
return;
|
|
}
|
|
|
|
let app;
|
|
try {
|
|
app = new pc.Application(canvas, {
|
|
mouse: new pc.Mouse(canvas),
|
|
keyboard: new pc.Keyboard(window),
|
|
touch: new pc.TouchDevice(canvas),
|
|
elementInput: new pc.ElementInput(canvas)
|
|
});
|
|
debugLog('✅ PlayCanvas 应用创建成功');
|
|
} catch (e) {
|
|
debugLog(`❌ PlayCanvas 初始化失败: ${e.message}`);
|
|
alert('游戏引擎初始化失败: ' + e.message);
|
|
throw e;
|
|
}
|
|
|
|
app.start();
|
|
debugLog('✅ PlayCanvas 已启动');
|
|
|
|
app.setCanvasFillMode(pc.FILLMODE_FILL_WINDOW);
|
|
app.setCanvasResolution(pc.RESOLUTION_AUTO);
|
|
|
|
window.addEventListener('resize', () => {
|
|
try {
|
|
app.resizeCanvas();
|
|
debugLog('✅ 画布大小已调整');
|
|
} catch (e) {
|
|
debugLog(`❌ 画布调整失败: ${e.message}`);
|
|
}
|
|
});
|
|
|
|
// ==================== 游戏状态 ====================
|
|
let gameRunning = false;
|
|
let score = 0;
|
|
let level = 0;
|
|
let lines = 0;
|
|
let gameSpeed = 1.0;
|
|
let isPaused = false;
|
|
let isGameOver = false;
|
|
|
|
// 游戏场地
|
|
const gridWidth = 10;
|
|
const gridHeight = 20;
|
|
const cellSize = 24;
|
|
let board = Array(gridHeight).fill().map(() => Array(gridWidth).fill(0));
|
|
|
|
// 当前方块
|
|
let currentPiece = null;
|
|
|
|
// 方块类型
|
|
const pieceTypes = [
|
|
{ type: 'I', shape: [[1,1,1,1]], color: new pc.Color(0, 0.96, 1) },
|
|
{ type: 'J', shape: [[1,0,0],[1,1,1]], color: new pc.Color(0, 0, 1) },
|
|
{ type: 'L', shape: [[0,0,1],[1,1,1]], color: new pc.Color(1, 0.5, 0) },
|
|
{ type: 'O', shape: [[1,1],[1,1]], color: new pc.Color(1, 1, 0) },
|
|
{ type: 'S', shape: [[0,1,1],[1,1,0]], color: new pc.Color(0, 1, 0) },
|
|
{ type: 'T', shape: [[0,1,0],[1,1,1]], color: new pc.Color(0.5, 0, 0.5) },
|
|
{ type: 'Z', shape: [[1,1,0],[0,1,1]], color: new pc.Color(1, 0, 0) }
|
|
];
|
|
|
|
// ==================== 游戏函数 ====================
|
|
function spawnNewPiece() {
|
|
const randomType = pieceTypes[Math.floor(Math.random() * pieceTypes.length)];
|
|
currentPiece = {
|
|
type: randomType.type,
|
|
shape: randomType.shape,
|
|
color: randomType.color,
|
|
x: Math.floor(gridWidth / 2) - Math.floor(randomType.shape[0].length / 2),
|
|
y: 0
|
|
};
|
|
debugLog(`生成新方块: ${currentPiece.type}`);
|
|
}
|
|
|
|
function isValidMove(shape, x, y) {
|
|
for (let row = 0; row < shape.length; row++) {
|
|
for (let col = 0; col < shape[row].length; col++) {
|
|
if (shape[row][col]) {
|
|
const newX = x + col;
|
|
const newY = y + row;
|
|
|
|
if (newX < 0 || newX >= gridWidth || newY >= gridHeight) {
|
|
return false;
|
|
}
|
|
|
|
if (newY >= 0 && board[newY][newX]) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function movePiece(dx, dy) {
|
|
if (!currentPiece || isPaused || isGameOver) return;
|
|
|
|
const newX = currentPiece.x + dx;
|
|
const newY = currentPiece.y + dy;
|
|
|
|
if (isValidMove(currentPiece.shape, newX, newY)) {
|
|
currentPiece.x = newX;
|
|
currentPiece.y = newY;
|
|
} else if (dy > 0) {
|
|
lockPiece();
|
|
checkLines();
|
|
spawnNewPiece();
|
|
|
|
if (!isValidMove(currentPiece.shape, currentPiece.x, currentPiece.y)) {
|
|
gameOver();
|
|
}
|
|
}
|
|
}
|
|
|
|
function lockPiece() {
|
|
const { shape, x, y, color } = currentPiece;
|
|
|
|
for (let row = 0; row < shape.length; row++) {
|
|
for (let col = 0; col < shape[row].length; col++) {
|
|
if (shape[row][col]) {
|
|
const boardX = x + col;
|
|
const boardY = y + row;
|
|
|
|
if (boardY >= 0) {
|
|
board[boardY][boardX] = color;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function checkLines() {
|
|
let linesCleared = 0;
|
|
|
|
for (let y = gridHeight - 1; y >= 0; y--) {
|
|
if (board[y].every(cell => cell !== 0)) {
|
|
board.splice(y, 1);
|
|
board.unshift(Array(gridWidth).fill(0));
|
|
linesCleared++;
|
|
y++;
|
|
}
|
|
}
|
|
|
|
if (linesCleared > 0) {
|
|
lines += linesCleared;
|
|
score += linesCleared * 100 * (level + 1);
|
|
level = Math.floor(lines / 10);
|
|
gameSpeed = 1.0 + level * 0.1;
|
|
|
|
updateUI();
|
|
debugLog(`消除 ${linesCleared} 行,得分: ${linesCleared * 100 * (level + 1)}`);
|
|
}
|
|
}
|
|
|
|
function rotatePiece() {
|
|
if (!currentPiece || isPaused || isGameOver) return;
|
|
|
|
const rotateMatrix = (matrix) => {
|
|
const result = [];
|
|
for (let i = 0; i < matrix[0].length; i++) {
|
|
result[i] = [];
|
|
for (let j = 0; j < matrix.length; j++) {
|
|
result[i][j] = matrix[matrix.length - 1 - j][i];
|
|
}
|
|
}
|
|
return result;
|
|
};
|
|
|
|
const rotated = rotateMatrix(currentPiece.shape);
|
|
if (isValidMove(rotated, currentPiece.x, currentPiece.y)) {
|
|
currentPiece.shape = rotated;
|
|
debugLog('旋转方块');
|
|
}
|
|
}
|
|
|
|
function gameOver() {
|
|
isGameOver = true;
|
|
gameRunning = false;
|
|
|
|
document.getElementById('finalScore').textContent = `最终得分: ${score}`;
|
|
document.getElementById('gameOver').style.display = 'block';
|
|
debugLog(`游戏结束!最终得分: ${score}`);
|
|
}
|
|
|
|
function updateUI() {
|
|
document.getElementById('score').textContent = `分数: ${score}`;
|
|
document.getElementById('level').textContent = `等级: ${level}`;
|
|
document.getElementById('lines').textContent = `消行: ${lines}`;
|
|
}
|
|
|
|
// ==================== 渲染函数 ====================
|
|
function render() {
|
|
const ctx = app.graphicsDevice.gl;
|
|
if (!ctx) return;
|
|
|
|
ctx.clearColor(0.1, 0.1, 0.15, 1.0);
|
|
ctx.clear(ctx.COLOR_BUFFER_BIT);
|
|
|
|
// 使用2D Canvas渲染游戏
|
|
const canvasCtx = canvas.getContext('2d');
|
|
if (!canvasCtx) return;
|
|
|
|
// 清空画布
|
|
canvasCtx.fillStyle = '#111827';
|
|
canvasCtx.fillRect(0, 0, canvas.width, canvas.height);
|
|
|
|
// 绘制网格
|
|
canvasCtx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
|
|
canvasCtx.lineWidth = 1;
|
|
|
|
for (let x = 0; x <= gridWidth; x++) {
|
|
canvasCtx.beginPath();
|
|
canvasCtx.moveTo(x * cellSize, 0);
|
|
canvasCtx.lineTo(x * cellSize, canvas.height);
|
|
canvasCtx.stroke();
|
|
}
|
|
|
|
for (let y = 0; y <= gridHeight; y++) {
|
|
canvasCtx.beginPath();
|
|
canvasCtx.moveTo(0, y * cellSize);
|
|
canvasCtx.lineTo(canvas.width, y * cellSize);
|
|
canvasCtx.stroke();
|
|
}
|
|
|
|
// 绘制已固定的方块
|
|
for (let y = 0; y < gridHeight; y++) {
|
|
for (let x = 0; x < gridWidth; x++) {
|
|
if (board[y][x]) {
|
|
const color = board[y][x];
|
|
canvasCtx.fillStyle = `rgb(${Math.floor(color.r * 255)}, ${Math.floor(color.g * 255)}, ${Math.floor(color.b * 255)})`;
|
|
canvasCtx.fillRect(x * cellSize, y * cellSize, cellSize, cellSize);
|
|
|
|
canvasCtx.strokeStyle = 'white';
|
|
canvasCtx.lineWidth = 2;
|
|
canvasCtx.strokeRect(x * cellSize, y * cellSize, cellSize, cellSize);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 绘制当前方块
|
|
if (currentPiece) {
|
|
const { shape, x, y, color } = currentPiece;
|
|
|
|
canvasCtx.fillStyle = `rgb(${Math.floor(color.r * 255)}, ${Math.floor(color.g * 255)}, ${Math.floor(color.b * 255)})`;
|
|
for (let row = 0; row < shape.length; row++) {
|
|
for (let col = 0; col < shape[row].length; col++) {
|
|
if (shape[row][col]) {
|
|
canvasCtx.fillRect((x + col) * cellSize, (y + row) * cellSize, cellSize, cellSize);
|
|
|
|
canvasCtx.strokeStyle = 'white';
|
|
canvasCtx.lineWidth = 2;
|
|
canvasCtx.strokeRect((x + col) * cellSize, (y + row) * cellSize, cellSize, cellSize);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ==================== 游戏循环 ====================
|
|
let dropTimer = 0;
|
|
let lastFrameTime = 0;
|
|
|
|
app.on('update', (dt) => {
|
|
try {
|
|
if (gameRunning && !isPaused && !isGameOver) {
|
|
const now = performance.now();
|
|
|
|
if (!lastFrameTime) lastFrameTime = now;
|
|
|
|
// 累积时间
|
|
dropTimer += dt * 1000; // dt是秒,转换为毫秒
|
|
|
|
// 每1000ms/游戏速度下落一次
|
|
if (dropTimer > 1000 / gameSpeed) {
|
|
movePiece(0, 1);
|
|
dropTimer = 0;
|
|
}
|
|
|
|
// 每帧都渲染
|
|
render();
|
|
|
|
lastFrameTime = now;
|
|
}
|
|
} catch (e) {
|
|
debugLog(`❌ 游戏循环错误: ${e.message}`);
|
|
}
|
|
});
|
|
|
|
// ==================== 输入控制 ====================
|
|
function setupInput() {
|
|
debugLog('设置输入控制...');
|
|
|
|
// 方向按钮
|
|
document.getElementById('left-btn').addEventListener('click', () => {
|
|
if (gameRunning) movePiece(-1, 0);
|
|
});
|
|
|
|
document.getElementById('right-btn').addEventListener('click', () => {
|
|
if (gameRunning) movePiece(1, 0);
|
|
});
|
|
|
|
document.getElementById('down-btn').addEventListener('click', () => {
|
|
if (gameRunning) movePiece(0, 1);
|
|
});
|
|
|
|
document.getElementById('rotate-btn').addEventListener('click', () => {
|
|
if (gameRunning) rotatePiece();
|
|
});
|
|
|
|
// 键盘控制
|
|
document.addEventListener('keydown', (e) => {
|
|
if (!gameRunning || isPaused || isGameOver) return;
|
|
|
|
switch(e.key) {
|
|
case 'ArrowLeft':
|
|
movePiece(-1, 0);
|
|
break;
|
|
case 'ArrowRight':
|
|
movePiece(1, 0);
|
|
break;
|
|
case 'ArrowDown':
|
|
movePiece(0, 1);
|
|
break;
|
|
case 'ArrowUp':
|
|
case ' ':
|
|
rotatePiece();
|
|
break;
|
|
case 'p':
|
|
case 'P':
|
|
togglePause();
|
|
break;
|
|
}
|
|
});
|
|
|
|
debugLog('✅ 输入控制设置完成');
|
|
}
|
|
|
|
function togglePause() {
|
|
isPaused = !isPaused;
|
|
debugLog(isPaused ? '⏸️ 游戏暂停' : '▶️ 游戏继续');
|
|
}
|
|
|
|
// ==================== 游戏控制函数 ====================
|
|
window.startGame = function() {
|
|
debugLog('🎮 开始游戏...');
|
|
|
|
// 重置游戏状态
|
|
board = Array(gridHeight).fill().map(() => Array(gridWidth).fill(0));
|
|
score = 0;
|
|
level = 0;
|
|
lines = 0;
|
|
gameSpeed = 1.0;
|
|
isPaused = false;
|
|
isGameOver = false;
|
|
dropTimer = 0;
|
|
lastFrameTime = 0;
|
|
|
|
// 生成第一个方块
|
|
spawnNewPiece();
|
|
|
|
// 更新UI
|
|
updateUI();
|
|
|
|
// 隐藏菜单
|
|
document.getElementById('menu').style.display = 'none';
|
|
document.getElementById('gameOver').style.display = 'none';
|
|
|
|
// 开始游戏
|
|
gameRunning = true;
|
|
|
|
debugLog('✅ 游戏开始!');
|
|
};
|
|
|
|
window.restartGame = function() {
|
|
debugLog('🔄 重新开始游戏');
|
|
startGame();
|
|
};
|
|
|
|
window.backToMenu = function() {
|
|
debugLog('🏠 返回菜单');
|
|
gameRunning = false;
|
|
document.getElementById('gameOver').style.display = 'none';
|
|
document.getElementById('menu').style.display = 'block';
|
|
};
|
|
|
|
window.showInstructions = function() {
|
|
alert('🎮 俄罗斯方块游戏说明:\n\n' +
|
|
'← → ↓:移动方块\n' +
|
|
'↑ 或 空格:旋转方块\n' +
|
|
'P:暂停/继续游戏\n\n' +
|
|
'目标:消除完整的行来得分!');
|
|
};
|
|
|
|
// 初始化输入控制
|
|
setupInput();
|
|
|
|
debugLog('✅ 俄罗斯方块游戏初始化完成');
|
|
hideLoading();
|
|
}
|
|
|
|
// ==================== 页面加载完成 ====================
|
|
function hideLoading() {
|
|
document.getElementById('loading').style.display = 'none';
|
|
}
|
|
|
|
// 页面加载完成后初始化游戏
|
|
window.addEventListener('load', () => {
|
|
debugLog('页面加载完成');
|
|
debugLog(`浏览器: ${navigator.userAgent}`);
|
|
debugLog(`平台: ${navigator.platform}`);
|
|
|
|
// 检查PlayCanvas是否加载
|
|
if (typeof pc === 'undefined') {
|
|
debugLog('❌ PlayCanvas 未加载');
|
|
alert('游戏引擎加载失败,请刷新页面');
|
|
return;
|
|
}
|
|
|
|
debugLog('✅ PlayCanvas 已加载');
|
|
|
|
// 初始化游戏
|
|
initGame();
|
|
});
|
|
|
|
// 错误处理
|
|
window.addEventListener('error', (e) => {
|
|
console.error('全局错误:', e);
|
|
debugLog(`错误: ${e.message}`);
|
|
alert('游戏加载错误: ' + e.message);
|
|
});
|
|
</script>
|
|
</body>
|
|
</html> |