代码: 全选
這裡為您修改了代碼,已完美解決您提到的三個問題。
### 🛠️ 主要修改說明
1. **圓柱體放置限制(同行同列判定)**:
* 新增了 `isRowOrColOccupied(c, r)` 函式,用來檢查即將放置的棋格之橫線(Row)或縱線(Column)上是否已經存在任何圓柱體。
* 將此檢查套用至 **第 4 階段**、**第 5 階段** 以及 **第 6 階段(邊緣圓柱)** 的放置判定中。
2. **第二局橙方自由選擇角落**:
* 修改了 `startPhase2()`。當進入第二局且輪到橙方先手放置角遊戲板時,畫面上會同時出現左上角(LT)**與**右上角(RT)的綠色互動提示墊,供玩家自由點選。
3. **重新開局由藍方先行**:
* 修復了原先 `reinitNextRound()` 函式內部強制將 `gameRound` 設為 `2` 的 Bug。現在將局數控制移至點擊按鈕的事件中。當兩局結束或第一局選擇「否(顯示總結算)」並點擊「重新開始整個遊戲」時,`gameRound` 會正確重設為 `1`,確保新賽局絕對由藍方先行。
---
### 完整修正後的 HTML 代碼
您可以直接複製下方完整的代碼並替換原本的檔案:
```html
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>【我為歌狂】3D遊戲 - 完整對戰版</title>
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
background-color: #2c3e50;
touch-action: none;
}
#game-container {
width: 100vw;
height: 100vh;
background-color: #000;
position: relative;
}
canvas {
display: block;
}
#ui-layer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
pointer-events: none;
z-index: 10;
}
#btn-start {
pointer-events: auto;
padding: 20px 50px;
font-size: 28px;
font-weight: bold;
color: #fff;
background-color: #e74c3c;
border: none;
border-radius: 10px;
cursor: pointer;
box-shadow: 0 5px 15px rgba(0,0,0,0.5);
transition: transform 0.1s, background-color 0.2s;
}
#btn-start:hover {
background-color: #c0392b;
transform: scale(1.05);
}
#btn-start:active {
transform: scale(0.95);
}
#message {
position: absolute;
top: 20px;
width: 100%;
text-align: center;
color: #fff;
font-size: 24px;
font-weight: bold;
pointer-events: none;
text-shadow: 2px 2px 4px rgba(0,0,0,0.8);
display: none;
}
</style>
<script type="importmap">
{
"imports": {
"three": "./three.module.js",
"three/addons/controls/": "./"
}
}
</script>
</head>
<body>
<div id="game-container"></div>
<div id="ui-layer">
<button id="btn-start">開始遊戲</button>
</div>
<div id="message"></div>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// ==========================================
// 1. 初始化場景、相機、渲染器
// ==========================================
const container = document.getElementById('game-container');
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x34495e);
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 2000);
camera.position.set(0, 500, 500);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.outputColorSpace = THREE.SRGBColorSpace;
container.appendChild(renderer.domElement);
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
const ambientLight = new THREE.AmbientLight(0xffffff, 0.7);
scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight(0xffffff, 0.6);
dirLight.position.set(200, 400, 200);
scene.add(dirLight);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enablePan = false;
controls.enableZoom = true;
controls.minPolarAngle = 0;
controls.maxPolarAngle = Math.PI / 2;
controls.minAzimuthAngle = -Math.PI / 4;
controls.maxAzimuthAngle = Math.PI / 4;
// ==========================================
// 2. 建立底板與全局追蹤容器
// ==========================================
const boardSize = 480;
const planeGeo = new THREE.PlaneGeometry(boardSize, boardSize);
const planeMat = new THREE.MeshLambertMaterial({ color: 0xADD8E6 });
const plane = new THREE.Mesh(planeGeo, planeMat);
plane.rotation.x = -Math.PI / 2;
scene.add(plane);
const edgesGeo = new THREE.EdgesGeometry(planeGeo);
const edgesMat = new THREE.LineDashedMaterial({ color: 0xffffff, dashSize: 10, gapSize: 10 });
const planeEdges = new THREE.LineSegments(edgesGeo, edgesMat);
planeEdges.computeLineDistances();
planeEdges.rotation.x = -Math.PI / 2;
planeEdges.position.y = 0.5;
scene.add(planeEdges);
// 用於重新開局時清理的全局容器
let boardMeshes = [];
let cylinderMeshes = [];
let microphones = [];
// ==========================================
// 3. 共用幾何體與材質生成
// ==========================================
const gameBoardGeo = new THREE.BoxGeometry(118, 4, 118);
const boardEdgesGeo = new THREE.EdgesGeometry(gameBoardGeo);
const boardEdgesMat = new THREE.LineBasicMaterial({ color: 0x363636, linewidth: 4 });
const sideBoardMat = new THREE.MeshLambertMaterial({ color: 0xD3D3D3 });
function createBoardTopTexture(dirs) {
const canvas = document.createElement('canvas');
canvas.width = 120; canvas.height = 120;
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#DEB887';
ctx.fillRect(0, 0, 120, 120);
ctx.strokeStyle = '#000000';
ctx.lineWidth = 4;
ctx.beginPath();
if (dirs.includes('top')) { ctx.moveTo(60, 60); ctx.lineTo(60, 0); }
if (dirs.includes('bottom')) { ctx.moveTo(60, 60); ctx.lineTo(60, 120); }
if (dirs.includes('left')) { ctx.moveTo(60, 60); ctx.lineTo(0, 60); }
if (dirs.includes('right')) { ctx.moveTo(60, 60); ctx.lineTo(120, 60); }
ctx.stroke();
const texture = new THREE.CanvasTexture(canvas);
texture.colorSpace = THREE.SRGBColorSpace;
return texture;
}
function createGameBoard(colCenter, rowCenter, dirs, startZOffset = 0) {
const topMat = new THREE.MeshLambertMaterial({ map: createBoardTopTexture(dirs) });
const materials = [sideBoardMat, sideBoardMat, topMat, sideBoardMat, sideBoardMat, sideBoardMat];
const board = new THREE.Mesh(gameBoardGeo, materials);
const pos = getCellWorldPos(colCenter, rowCenter);
board.position.set(pos.x, 2, pos.z + startZOffset);
const boardEdges = new THREE.LineSegments(boardEdgesGeo, boardEdgesMat);
board.add(boardEdges);
scene.add(board);
boardMeshes.push(board);
return { mesh: board, targetZ: pos.z, startZ: pos.z + startZOffset };
}
function getCellWorldPos(col, row) {
return { x: (col - 3.5) * 60, z: (row - 3.5) * 60 };
}
const faceMapping = [3, 4, 1, 6, 2, 5];
function createDiceMaterial(baseColor, dotColor, shape, number) {
const canvas = document.createElement('canvas');
canvas.width = 128; canvas.height = 128;
const ctx = canvas.getContext('2d');
ctx.fillStyle = baseColor;
ctx.fillRect(0, 0, 128, 128);
ctx.fillStyle = dotColor;
const drawShape = (x, y) => {
ctx.beginPath();
if (shape === 'triangle') {
const r = 16;
ctx.moveTo(x, y - r);
ctx.lineTo(x + r*0.866, y + r*0.5);
ctx.lineTo(x - r*0.866, y + r*0.5);
} else {
const r = 14;
for (let i=0; i<6; i++) {
const angle = i * Math.PI / 3;
if(i===0) ctx.moveTo(x + r*Math.cos(angle), y + r*Math.sin(angle));
else ctx.lineTo(x + r*Math.cos(angle), y + r*Math.sin(angle));
}
}
ctx.closePath();
ctx.fill();
};
const dots = [];
if(number === 1) dots.push([64, 64]);
if(number === 2) dots.push([32, 32], [96, 96]);
if(number === 3) dots.push([32, 32], [64, 64], [96, 96]);
if(number === 4) dots.push([32, 32], [96, 32], [32, 96], [96, 96]);
if(number === 5) dots.push([32, 32], [96, 32], [64, 64], [32, 96], [96, 96]);
if(number === 6) dots.push([32, 24], [32, 64], [32, 104], [96, 24], [96, 64], [96, 104]);
dots.forEach(p => drawShape(p[0], p[1]));
const texture = new THREE.CanvasTexture(canvas);
texture.colorSpace = THREE.SRGBColorSpace;
return new THREE.MeshLambertMaterial({ map: texture });
}
function createDiceMesh(baseColor, dotColor, shape) {
const materials = faceMapping.map(num => createDiceMaterial(baseColor, dotColor, shape, num));
const cube = new THREE.Mesh(new THREE.BoxGeometry(48, 48, 48), materials);
cube.userData = {
currentNumber: 1, score: 1, animating: false, startTime: 0,
startQuat: new THREE.Quaternion(), targetQuat: new THREE.Quaternion(),
moving: false, moveStartTime: 0, moveDuration: 800,
startX: 0, startZ: 0, targetX: 0, targetZ: 0, col: 0, row: 0, color: ''
};
return cube;
}
// 依據分數翻轉立方體至指定朝上點數
function animateCubeToScore(cube, targetScore) {
cube.userData.animating = true;
cube.userData.startTime = performance.now();
cube.userData.startQuat.copy(cube.quaternion);
cube.userData.currentNumber = targetScore;
let rx = 0, ry = 0, rz = 0;
switch(targetScore) {
case 1: break;
case 2: rx = -Math.PI/2; break;
case 3: rz = Math.PI/2; break;
case 4: rz = -Math.PI/2; break;
case 5: rx = Math.PI/2; break;
case 6: rx = Math.PI; break;
}
cube.userData.targetQuat.setFromEuler(new THREE.Euler(rx, ry, rz));
}
// ==========================================
// 4. 遊戲狀態與互動邏輯
// ==========================================
const itemY = 28;
let cubes = [];
let cylindersData = [];
let markers = [];
let raycaster = new THREE.Raycaster();
let mouse = new THREE.Vector2();
let blueCornerType = '';
let orangeCornerType = '';
let originalBlueCorner = '';
let originalOrangeCorner = '';
let placedEdges = { top: false, bottom: false, left: false, right: false };
let placedCorners = { LB: false, RB: false, LT: false, RT: false };
let centerBoardsAnimData = [];
let isAnimatingPhase1 = false;
let phase1Progress = 0;
let edgeTurn = 1;
// 核心對戰局數與行走變數
let gameRound = 1;
let gameplayActive = false;
let currentPlayer = 'blue';
let walkCount = 1;
let lastDirection = null;
let turnStopPositions = [];
let blueTotalScore = 0;
let orangeTotalScore = 0;
const uiMsg = document.getElementById('message');
function showMessage(msg) {
uiMsg.style.display = 'block';
uiMsg.innerText = msg;
}
function createMarker(col, row, color, onClickCallback, customY = 5) {
const pos = getCellWorldPos(col, row);
const geo = new THREE.CylinderGeometry(18, 18, 2, 32);
const mat = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: 0.8 });
const marker = new THREE.Mesh(geo, mat);
marker.position.set(pos.x, customY, pos.z);
marker.userData.onClick = onClickCallback;
scene.add(marker);
markers.push(marker);
}
function clearMarkers() {
markers.forEach(m => scene.remove(m));
markers = [];
document.body.style.cursor = 'default';
}
function placeCylinder(col, row) {
const pos = getCellWorldPos(col, row);
const cylGeo = new THREE.CylinderGeometry(24, 24, 48, 32);
const cylMat = new THREE.MeshLambertMaterial({ color: 0x800080 });
const cylinder = new THREE.Mesh(cylGeo, cylMat);
cylinder.position.set(pos.x, itemY, pos.z);
const cylEdgesGeo = new THREE.EdgesGeometry(cylGeo);
const cylEdgesMat = new THREE.LineBasicMaterial({ color: 0x000000, linewidth: 1 });
const cylEdges = new THREE.LineSegments(cylEdgesGeo, cylEdgesMat);
cylinder.add(cylEdges);
scene.add(cylinder);
cylinderMeshes.push(cylinder);
cylindersData.push({ c: col, r: row });
}
function placeCornerBoard(type) {
if (placedCorners[type]) return;
placedCorners[type] = true;
if (type === 'LB') createGameBoard(0.5, 6.5, ['top', 'right']);
if (type === 'RB') createGameBoard(6.5, 6.5, ['top', 'left']);
if (type === 'LT') createGameBoard(0.5, 0.5, ['bottom', 'right']);
if (type === 'RT') createGameBoard(6.5, 0.5, ['bottom', 'left']);
}
function placeEdgeBoards(side) {
if (placedEdges[side]) return;
placedEdges[side] = true;
if (side === 'bottom') {
createGameBoard(2.5, 6.5, ['left', 'right', 'bottom']);
createGameBoard(4.5, 6.5, ['left', 'right', 'bottom']);
}
if (side === 'top') {
createGameBoard(2.5, 0.5, ['left', 'right', 'top']);
createGameBoard(4.5, 0.5, ['left', 'right', 'top']);
}
if (side === 'left') {
createGameBoard(0.5, 2.5, ['top', 'bottom', 'left']);
createGameBoard(0.5, 4.5, ['top', 'bottom', 'left']);
}
if (side === 'right') {
createGameBoard(6.5, 2.5, ['top', 'bottom', 'right']);
createGameBoard(6.5, 4.5, ['top', 'bottom', 'right']);
}
}
function placeCube(col, row, color) {
const pos = getCellWorldPos(col, row);
let cube;
if (color === 'blue') cube = createDiceMesh('#00008B', '#FFFFE0', 'hexagon');
else cube = createDiceMesh('#FFA500', '#90EE90', 'triangle');
cube.position.set(pos.x, itemY, pos.z);
cube.userData.col = col;
cube.userData.row = row;
cube.userData.color = color;
cube.userData.score = 1;
cube.userData.currentNumber = 1;
scene.add(cube);
cubes.push(cube);
}
function createMicrophoneMesh(color, col, row) {
const pos = getCellWorldPos(col, row);
let geo;
const mat = new THREE.MeshLambertMaterial({ color: color === 'blue' ? 0x00008B : 0xFFA500 });
if (color === 'blue') {
geo = new THREE.CylinderGeometry(20, 20, 4, 6); // 藍方正六邊形
} else {
geo = new THREE.CylinderGeometry(20, 20, 4, 3); // 橘方三角形
}
const mic = new THREE.Mesh(geo, mat);
mic.position.set(pos.x, 6, pos.z); // 話筒高為4,平貼於面板之上 (面板表面Y為4)
const edgesGeo = new THREE.EdgesGeometry(geo);
const edgesMat = new THREE.LineBasicMaterial({ color: 0x000000, linewidth: 1 });
const edges = new THREE.LineSegments(edgesGeo, edgesMat);
mic.add(edges);
mic.userData = { color: color, col: col, row: row };
scene.add(mic);
microphones.push(mic);
}
function isOccupiedByCyl(c, r) {
return cylindersData.some(cyl => cyl.c === c && cyl.r === r);
}
// 新增:檢查橫線(Row)或縱線(Col)上是否已有圓柱體
function isRowOrColOccupied(c, r) {
return cylindersData.some(cyl => cyl.c === c || cyl.r === r);
}
function isDiagAdjacent(c1, r1, c2, r2) {
return Math.abs(c1 - c2) === 1 && Math.abs(r1 - r2) === 1;
}
function isValidOrangeCenterCyl(c, r) {
if (isRowOrColOccupied(c, r)) return false;
return !cylindersData.some(cyl => isDiagAdjacent(c, r, cyl.c, cyl.r));
}
function getEdgeCells(cornerType) {
let cells = [];
if(cornerType.includes('B')) {
for(let c=2; c<=5; c++) cells.push({c, r:7, side:'bottom'});
}
if(cornerType.includes('T')) {
for(let c=2; c<=5; c++) cells.push({c, r:0, side:'top'});
}
if(cornerType.includes('L')) {
for(let r=2; r<=5; r++) cells.push({c:0, r, side:'left'});
}
if(cornerType.includes('R')) {
for(let r=2; r<=5; r++) cells.push({c:7, r, side:'right'});
}
return cells;
}
function getOuterCornerCells(cornerType) {
if (cornerType === 'LB') return [{c:0, r:7}, {c:1, r:7}, {c:0, r:6}];
if (cornerType === 'RB') return [{c:7, r:7}, {c:6, r:7}, {c:7, r:6}];
if (cornerType === 'LT') return [{c:0, r:0}, {c:1, r:0}, {c:0, r:1}];
if (cornerType === 'RT') return [{c:7, r:0}, {c:6, r:0}, {c:7, r:1}];
}
// ==========================================
// 5. 核心開局流程控制器
// ==========================================
function startPhase1() {
showMessage(`第 ${gameRound} 局開局:中心遊戲板就位`);
centerBoardsAnimData.push(createGameBoard(2.5, 2.5, ['top', 'bottom', 'left', 'right'], -600));
centerBoardsAnimData.push(createGameBoard(4.5, 2.5, ['top', 'bottom', 'left', 'right'], -600));
centerBoardsAnimData.push(createGameBoard(2.5, 4.5, ['top', 'bottom', 'left', 'right'], 600));
centerBoardsAnimData.push(createGameBoard(4.5, 4.5, ['top', 'bottom', 'left', 'right'], 600));
isAnimatingPhase1 = true;
}
function startPhase2() {
if (gameRound === 1) {
showMessage("藍方行動:選擇並放置角遊戲板");
createMarker(0.5, 6.5, 0x00008B, () => {
clearMarkers(); blueCornerType = 'LB'; placeCornerBoard('LB'); startPhase3();
});
createMarker(6.5, 6.5, 0x00008B, () => {
clearMarkers(); blueCornerType = 'RB'; placeCornerBoard('RB'); startPhase3();
});
} else {
// 第二局由橘方先手,提供自由選擇左上角(LT)或右上角(RT)
showMessage("橘方行動:選擇並放置角遊戲板(左上角或右上角)");
createMarker(0.5, 0.5, 0xFFA500, () => {
clearMarkers(); orangeCornerType = 'LT'; placeCornerBoard('LT'); startPhase3();
});
createMarker(6.5, 0.5, 0xFFA500, () => {
clearMarkers(); orangeCornerType = 'RT'; placeCornerBoard('RT'); startPhase3();
});
}
}
function startPhase3() {
if (gameRound === 1) {
showMessage("橘方行動:放置對角遊戲板");
orangeCornerType = (blueCornerType === 'LB') ? 'RT' : 'LT';
const centerPos = (orangeCornerType === 'RT') ? {c: 6.5, r: 0.5} : {c: 0.5, r: 0.5};
createMarker(centerPos.c, centerPos.r, 0xFFA500, () => {
clearMarkers(); placeCornerBoard(orangeCornerType); startPhase4();
});
} else {
showMessage("藍方行動:放置對角遊戲板");
blueCornerType = (orangeCornerType === 'LT') ? 'RB' : 'LB';
const centerPos = (blueCornerType === 'RB') ? {c: 6.5, r: 6.5} : {c: 0.5, r: 6.5};
createMarker(centerPos.c, centerPos.r, 0x00008B, () => {
clearMarkers(); placeCornerBoard(blueCornerType); startPhase4();
});
}
}
function startPhase4() {
const teamName = gameRound === 1 ? "藍方" : "橘方";
const colorHex = gameRound === 1 ? 0x00008B : 0xFFA500;
showMessage(`${teamName}行動:在中心板上放置一個圓柱體`);
for(let r=2; r<=5; r++) {
for(let c=2; c<=5; c++) {
// 套用不可同行同列限制
if (!isRowOrColOccupied(c, r)) {
createMarker(c, r, colorHex, () => {
clearMarkers(); placeCylinder(c, r); startPhase5();
});
}
}
}
}
function startPhase5() {
const teamName = gameRound === 1 ? "橘方" : "藍方";
const colorHex = gameRound === 1 ? 0xFFA500 : 0x00008B;
showMessage(`${teamName}行動:在中心板上放置一個圓柱體 (避開同行/同列/斜相鄰)`);
for(let r=2; r<=5; r++) {
for(let c=2; c<=5; c++) {
if(isValidOrangeCenterCyl(c, r)) {
createMarker(c, r, colorHex, () => {
clearMarkers(); placeCylinder(c, r); startPhase6();
});
}
}
}
}
function startPhase6() {
if (edgeTurn > 4) {
startPhase7();
return;
}
const isBlueTurn = gameRound === 1 ? (edgeTurn % 2 !== 0) : (edgeTurn % 2 === 0);
const colorHex = isBlueTurn ? 0x00008B : 0xFFA500;
const cornerType = isBlueTurn ? blueCornerType : orangeCornerType;
const teamName = isBlueTurn ? "藍方" : "橘方";
showMessage(`${teamName}行動:放置邊遊戲板及圓柱體`);
// 修改:過濾邊緣棋位時同樣必須滿足「不與已有圓柱體同行同列」
const validCells = getEdgeCells(cornerType).filter(cell => !isRowOrColOccupied(cell.c, cell.r));
validCells.forEach(cell => {
createMarker(cell.c, cell.r, colorHex, () => {
clearMarkers();
placeEdgeBoards(cell.side);
placeCylinder(cell.c, cell.r);
edgeTurn++;
startPhase6();
});
});
}
function startPhase7() {
const teamName = gameRound === 1 ? "藍方" : "橘方";
const colorHex = gameRound === 1 ? 0x00008B : 0xFFA500;
const cornerType = gameRound === 1 ? blueCornerType : orangeCornerType;
showMessage(`${teamName}行動:放置本方移動立方體`);
const cells = getOuterCornerCells(cornerType);
cells.forEach(cell => {
createMarker(cell.c, cell.r, colorHex, () => {
clearMarkers();
placeCube(cell.c, cell.r, gameRound === 1 ? 'blue' : 'orange');
startPhase8();
});
});
}
function startPhase8() {
const teamName = gameRound === 1 ? "橘方" : "藍方";
const colorHex = gameRound === 1 ? 0xFFA500 : 0x00008B;
const cornerType = gameRound === 1 ? orangeCornerType : blueCornerType;
showMessage(`${teamName}行動:放置本方移動立方體`);
const cells = getOuterCornerCells(cornerType);
cells.forEach(cell => {
createMarker(cell.c, cell.r, colorHex, () => {
clearMarkers();
placeCube(cell.c, cell.r, gameRound === 1 ? 'orange' : 'blue');
// 補齊剩餘未覆蓋的角落
['LB', 'RB', 'LT', 'RT'].forEach(type => {
if (!placedCorners[type]) placeCornerBoard(type);
});
if (gameRound === 1) {
originalBlueCorner = blueCornerType;
originalOrangeCorner = orangeCornerType;
}
showMessage("開局階段結束!即將進入正式對戰模式...");
setTimeout(() => {
uiMsg.style.display = 'none';
startGameplayPhase();
}, 2500);
});
});
}
// ==========================================
// 6. 正式對戰遊戲階段邏輯
// ==========================================
function startGameplayPhase() {
gameplayActive = true;
currentPlayer = gameRound === 1 ? 'blue' : 'orange';
startPlayerTurn();
}
function startPlayerTurn() {
walkCount = 1;
lastDirection = null;
turnStopPositions = [];
showWalkOptions();
}
// 判斷格子是否為無法逾越的物理障礙物
function isObstacle(c, r) {
if (c < 0 || c > 7 || r < 0 || r > 7) return true; // 棋盤邊緣
if (isOccupiedByCyl(c, r)) return true; // 圓柱體
// 敵方立方體
const oppColor = currentPlayer === 'blue' ? 'orange' : 'blue';
const oppCube = cubes.find(q => q.userData.color === oppColor);
if (oppCube && oppCube.userData.col === c && oppCube.userData.row === r) return true;
return false;
}
// 計算一直直線滑動直至撞擊障礙物停下的終點座標
function calculateStopPos(startC, startR, dc, dr) {
let c = startC;
let r = startR;
while (true) {
let nextC = c + dc;
let nextR = r + dr;
if (isObstacle(nextC, nextR)) {
break;
}
c = nextC;
r = nextR;
}
return { c, r };
}
function showWalkOptions() {
clearMarkers();
const activeCube = cubes.find(q => q.userData.color === currentPlayer);
const c = activeCube.userData.col;
const r = activeCube.userData.row;
let validMoves = [];
if (walkCount === 1) {
// 第一次行走:任意四個十字方向皆可
const dirs = [{dc:0, dr:-1}, {dc:0, dr:1}, {dc:-1, dr:0}, {dc:1, dr:0}];
dirs.forEach(d => {
const stop = calculateStopPos(c, r, d.dc, d.dr);
if (stop.c !== c || stop.r !== r) validMoves.push({ dir: d, stop: stop });
});
} else {
// 第二、三次行走:基於前一次方向進行90度偏轉(左或右)
const dLeft = { dc: lastDirection.dr, dr: -lastDirection.dc };
const dRight = { dc: -lastDirection.dr, dr: lastDirection.dc };
[dLeft, dRight].forEach(d => {
const stop = calculateStopPos(c, r, d.dc, d.dr);
if (stop.c !== c || stop.r !== r) validMoves.push({ dir: d, stop: stop });
});
// 僅當左右皆為死路時,才允許往後退 (180度反轉)
if (validMoves.length === 0) {
const dBack = { dc: -lastDirection.dc, dr: -lastDirection.dr };
const stop = calculateStopPos(c, r, dBack.dc, dBack.dr);
if (stop.c !== c || stop.r !== r) validMoves.push({ dir: dBack, stop: stop });
}
}
// 如果徹底無路可走,自動跳過本次行走
if (validMoves.length === 0) {
showMessage(`${currentPlayer === 'blue'?'藍方':'橘方'}第 ${walkCount} 次行走無路可走`);
setTimeout(() => { handleWalkComplete(c, r, null, []); }, 1200);
return;
}
showMessage(`輪到 ${currentPlayer === 'blue'?'藍方':'橘方'} 行動:第 ${walkCount} 次行走`);
validMoves.forEach(mv => {
// 如果終點格子上已經放了任何一方的話筒,則行動提示標示高度調高 5 單位
const hasMic = microphones.some(m => m.userData.col === mv.stop.c && m.userData.row === mv.stop.r);
const markerY = hasMic ? 10 : 5;
const colorHex = currentPlayer === 'blue' ? 0x0000FF : 0xFF8C00;
createMarker(mv.stop.c, mv.stop.r, colorHex, () => {
clearMarkers();
executeCubeMovement(activeCube, mv.dir, mv.stop);
}, markerY);
});
}
function executeCubeMovement(cube, dir, stop) {
let traversed = [];
let curC = cube.userData.col;
let curR = cube.userData.row;
// 紀錄直線平移中經過的所有路徑格子 (含終點)
while (curC !== stop.c || curR !== stop.r) {
curC += dir.dc;
curR += dir.dr;
traversed.push({ c: curC, r: curR });
}
const worldPos = getCellWorldPos(stop.c, stop.r);
cube.userData.moving = true;
cube.userData.startX = cube.position.x;
cube.userData.startZ = cube.position.z;
cube.userData.targetX = worldPos.x;
cube.userData.targetZ = worldPos.z;
cube.userData.moveStartTime = performance.now();
cube.userData.onMoveComplete = () => {
cube.userData.col = stop.c;
cube.userData.row = stop.r;
handleWalkComplete(stop.c, stop.r, dir, traversed);
};
}
function handleWalkComplete(stopC, stopR, dir, traversed) {
lastDirection = dir;
const oppColor = currentPlayer === 'blue' ? 'orange' : 'blue';
if (walkCount === 1 || walkCount === 2) {
turnStopPositions.push({ c: stopC, r: stopR });
// 規則:第1, 2次行走若經過敵方話筒,將其清除,但不給分
if (traversed.length > 0) {
microphones = microphones.filter(m => {
const passThrough = (m.userData.color === oppColor && traversed.some(t => t.c === m.userData.col && t.r === m.userData.row));
if (passThrough) scene.remove(m);
return !passThrough;
});
}
walkCount++;
showWalkOptions();
} else if (walkCount === 3) {
// 第三次行走結束:結算得分與話筒移除
let earnedPoints = 0;
let penaltyPoint = false;
if (traversed.length > 0) {
// 1. 經過敵方話筒:每經過一個得一分,並清空該話筒
const enemyMics = microphones.filter(m => m.userData.color === oppColor && traversed.some(t => t.c === m.userData.col && t.r === m.userData.row));
earnedPoints = enemyMics.length;
enemyMics.forEach(m => scene.remove(m));
microphones = microphones.filter(m => !enemyMics.includes(m));
// 2. 本方話筒判定:若最終停留在本方話筒上,拿走話筒,且使對方得一分
const ownMicAtEnd = microphones.find(m => m.userData.color === currentPlayer && m.userData.col === stopC && m.userData.row === stopR);
if (ownMicAtEnd) {
penaltyPoint = true;
scene.remove(ownMicAtEnd);
microphones = microphones.filter(m => m !== ownMicAtEnd);
}
}
const pCube = cubes.find(q => q.userData.color === currentPlayer);
const oCube = cubes.find(q => q.userData.color === oppColor);
if (earnedPoints > 0) {
pCube.userData.score += earnedPoints;
animateCubeToScore(pCube, Math.min(6, pCube.userData.score));
}
if (penaltyPoint) {
oCube.userData.score += 1;
animateCubeToScore(oCube, Math.min(6, oCube.userData.score));
}
// 檢查是否有一方達到 6 分而終止遊戲
if (pCube.userData.score >= 6 || oCube.userData.score >= 6) {
triggerRoundEnd();
return;
}
// 進入放置話筒階段
showMicrophonePlacementOptions();
}
}
function showMicrophonePlacementOptions() {
clearMarkers();
const currentMicsCount = microphones.filter(m => m.userData.color === currentPlayer).length;
// 觸發不需放置話筒的特殊條件:場上已有4個本方話筒
if (currentMicsCount >= 4) {
switchTurn();
return;
}
// 提取第1、2次停留點(去重)
let targetSpots = [];
turnStopPositions.forEach(p => {
if (!targetSpots.some(ts => ts.c === p.c && ts.r === p.r)) targetSpots.push(p);
});
// 過濾掉已經有本方話筒的棋位
let availableSpots = targetSpots.filter(p => {
return !microphones.some(m => m.userData.color === currentPlayer && m.userData.col === p.c && m.userData.row === p.r);
});
// 如果兩個停留點皆已存在本方話筒,則本次行動不需放置
if (availableSpots.length === 0) {
switchTurn();
return;
}
showMessage(`${currentPlayer === 'blue'?'藍方':'橘方'}行動:選擇在第一次或第二次停靠點放置話筒`);
availableSpots.forEach(spot => {
createMarker(spot.c, spot.r, 0x00FF7F, () => {
clearMarkers();
createMicrophoneMesh(currentPlayer, spot.c, spot.r);
switchTurn();
}, 6);
});
}
function switchTurn() {
currentPlayer = currentPlayer === 'blue' ? 'orange' : 'blue';
startPlayerTurn();
}
// ==========================================
// 7. 局末結算與重開 UI 控制
// ==========================================
function triggerRoundEnd() {
gameplayActive = false;
clearMarkers();
const blueFinal = cubes.find(q => q.userData.color === 'blue').userData.score;
const orangeFinal = cubes.find(q => q.userData.color === 'orange').userData.score;
blueTotalScore += blueFinal;
orangeTotalScore += orangeFinal;
let roundWinner = blueFinal > orangeFinal ? "藍方" : (orangeFinal > blueFinal ? "橘方" : "平手");
// 構建 HTML 結算互動介面
const overlay = document.createElement('div');
overlay.style.cssText = 'position:absolute; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.85); display:flex; flex-direction:column; justify-content:center; align-items:center; color:#fff; font-family:sans-serif; z-index:100;';
const title = document.createElement('h2');
title.style.fontSize = '36px';
title.innerText = `第 ${gameRound} 局結束!本局勝者:${roundWinner}`;
overlay.appendChild(title);
const scoreInfo = document.createElement('p');
scoreInfo.style.fontSize = '22px';
scoreInfo.innerText = `藍方單局得分:${blueFinal} | 橘方單局得分:${orangeFinal}`;
overlay.appendChild(scoreInfo);
if (gameRound === 1) {
// 第一局結束,提供三個選項
const btnNo = createOverlayButton("否 (顯示總結算)", () => {
document.body.removeChild(overlay); showFinalGameSummary();
});
const btnSame = createOverlayButton("用當前場地再戰一局", () => {
document.body.removeChild(overlay); gameRound = 2; reinitNextRound(true);
});
const btnNew = createOverlayButton("重新開局", () => {
document.body.removeChild(overlay); gameRound = 2; reinitNextRound(false);
});
overlay.appendChild(btnNo); overlay.appendChild(btnSame); overlay.appendChild(btnNew);
} else {
// 兩局都打完了
const btnEnd = createOverlayButton("查看最終贏家及總分", () => {
document.body.removeChild(overlay); showFinalGameSummary();
});
overlay.appendChild(btnEnd);
}
document.body.appendChild(overlay);
}
function createOverlayButton(text, clickFn) {
const btn = document.createElement('button');
btn.innerText = text;
btn.style.cssText = 'padding:14px 35px; margin:10px; font-size:18px; font-weight:bold; color:#fff; background-color:#3498db; border:none; border-radius:5px; cursor:pointer; min-width:260px; box-shadow:0 3px 6px rgba(0,0,0,0.3);';
btn.addEventListener('click', clickFn);
return btn;
}
function cleanupEntities() {
microphones.forEach(m => scene.remove(m));
microphones = [];
cubes.forEach(c => scene.remove(c));
cubes = [];
clearMarkers();
}
function reinitNextRound(keepBoard) {
// 修改:移除原本在此處寫死的 gameRound = 2 賦值,使其相容於全重開機制
cleanupEntities();
if (keepBoard) {
// 沿用場地:不清除棋盤與圓柱,直接重新進入立方體角位放置階段
orangeCornerType = (originalBlueCorner === 'LB') ? 'LT' : 'RT';
blueCornerType = (orangeCornerType === 'LT') ? 'RB' : 'LB';
startPhase7();
} else {
// 重新開局:將所有動態棋盤、圓柱從場景與陣列中徹底清空
boardMeshes.forEach(b => scene.remove(b));
boardMeshes = [];
centerBoardsAnimData = [];
cylinderMeshes.forEach(c => scene.remove(c));
cylinderMeshes = [];
cylindersData = [];
placedEdges = { top: false, bottom: false, left: false, right: false };
placedCorners = { LB: false, RB: false, LT: false, RT: false };
edgeTurn = 1;
phase1Progress = 0;
startPhase1();
}
}
function showFinalGameSummary() {
const overlay = document.createElement('div');
overlay.style.cssText = 'position:absolute; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.92); display:flex; flex-direction:column; justify-content:center; align-items:center; color:#fff; font-family:sans-serif; z-index:100;';
let ultimateWinner = blueTotalScore > orangeTotalScore ? "藍方" : (orangeTotalScore > blueTotalScore ? "橘方" : "平手");
const h1 = document.createElement('h1');
h1.style.fontSize = '42px';
h1.innerText = `對抗賽終局!最終贏家:${ultimateWinner}`;
overlay.appendChild(h1);
const p = document.createElement('p');
p.style.fontSize = '26px';
p.style.margin = '20px 0 40px 0';
p.innerText = `總比分 -> 藍方總得分:${blueTotalScore} 點 || 橘方總得分:${orangeTotalScore} 點`;
overlay.appendChild(p);
const btnRestartAll = createOverlayButton("重新開始整個遊戲", () => {
document.body.removeChild(overlay);
// 完全重置所有統計數據歸零,重開必定為藍方先行 (gameRound = 1)
gameRound = 1; blueTotalScore = 0; orangeTotalScore = 0;
reinitNextRound(false);
});
overlay.appendChild(btnRestartAll);
document.body.appendChild(overlay);
}
// ==========================================
// 8. 事件綁定與主迴圈
// ==========================================
document.getElementById('btn-start').addEventListener('click', (e) => {
e.target.parentElement.style.display = 'none';
startPhase1();
});
container.addEventListener('pointerdown', (event) => {
const rect = container.getBoundingClientRect();
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
if (markers.length > 0) {
const intersects = raycaster.intersectObjects(markers);
if (intersects.length > 0) {
intersects[0].object.userData.onClick();
}
}
});
container.addEventListener('pointermove', (event) => {
if (markers.length === 0) return;
const rect = container.getBoundingClientRect();
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(markers);
if (intersects.length > 0) document.body.style.cursor = 'pointer';
else document.body.style.cursor = 'default';
});
function animate() {
requestAnimationFrame(animate);
const now = performance.now();
// 處理中心遊戲板入場動畫
if (isAnimatingPhase1) {
phase1Progress += 0.015;
if (phase1Progress >= 1) {
phase1Progress = 1;
isAnimatingPhase1 = false;
startPhase2();
}
const easeOut = phase1Progress * (2 - phase1Progress);
centerBoardsAnimData.forEach(b => {
b.mesh.position.z = b.startZ + (b.targetZ - b.startZ) * easeOut;
});
}
// 處理正式對戰階段:立方體平移與高拋平滑曲線控制效果
cubes.forEach(cube => {
if (cube.userData.moving) {
const elapsed = now - cube.userData.moveStartTime;
let t = elapsed / cube.userData.moveDuration;
if (t >= 1) {
t = 1;
cube.userData.moving = false;
if (cube.userData.onMoveComplete) {
const cb = cube.userData.onMoveComplete;
cube.userData.onMoveComplete = null;
cb();
}
}
// 直線平移 X 與 Z
cube.position.x = THREE.MathUtils.lerp(cube.userData.startX, cube.userData.targetX, t);
cube.position.z = THREE.MathUtils.lerp(cube.userData.startZ, cube.userData.targetZ, t);
// 貝塞爾高拋躍起特效:升起 20 個高度單位 (相當於5個話筒高度4*5),落回原本底盤
cube.position.y = itemY + Math.sin(t * Math.PI) * 20;
}
});
// 處理立方體得分時的 3D 旋轉翻轉點數動畫
cubes.forEach(cube => {
if (cube.userData.animating) {
const elapsed = now - cube.userData.startTime;
const duration = 1000;
let t = elapsed / duration;
if (t >= 1) {
t = 1;
cube.userData.animating = false;
}
// 翻轉時的離地跳躍微幅晃動
if (!cube.userData.moving) {
cube.position.y = itemY + Math.sin(t * Math.PI) * 48;
}
cube.quaternion.slerpQuaternions(cube.userData.startQuat, cube.userData.targetQuat, t);
}
});
controls.update();
renderer.render(scene, camera);
}
animate();
</script>
</body>
</html>
```