972 lines
35 KiB
HTML
972 lines
35 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>🎮 俄罗斯方块 - PlayCanvas版</title>
|
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🎮</text></svg>">
|
|
<!-- crypto.randomUUID 兼容性修复 -->
|
|
<script>
|
|
if (!crypto.randomUUID) {
|
|
crypto.randomUUID = function() {
|
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
|
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
|
|
return v.toString(16);
|
|
});
|
|
};
|
|
}
|
|
</script>
|
|
<!-- Poki SDK -->
|
|
<script src="https://game-cdn.poki.com/scripts/v2/poki-sdk.js" async></script>
|
|
<!-- 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;
|
|
-webkit-tap-highlight-color: transparent;
|
|
user-select: none;
|
|
}
|
|
body {
|
|
overflow: hidden;
|
|
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
touch-action: none;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
/* 手机端:上下布局,按钮在棋盘正下方 */
|
|
#game-container {
|
|
display: none;
|
|
}
|
|
#application {
|
|
width: 100vw;
|
|
height: 100vh;
|
|
display: block;
|
|
}
|
|
#ui {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
pointer-events: none;
|
|
z-index: 100;
|
|
}
|
|
/* PC宽屏布局 - 使用固定位置避免重叠 */
|
|
#game-layout {
|
|
display: none; /* PC端不使用这个容器 */
|
|
}
|
|
|
|
/* PC端:分数面板在左侧 */
|
|
#score-panel {
|
|
position: fixed;
|
|
left: 30px;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
color: white;
|
|
font-size: 20px;
|
|
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
|
|
background: rgba(0,0,0,0.6);
|
|
padding: 25px;
|
|
border-radius: 15px;
|
|
min-width: 160px;
|
|
z-index: 101;
|
|
}
|
|
#score-panel div {
|
|
margin-bottom: 15px;
|
|
}
|
|
#score-panel div:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
.label {
|
|
color: #aaa;
|
|
font-size: 14px;
|
|
display: block;
|
|
}
|
|
.value {
|
|
color: #ffd700;
|
|
font-weight: bold;
|
|
font-size: 28px;
|
|
}
|
|
|
|
/* PC端:控制按钮在右下方(菜单下方) */
|
|
#controls-pc {
|
|
position: fixed;
|
|
right: 30px;
|
|
bottom: 30px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 20px;
|
|
align-items: center;
|
|
z-index: 101;
|
|
}
|
|
#controls-mobile {
|
|
display: none;
|
|
}
|
|
/* 菜单放在右侧 */
|
|
#menu {
|
|
position: fixed;
|
|
right: 30px;
|
|
top: 20px;
|
|
text-align: center;
|
|
pointer-events: auto;
|
|
z-index: 101;
|
|
background: rgba(0,0,0,0.7);
|
|
padding: 25px;
|
|
border-radius: 20px;
|
|
max-width: 280px;
|
|
}
|
|
#menu h1 {
|
|
color: #00d4ff;
|
|
font-size: 28px;
|
|
margin-bottom: 20px;
|
|
text-shadow: 0 0 20px rgba(0,212,255,0.5);
|
|
}
|
|
.btn {
|
|
padding: 18px 50px;
|
|
font-size: 24px;
|
|
background: linear-gradient(135deg, #667eea, #764ba2);
|
|
color: white;
|
|
border: none;
|
|
border-radius: 35px;
|
|
cursor: pointer;
|
|
margin: 10px;
|
|
transition: transform 0.2s, box-shadow 0.2s;
|
|
pointer-events: auto;
|
|
font-weight: bold;
|
|
}
|
|
.btn:hover {
|
|
transform: scale(1.08);
|
|
box-shadow: 0 12px 35px rgba(102,126,234,0.5);
|
|
}
|
|
/* 游戏结束放在左侧 */
|
|
#gameOver {
|
|
display: none;
|
|
position: fixed;
|
|
left: 30px;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
text-align: center;
|
|
background: rgba(0,0,0,0.9);
|
|
padding: 40px;
|
|
border-radius: 20px;
|
|
pointer-events: auto;
|
|
z-index: 102;
|
|
min-width: 250px;
|
|
}
|
|
#gameOver h2 {
|
|
color: #ff6b6b;
|
|
font-size: 36px;
|
|
margin-bottom: 20px;
|
|
}
|
|
/* 虚拟按键 */
|
|
#controls {
|
|
position: absolute;
|
|
bottom: 30px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 15px;
|
|
pointer-events: auto;
|
|
}
|
|
.control-row {
|
|
display: flex;
|
|
gap: 15px;
|
|
}
|
|
.control-btn {
|
|
width: 110px;
|
|
height: 110px;
|
|
border-radius: 50%;
|
|
background: rgba(255,255,255,0.15);
|
|
border: 4px solid rgba(255,255,255,0.3);
|
|
color: white;
|
|
font-size: 44px;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-weight: bold;
|
|
}
|
|
.control-btn:hover {
|
|
background: rgba(255,255,255,0.25);
|
|
transform: scale(1.1);
|
|
}
|
|
.control-btn:active {
|
|
background: rgba(255,255,255,0.4);
|
|
transform: scale(0.95);
|
|
}
|
|
.rotate-btn {
|
|
width: 120px;
|
|
height: 120px;
|
|
font-size: 52px;
|
|
background: rgba(255,107,107,0.3);
|
|
border-color: rgba(255,107,107,0.5);
|
|
}
|
|
/* 手机端布局 - 严格三层:记分牌-棋盘-按钮 */
|
|
@media (max-width: 900px) {
|
|
body {
|
|
margin: 0;
|
|
padding: 0;
|
|
overflow: hidden;
|
|
background: #1a1a2e;
|
|
}
|
|
/* Canvas全屏渲染 */
|
|
#application {
|
|
position: fixed !important;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100% !important;
|
|
height: 100% !important;
|
|
z-index: 1;
|
|
}
|
|
/* UI层 */
|
|
#ui {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
pointer-events: none;
|
|
z-index: 10;
|
|
}
|
|
#ui > * {
|
|
pointer-events: auto;
|
|
}
|
|
/* 记分牌 - 顶部固定 */
|
|
#score-panel {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
height: 45px;
|
|
display: flex;
|
|
flex-direction: row;
|
|
justify-content: center;
|
|
align-items: center;
|
|
gap: 18px;
|
|
font-size: 11px;
|
|
padding: 0 10px;
|
|
background: rgba(0,0,0,0.9);
|
|
z-index: 20;
|
|
}
|
|
#score-panel div {
|
|
margin: 0;
|
|
text-align: center;
|
|
min-width: 50px;
|
|
}
|
|
#score-panel .label {
|
|
font-size: 9px;
|
|
}
|
|
#score-panel .value {
|
|
font-size: 15px;
|
|
}
|
|
/* PC按钮隐藏 */
|
|
#controls-pc {
|
|
display: none !important;
|
|
}
|
|
/* 手机按钮 - 底部固定 */
|
|
#controls-mobile {
|
|
display: flex !important;
|
|
position: absolute;
|
|
bottom: 0;
|
|
left: 0;
|
|
right: 0;
|
|
height: 175px;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
gap: 6px;
|
|
padding: 8px;
|
|
background: rgba(0,0,0,0.9);
|
|
z-index: 20;
|
|
}
|
|
#controls-mobile .control-row {
|
|
gap: 12px;
|
|
justify-content: center;
|
|
}
|
|
.control-btn {
|
|
width: 60px;
|
|
height: 60px;
|
|
font-size: 24px;
|
|
border-width: 2px;
|
|
}
|
|
.rotate-btn {
|
|
width: 60px;
|
|
height: 60px;
|
|
font-size: 28px;
|
|
}
|
|
#mobile-start-btn, #mobile-restart-btn {
|
|
width: 60px;
|
|
height: 60px;
|
|
border-radius: 50%;
|
|
border: 2px solid rgba(255,255,255,0.5);
|
|
color: white;
|
|
font-size: 11px;
|
|
font-weight: bold;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
text-align: center;
|
|
line-height: 1.2;
|
|
padding: 0;
|
|
}
|
|
#mobile-start-btn {
|
|
background: linear-gradient(135deg, #667eea, #764ba2);
|
|
}
|
|
#mobile-restart-btn {
|
|
background: linear-gradient(135deg, #56ab2f, #a8e063);
|
|
}
|
|
#mobile-rules {
|
|
color: #aaa;
|
|
font-size: 10px;
|
|
text-align: center;
|
|
padding: 4px 10px;
|
|
line-height: 1.4;
|
|
}
|
|
#mobile-rules span {
|
|
color: #ffd700;
|
|
}
|
|
#menu {
|
|
display: none !important;
|
|
}
|
|
#gameOver {
|
|
position: fixed;
|
|
left: 50%;
|
|
top: 50%;
|
|
transform: translate(-50%, -50%);
|
|
padding: 25px;
|
|
min-width: 180px;
|
|
z-index: 100;
|
|
}
|
|
#gameOver h2 {
|
|
font-size: 26px;
|
|
}
|
|
#gameOver .btn {
|
|
padding: 12px 25px;
|
|
font-size: 16px;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<canvas id="application"></canvas>
|
|
|
|
<div id="ui">
|
|
<!-- 菜单和结束画面 -->
|
|
<div id="menu">
|
|
<h1>🎮 俄罗斯方块</h1>
|
|
<button class="btn" onclick="startGame()">开始游戏</button>
|
|
<div style="margin-top: 30px; padding: 20px; background: rgba(0,0,0,0.6); border-radius: 15px; max-width: 320px; text-align: left; color: #ccc; font-size: 14px; line-height: 1.8;">
|
|
<h3 style="color: #00d4ff; margin-bottom: 15px; text-align: center;">📜 游戏规则</h3>
|
|
<p><span style="color: #4ecdc4;">🎮 控制:</span> 键盘方向键或点击按钮</p>
|
|
<p><span style="color: #ffeaa7;">← →:</span> 左右移动</p>
|
|
<p><span style="color: #ff6b6b;">↓:</span> 加速下落</p>
|
|
<p><span style="color: #a29bfe;">↑/空格:</span> 旋转方块</p>
|
|
<p><span style="color: #fd79a8;">目标:</span> 填满整行消除得分</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="gameOver">
|
|
<h2>游戏结束!</h2>
|
|
<div style="color: white; font-size: 24px; margin-bottom: 20px;">最终分数: <span id="finalScore">0</span></div>
|
|
<button class="btn" onclick="restartGame()">再玩一次</button>
|
|
</div>
|
|
|
|
<!-- 分数面板:PC在左侧,手机在顶部 -->
|
|
<div id="score-panel">
|
|
<div><span class="label">分数</span> <span id="score" class="value">0</span></div>
|
|
<div><span class="label">等级</span> <span id="level" class="value">0</span></div>
|
|
<div><span class="label">消行</span> <span id="lines" class="value">0</span></div>
|
|
<div><span class="label">速度</span> <span id="speed" class="value">1.0</span></div>
|
|
</div>
|
|
|
|
<!-- PC端控制按钮(在右侧) -->
|
|
<div id="controls-pc">
|
|
<div class="control-row">
|
|
<button class="control-btn rotate-btn" onclick="rotatePiece()">↻</button>
|
|
</div>
|
|
<div class="control-row">
|
|
<button class="control-btn" onclick="moveLeft()">←</button>
|
|
<button class="control-btn" onclick="moveDown()">↓</button>
|
|
<button class="control-btn" onclick="moveRight()">→</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 手机端控制按钮(在底部) -->
|
|
<div id="controls-mobile">
|
|
<!-- 第一行:开始 + 旋转 + 重新开始 -->
|
|
<div class="control-row">
|
|
<button id="mobile-start-btn" onclick="startGame()">开始<br>游戏</button>
|
|
<button class="control-btn rotate-btn" onclick="rotatePiece()">↻</button>
|
|
<button id="mobile-restart-btn" onclick="restartGame()">重新<br>开始</button>
|
|
</div>
|
|
<!-- 第二行:左 下 右 -->
|
|
<div class="control-row">
|
|
<button class="control-btn" onclick="moveLeft()">←</button>
|
|
<button class="control-btn" onclick="moveDown()">↓</button>
|
|
<button class="control-btn" onclick="moveRight()">→</button>
|
|
</div>
|
|
<!-- 第三行:规则 -->
|
|
<div id="mobile-rules">
|
|
<span>← →</span> 移动 <span>↓</span> 加速 <span>↻</span> 旋转
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// ==================== Poki SDK ====================
|
|
let pokiReady = false;
|
|
function initPoki() {
|
|
if (typeof PokiSDK !== 'undefined') {
|
|
return PokiSDK.init().then(() => {
|
|
console.log('✅ Poki SDK 初始化成功');
|
|
pokiReady = true;
|
|
PokiSDK.gameLoadingStart();
|
|
setTimeout(() => PokiSDK.gameLoadingFinished(), 500);
|
|
}).catch(err => {
|
|
console.log('⚠️ Poki SDK 未就绪:', err);
|
|
return Promise.resolve();
|
|
});
|
|
}
|
|
return Promise.resolve();
|
|
}
|
|
|
|
// ==================== 游戏配置 ====================
|
|
const GRID_W = 10;
|
|
const GRID_H = 20;
|
|
const CELL_SIZE = 1.5;
|
|
|
|
const COLORS = {
|
|
I: new pc.Color(0, 0.9, 0.9),
|
|
O: new pc.Color(0.9, 0.9, 0),
|
|
T: new pc.Color(0.6, 0, 0.6),
|
|
L: new pc.Color(1, 0.5, 0),
|
|
J: new pc.Color(0, 0, 0.9),
|
|
S: new pc.Color(0, 0.9, 0),
|
|
Z: new pc.Color(0.9, 0, 0)
|
|
};
|
|
|
|
// 方块形状定义
|
|
const SHAPES = {
|
|
I: [[1,1,1,1]],
|
|
O: [[1,1],[1,1]],
|
|
T: [[0,1,0],[1,1,1]],
|
|
L: [[1,0,0],[1,1,1]],
|
|
J: [[0,0,1],[1,1,1]],
|
|
S: [[0,1,1],[1,1,0]],
|
|
Z: [[1,1,0],[0,1,1]]
|
|
};
|
|
|
|
// ==================== 全局变量 ====================
|
|
let app, gameRunning = false, gamePaused = false;
|
|
let board = [], currentPiece = null;
|
|
let score = 0, level = 0, lines = 0; // 规则:从Level 0开始
|
|
let dropTimer = 0, dropInterval = 1.0, lockTimer = 0, lockDelay = 0.5;
|
|
let isLocking = false; // 正在锁定延迟中
|
|
let boardEntities = [], pieceEntities = [];
|
|
|
|
// ==================== 初始化 ====================
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
initGame();
|
|
});
|
|
|
|
function initGame() {
|
|
console.log('🎮 初始化 PlayCanvas...');
|
|
const canvas = document.getElementById('application');
|
|
|
|
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)
|
|
});
|
|
|
|
app.start();
|
|
app.setCanvasFillMode(pc.FILLMODE_FILL_WINDOW);
|
|
app.setCanvasResolution(pc.RESOLUTION_AUTO);
|
|
window.addEventListener('resize', () => app.resizeCanvas());
|
|
|
|
setupScene();
|
|
setupInput();
|
|
initBoard();
|
|
|
|
// 设置游戏循环(必须在app创建后)
|
|
setupGameLoop();
|
|
|
|
console.log('✅ PlayCanvas 初始化成功');
|
|
} catch (e) {
|
|
console.error('❌ 初始化失败:', e);
|
|
alert('游戏加载失败: ' + e.message);
|
|
}
|
|
}
|
|
|
|
function setupGameLoop() {
|
|
app.on('update', (dt) => {
|
|
if (!gameRunning || gamePaused) return;
|
|
|
|
// 锁定延迟处理
|
|
if (isLocking) {
|
|
lockTimer += dt;
|
|
if (lockTimer >= lockDelay) {
|
|
// 延迟结束,确认锁定
|
|
lockAndSpawn();
|
|
isLocking = false;
|
|
lockTimer = 0;
|
|
}
|
|
return; // 锁定期间不处理自然下落
|
|
}
|
|
|
|
dropTimer += dt;
|
|
if (dropTimer > dropInterval) {
|
|
if (currentPiece) {
|
|
if (!currentPiece.move(0, 1)) {
|
|
// 规则:无法下落,进入锁定延迟(0.5秒)
|
|
isLocking = true;
|
|
lockTimer = 0;
|
|
}
|
|
}
|
|
dropTimer = 0;
|
|
}
|
|
});
|
|
}
|
|
|
|
function setupScene() {
|
|
// 根据屏幕尺寸计算最佳相机视野
|
|
const isMobile = window.innerWidth <= 900;
|
|
// 手机端:根据可视区域比例调整相机
|
|
const orthoHeight = isMobile ? 32 : 35;
|
|
|
|
// 相机
|
|
const camera = new pc.Entity('Camera');
|
|
camera.addComponent('camera', {
|
|
clearColor: new pc.Color(0.102, 0.102, 0.18), // #1a1a2e 与body背景一致
|
|
orthographic: true,
|
|
orthoHeight: orthoHeight,
|
|
nearClip: 0.1,
|
|
farClip: 100
|
|
});
|
|
// 手机端:完全俯视,棋盘微调向右,大小再90%
|
|
if (isMobile) {
|
|
camera.setPosition(6.55, 30, 11); // 左移半个格子(回退)
|
|
camera.setEulerAngles(-90, 0, 0); // 完全俯视
|
|
camera.camera.orthoHeight = 21.9; // 棋盘再缩90%
|
|
} else {
|
|
camera.setPosition(5, 20, 20);
|
|
camera.setEulerAngles(-60, 0, 0);
|
|
}
|
|
app.root.addChild(camera);
|
|
|
|
// 灯光
|
|
const light = new pc.Entity('Light');
|
|
light.addComponent('light', {
|
|
type: 'directional',
|
|
color: new pc.Color(1, 1, 1),
|
|
intensity: 1.2
|
|
});
|
|
light.setEulerAngles(45, 45, 0);
|
|
app.root.addChild(light);
|
|
|
|
// 创建游戏区域背景 - 使用与body一致的颜色 #1a1a2e
|
|
const bg = new pc.Entity('Background');
|
|
bg.addComponent('render', { type: 'box' });
|
|
bg.setLocalScale(GRID_W * CELL_SIZE + 0.5, 0.1, GRID_H * CELL_SIZE + 0.5);
|
|
bg.setPosition(GRID_W * CELL_SIZE / 2 - CELL_SIZE/2, -0.1, GRID_H * CELL_SIZE / 2 - CELL_SIZE/2);
|
|
const bgMat = new pc.StandardMaterial();
|
|
bgMat.diffuse = new pc.Color(0.102, 0.102, 0.18); // #1a1a2e
|
|
bgMat.update();
|
|
bg.render.material = bgMat;
|
|
app.root.addChild(bg);
|
|
|
|
// 网格线
|
|
createGridLines();
|
|
}
|
|
|
|
function createGridLines() {
|
|
const lineMat = new pc.StandardMaterial();
|
|
lineMat.diffuse = new pc.Color(0.2, 0.2, 0.25);
|
|
lineMat.update();
|
|
|
|
// 垂直线
|
|
for (let x = 0; x <= GRID_W; x++) {
|
|
const line = new pc.Entity();
|
|
line.addComponent('render', { type: 'box' });
|
|
line.setLocalScale(0.05, 0.1, GRID_H * CELL_SIZE);
|
|
line.setPosition(x * CELL_SIZE - CELL_SIZE/2, 0.01, GRID_H * CELL_SIZE / 2 - CELL_SIZE/2);
|
|
line.render.material = lineMat;
|
|
app.root.addChild(line);
|
|
}
|
|
// 水平线
|
|
for (let z = 0; z <= GRID_H; z++) {
|
|
const line = new pc.Entity();
|
|
line.addComponent('render', { type: 'box' });
|
|
line.setLocalScale(GRID_W * CELL_SIZE, 0.1, 0.05);
|
|
line.setPosition(GRID_W * CELL_SIZE / 2 - CELL_SIZE/2, 0.01, z * CELL_SIZE - CELL_SIZE/2);
|
|
line.render.material = lineMat;
|
|
app.root.addChild(line);
|
|
}
|
|
}
|
|
|
|
function initBoard() {
|
|
board = [];
|
|
for (let y = 0; y < GRID_H; y++) {
|
|
board[y] = [];
|
|
for (let x = 0; x < GRID_W; x++) {
|
|
board[y][x] = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ==================== 方块类 ====================
|
|
class Piece {
|
|
constructor(type) {
|
|
this.type = type;
|
|
this.shape = JSON.parse(JSON.stringify(SHAPES[type]));
|
|
this.color = COLORS[type];
|
|
// 规则:I型出生在x=3,其他在x=4,y=0
|
|
this.x = (type === 'I') ? 3 : 4;
|
|
this.y = 0;
|
|
this.entities = [];
|
|
this.createEntities();
|
|
}
|
|
|
|
createEntities() {
|
|
// 清理旧实体 - 修复:e是{item}对象,需要访问e.entity
|
|
this.entities.forEach(item => {
|
|
if (item.entity && item.entity.parent) {
|
|
item.entity.destroy();
|
|
}
|
|
});
|
|
this.entities = [];
|
|
|
|
for (let row = 0; row < this.shape.length; row++) {
|
|
for (let col = 0; col < this.shape[row].length; col++) {
|
|
if (this.shape[row][col]) {
|
|
const block = new pc.Entity();
|
|
block.addComponent('render', { type: 'box' });
|
|
block.setLocalScale(CELL_SIZE * 0.9, 0.5, CELL_SIZE * 0.9);
|
|
|
|
const mat = new pc.StandardMaterial();
|
|
mat.diffuse = this.color;
|
|
mat.emissive = new pc.Color(this.color.r * 0.3, this.color.g * 0.3, this.color.b * 0.3);
|
|
mat.update();
|
|
block.render.material = mat;
|
|
|
|
app.root.addChild(block);
|
|
this.entities.push({ entity: block, row: row, col: col });
|
|
}
|
|
}
|
|
}
|
|
this.updatePositions();
|
|
}
|
|
|
|
updatePositions() {
|
|
this.entities.forEach(item => {
|
|
const worldX = (this.x + item.col) * CELL_SIZE;
|
|
const worldZ = (this.y + item.row) * CELL_SIZE;
|
|
item.entity.setPosition(worldX, 0.25, worldZ);
|
|
});
|
|
}
|
|
|
|
move(dx, dy) {
|
|
const newX = this.x + dx;
|
|
const newY = this.y + dy;
|
|
if (this.isValidPosition(newX, newY, this.shape)) {
|
|
this.x = newX;
|
|
this.y = newY;
|
|
this.updatePositions();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
rotate() {
|
|
const rows = this.shape.length;
|
|
const cols = this.shape[0].length;
|
|
const rotated = [];
|
|
for (let i = 0; i < cols; i++) {
|
|
rotated[i] = [];
|
|
for (let j = 0; j < rows; j++) {
|
|
rotated[i][j] = this.shape[rows - 1 - j][i];
|
|
}
|
|
}
|
|
if (this.isValidPosition(this.x, this.y, rotated)) {
|
|
this.shape = rotated;
|
|
this.createEntities();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
isValidPosition(x, y, shape) {
|
|
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 >= GRID_W || newY >= GRID_H) return false;
|
|
if (newY >= 0 && board[newY][newX]) return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
lock() {
|
|
// 将方块固定到游戏板
|
|
for (let row = 0; row < this.shape.length; row++) {
|
|
for (let col = 0; col < this.shape[row].length; col++) {
|
|
if (this.shape[row][col]) {
|
|
const x = this.x + col;
|
|
const y = this.y + row;
|
|
if (y >= 0) {
|
|
const block = new pc.Entity();
|
|
block.addComponent('render', { type: 'box' });
|
|
block.setLocalScale(CELL_SIZE * 0.9, 0.5, CELL_SIZE * 0.9);
|
|
block.setPosition(x * CELL_SIZE, 0.25, y * CELL_SIZE);
|
|
|
|
const mat = new pc.StandardMaterial();
|
|
mat.diffuse = this.color;
|
|
mat.emissive = new pc.Color(this.color.r * 0.2, this.color.g * 0.2, this.color.b * 0.2);
|
|
mat.update();
|
|
block.render.material = mat;
|
|
|
|
app.root.addChild(block);
|
|
board[y][x] = block;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// 清理当前方块实体
|
|
this.entities.forEach(item => item.entity.destroy());
|
|
}
|
|
}
|
|
|
|
// ==================== 游戏逻辑 ====================
|
|
function spawnPiece() {
|
|
const types = Object.keys(SHAPES);
|
|
const type = types[Math.floor(Math.random() * types.length)];
|
|
currentPiece = new Piece(type);
|
|
|
|
// 检查游戏结束
|
|
if (!currentPiece.isValidPosition(currentPiece.x, currentPiece.y, currentPiece.shape)) {
|
|
gameOver();
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function checkLines() {
|
|
let linesCleared = 0;
|
|
for (let y = GRID_H - 1; y >= 0; y--) {
|
|
let full = true;
|
|
for (let x = 0; x < GRID_W; x++) {
|
|
if (!board[y][x]) { full = false; break; }
|
|
}
|
|
if (full) {
|
|
// 删除这一行
|
|
for (let x = 0; x < GRID_W; x++) {
|
|
if (board[y][x]) board[y][x].destroy();
|
|
}
|
|
// 上方行下移
|
|
for (let moveY = y; moveY > 0; moveY--) {
|
|
board[moveY] = board[moveY - 1];
|
|
// 更新实体位置
|
|
for (let x = 0; x < GRID_W; x++) {
|
|
if (board[moveY][x]) {
|
|
board[moveY][x].setPosition(x * CELL_SIZE, 0.25, moveY * CELL_SIZE);
|
|
}
|
|
}
|
|
}
|
|
board[0] = new Array(GRID_W).fill(null);
|
|
linesCleared++;
|
|
y++; // 重新检查当前行
|
|
}
|
|
}
|
|
|
|
if (linesCleared > 0) {
|
|
// 规则:基础分 × (等级+1),等级0时×1,等级1时×2...
|
|
const basePoints = [0, 100, 300, 500, 800][linesCleared];
|
|
const multiplier = level + 1;
|
|
score += basePoints * multiplier;
|
|
lines += linesCleared;
|
|
// 规则:每消除10行升1级
|
|
level = Math.floor(lines / 10);
|
|
// 速度随等级增加
|
|
dropInterval = Math.max(0.05, 1.0 - level * 0.05);
|
|
updateUI();
|
|
}
|
|
}
|
|
|
|
// ==================== 游戏控制 ====================
|
|
window.startGame = function() {
|
|
if (gameRunning) return;
|
|
|
|
document.getElementById('menu').style.display = 'none';
|
|
|
|
// 清理旧游戏
|
|
for (let y = 0; y < GRID_H; y++) {
|
|
for (let x = 0; x < GRID_W; x++) {
|
|
if (board[y][x]) {
|
|
board[y][x].destroy();
|
|
board[y][x] = null;
|
|
}
|
|
}
|
|
}
|
|
if (currentPiece) {
|
|
currentPiece.entities.forEach(item => item.entity.destroy());
|
|
currentPiece = null;
|
|
}
|
|
|
|
gameRunning = true;
|
|
gamePaused = false;
|
|
score = 0;
|
|
level = 0; // 规则:从Level 0开始
|
|
lines = 0;
|
|
dropInterval = 1.0;
|
|
dropTimer = 0;
|
|
lockTimer = 0;
|
|
isLocking = false;
|
|
|
|
spawnPiece();
|
|
updateUI();
|
|
|
|
if (pokiReady && typeof PokiSDK !== 'undefined') {
|
|
PokiSDK.gameplayStart();
|
|
}
|
|
};
|
|
|
|
window.restartGame = function() {
|
|
document.getElementById('gameOver').style.display = 'none';
|
|
startGame();
|
|
};
|
|
|
|
function gameOver() {
|
|
gameRunning = false;
|
|
document.getElementById('finalScore').textContent = score;
|
|
document.getElementById('gameOver').style.display = 'block';
|
|
|
|
if (pokiReady && typeof PokiSDK !== 'undefined') {
|
|
PokiSDK.gameplayStop();
|
|
PokiSDK.commercialBreak().catch(() => {});
|
|
}
|
|
}
|
|
|
|
function updateUI() {
|
|
document.getElementById('score').textContent = score;
|
|
document.getElementById('level').textContent = level;
|
|
document.getElementById('lines').textContent = lines;
|
|
document.getElementById('speed').textContent = (1.0 / dropInterval).toFixed(1) + 'x';
|
|
}
|
|
|
|
// ==================== 输入控制 ====================
|
|
window.moveLeft = function() {
|
|
if (gameRunning && currentPiece) {
|
|
if (currentPiece.move(-1, 0)) {
|
|
// 规则:锁定延迟期间移动成功,重置锁定计时
|
|
if (isLocking) {
|
|
lockTimer = 0;
|
|
// 移动后检查是否还能下落
|
|
if (currentPiece.isValidPosition(currentPiece.x, currentPiece.y + 1, currentPiece.shape)) {
|
|
isLocking = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
window.moveRight = function() {
|
|
if (gameRunning && currentPiece) {
|
|
if (currentPiece.move(1, 0)) {
|
|
// 规则:锁定延迟期间移动成功,重置锁定计时
|
|
if (isLocking) {
|
|
lockTimer = 0;
|
|
// 移动后检查是否还能下落
|
|
if (currentPiece.isValidPosition(currentPiece.x, currentPiece.y + 1, currentPiece.shape)) {
|
|
isLocking = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
window.moveDown = function() {
|
|
if (gameRunning && currentPiece) {
|
|
if (currentPiece.move(0, 1)) {
|
|
// 规则:软降每格+1分
|
|
score += 1;
|
|
updateUI();
|
|
// 移动成功,重置锁定状态
|
|
isLocking = false;
|
|
lockTimer = 0;
|
|
} else if (!isLocking) {
|
|
// 无法下落,立即锁定(软降时)
|
|
lockAndSpawn();
|
|
}
|
|
}
|
|
};
|
|
window.rotatePiece = function() {
|
|
if (gameRunning && currentPiece) {
|
|
if (currentPiece.rotate()) {
|
|
// 规则:锁定延迟期间旋转成功,重置锁定计时
|
|
if (isLocking) {
|
|
lockTimer = 0;
|
|
// 旋转后检查是否还能下落
|
|
if (currentPiece.isValidPosition(currentPiece.x, currentPiece.y + 1, currentPiece.shape)) {
|
|
isLocking = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
function lockAndSpawn() {
|
|
if (currentPiece) {
|
|
currentPiece.lock();
|
|
checkLines();
|
|
spawnPiece();
|
|
}
|
|
}
|
|
|
|
function setupInput() {
|
|
app.keyboard.on('keydown', (event) => {
|
|
if (!gameRunning) return;
|
|
|
|
switch(event.key) {
|
|
case pc.KEY_LEFT:
|
|
moveLeft();
|
|
break;
|
|
case pc.KEY_RIGHT:
|
|
moveRight();
|
|
break;
|
|
case pc.KEY_DOWN:
|
|
moveDown();
|
|
break;
|
|
case pc.KEY_UP:
|
|
case pc.KEY_SPACE:
|
|
rotatePiece();
|
|
break;
|
|
case pc.KEY_P:
|
|
gamePaused = !gamePaused;
|
|
break;
|
|
}
|
|
});
|
|
}
|
|
|
|
// 启动Poki
|
|
initPoki().then(() => {
|
|
console.log('🎮 俄罗斯方块 PlayCanvas版已加载');
|
|
});
|
|
</script>
|
|
<!-- 登录认证 -->
|
|
<script src="../auth.js?v=1"></script>
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
GAME_AUTH.init('tetris', '俄罗斯方块');
|
|
});
|
|
</script>
|
|
</body>
|
|
</html> |