代码: 全选
這三個問題都已經找到了原因。以下為您詳細說明並提供修復代碼,您可以直接替換原有的對應片段。
### 一、修復「顯示箭頭」功能的問題
**問題原因**:
當關閉「顯示箭頭」模式時,程式只負責隱藏 UI,並單純調用 `showWalkOptions()`。但如果箭頭開啟前是在「布局階段」,這時 `gameplayActive` 為 `false`,關閉時便不會恢復任何標記;此外,關閉事件中漏掉了重新檢查按鈕狀態的 `updateUndoRedoButtons()`,導致 `turnUndo` 等內部邏輯產生錯亂。
**修改步驟 1:建立顯示文字的方法**
在程式碼的全局區域(建議放在 `showWalkOptions` 函式附近,或是腳本的頂層函數區塊),新增這個獨立的函式:
```javascript
function updateArrowModeMessage() {
if (!gameplayActive) return;
let currentTurnNumber = 1;
let lastPlayer = null;
for (let i = 0; i < historyUndoStack.length; i++) {
let s = historyUndoStack[i];
if (s.gameplayActive) {
if (lastPlayer === null) lastPlayer = s.currentPlayer;
else if (s.currentPlayer !== lastPlayer) {
currentTurnNumber++;
lastPlayer = s.currentPlayer;
}
}
}
const teamStr = currentPlayer === 'blue'
? (window.currentLang === 'zh' ? '藍' : 'Blue')
: (window.currentLang === 'zh' ? '綠' : 'Green');
const msgStr = window.currentLang === 'zh'
? `展示行走路徑:第${currentTurnNumber}回合,輪到${teamStr}方`
: `Showing Path: Turn ${currentTurnNumber}, ${teamStr}'s turn`;
uiMsg.style.display = 'block';
uiMsg.innerText = msgStr;
}
```
**修改步驟 2:替換觸發「顯示箭頭」的程式碼**
在 `document.querySelectorAll('.io-option').forEach` 綁定的點擊事件中,找到 `else if (action === 'arrow')` 區塊,將原本負責計算並顯示文字的段落替換如下:
```javascript
} else if (action === 'arrow') {
window.isArrowMode = true;
// 如果不是在回合開頭,自動撤銷到回合起點
while (gameplayActive && walkCount > 1) {
if (historyUndoStack.length === 0) break;
walkUndo();
}
document.getElementById('btn-ai-blue').style.display = 'none';
document.getElementById('btn-ai-green').style.display = 'none';
document.getElementById('ai-strength-trigger').style.display = 'none';
document.getElementById('btn-settings').style.display = 'none';
document.querySelector('#history-controls .divider').style.display = 'none';
document.getElementById('btn-walk-undo').style.display = 'none';
document.getElementById('btn-walk-redo').style.display = 'none';
document.getElementById('btn-io-dropdown-trigger').style.display = 'none'; // 隱藏工具菜單
let btnCloseArrow = document.getElementById('btn-close-arrow');
if (btnCloseArrow) btnCloseArrow.style.display = 'flex';
updateArrowModeMessage();
drawArrows();
}
```
**修改步驟 3:修復「關閉箭頭」的按鈕邏輯**
找到 `btnCloseArrow.addEventListener('click', ...)` 的區塊,整段替換為以下代碼以確保狀態和 UI 能正確復原:
```javascript
if (btnCloseArrow) {
btnCloseArrow.addEventListener('click', () => {
window.isArrowMode = false;
document.getElementById('btn-ai-blue').style.display = '';
document.getElementById('btn-ai-green').style.display = '';
document.getElementById('ai-strength-trigger').style.display = '';
document.getElementById('btn-settings').style.display = '';
document.querySelector('#history-controls .divider').style.display = '';
document.getElementById('btn-walk-undo').style.display = '';
document.getElementById('btn-walk-redo').style.display = '';
document.getElementById('btn-io-dropdown-trigger').style.display = ''; // 恢復工具菜單
btnCloseArrow.style.display = 'none';
clearArrows();
if (gameplayActive) {
if (currentSubPhase === 'walk') showWalkOptions();
else showMicrophonePlacementOptions();
} else {
if (currentPhaseFn) currentPhaseFn(); // 恢復布局階段狀態
}
updateUndoRedoButtons(); // 更新撤銷重做按鈕狀態
});
}
```
**修改步驟 4:在 `turnUndo` 和 `turnRedo` 中調用文字更新**
在 `turnUndo()` 的最後面加上判斷:
```javascript
restoreSnapshot(historyUndoStack.pop());
updateUndoRedoButtons();
if (window.isArrowMode && gameplayActive) updateArrowModeMessage();
```
同樣地,在 `turnRedo()` 的最後面加上判斷:
```javascript
restoreSnapshot(historyRedoStack.pop());
updateUndoRedoButtons();
if (window.isArrowMode && gameplayActive) updateArrowModeMessage();
```
---
### 二、修復「播放動畫」功能的問題
**問題原因**:
1. 播放動畫狀態下,只要碰到「得分達到 6」的歷史節點,就會無條件立即調用 `triggerRoundEnd()`,導致蓋過了 1000 毫秒的翻轉動畫。
2. 開局階段的回放會被 `!window.IS_PLAYBACK_ANIM` 直接擋下 `currentPhaseFn()` 的執行,導致 `div#message` 不會變更文字。
**修改步驟**:
找到 `restoreSnapshot(snap)` 函數中,判斷 `if (!gameplayActive)` 的區段(約在第 641 行附近),將其整段替換如下:
```javascript
if (!gameplayActive) {
let hasWinner = snap.cubesData.some(c => c.score >= 6);
if (hasWinner) {
// 若有贏家,確保給予翻轉動畫 1000 毫秒的延遲再彈出結束畫面
if (!window.IS_FAST_FORWARD) {
setTimeout(() => triggerRoundEnd(), 1000);
} else {
triggerRoundEnd();
}
return;
}
if (currentPhaseFn) {
if (window.IS_PLAYBACK_ANIM) {
// 播放動畫時,我們只需顯示文字,不生成可點擊的 marker
const tempCreateMarker = createMarker;
const tempClearMarkers = clearMarkers;
createMarker = function() {}; // 暫時架空生成標記的行為
clearMarkers = function() {};
currentPhaseFn();
createMarker = tempCreateMarker;
clearMarkers = tempClearMarkers;
// 同時確保清空任何不小心被加進來的 marker
markers.forEach(m => scene.remove(m));
markers = [];
} else {
currentPhaseFn();
}
}
} else {
if (!window.IS_PLAYBACK_ANIM) {
// 播放時隱藏行動標記
if (currentSubPhase === 'walk') showWalkOptions();
else showMicrophonePlacementOptions();
}
}
```
---
### 三、修復 `turnUndo()` 和 `turnRedo()` 的布局階段錯位問題
**問題原因**:
當程式切換到第二局(或其他換先的回合)時,`startPhase2()` 和 `startPhase3()` 中的 `createMarker` 點擊回調漏寫了 `pushAction()` 函式,導致放置「角遊戲板」的操作並未存入 `historyUndoStack` 快照陣列,自然無法進行回退。
**修改步驟 1:修補 `startPhase2**`
找到 `startPhase2()`,將 `else` 區塊(第二局綠方先手邏輯)補上 `pushAction()`:
```javascript
} else {
// 第二局由綠方先手,提供自由選擇左上角(LT)或右上角(RT)
showMessage('綠方行動:選擇並放置角遊戲板(左上角或右上角)');
createMarker(1, 1, 0xa5ff00, () => {
pushAction(); // 補上
recordMove(1, 1);
clearMarkers();
orangeCornerType = 'LT';
placeCornerBoard('LT');
startPhase3();
});
createMarker(6, 1, 0xa5ff00, () => {
pushAction(); // 補上
recordMove(6, 1);
clearMarkers();
orangeCornerType = 'RT';
placeCornerBoard('RT');
startPhase3();
});
}
```
**修改步驟 2:修補 `startPhase3**`
找到 `startPhase3()`,為所有狀況的 `createMarker` 補上 `pushAction()`:
```javascript
function startPhase3() {
currentPhaseFn = startPhase3;
if (gameRound === 1) {
showMessage('綠方行動:放置對角遊戲板');
orangeCornerType = blueCornerType === 'LB' ? 'RT' : 'LT';
const centerPos = orangeCornerType === 'RT' ? { c: 6, r: 1 } : { c: 1, r: 1 };
createMarker(centerPos.c, centerPos.r, 0xa5ff00, () => {
pushAction(); // 補上
recordMove(centerPos.c, centerPos.r);
clearMarkers();
placeCornerBoard(orangeCornerType);
startPhase4();
});
} else {
showMessage('藍方行動:放置對角遊戲板');
blueCornerType = orangeCornerType === 'LT' ? 'RB' : 'LB';
const centerPos = blueCornerType === 'RB' ? { c: 6, r: 6 } : { c: 1, r: 6 };
createMarker(centerPos.c, centerPos.r, 0x00008b, () => {
pushAction(); // 補上
recordMove(centerPos.c, centerPos.r);
clearMarkers();
placeCornerBoard(blueCornerType);
startPhase4();
});
}
triggerAIIfReady();
}
```
修復和改進:
一,「顯示箭頭」功能的問題
當點擊「顯示箭頭」之後,在div#message顯示文字:「展示行走路徑:第n回合,輪到…方」。現在把這段顯示文字的程式碼打包成一個函式,再在turnUndo()和turnRedo()中調用這個函式。(注意在turnUndo()和turnRedo()中調用這個函式的時候,只限於遊戲階段,如果是布局階段,仍然要用原來的顯示文字的方法)
當點擊「顯示箭頭」,再點擊「關閉」,這時turnUndo()和turnRedo()就會錯亂,如某一回合回退時,本來只影響到一個立方體,但是兩個立方體位置都變了。查找原因。
二,「播放動畫」功能的問題
當處於「自動播放」狀態時,當播放到最後獲勝的一步,需要先等立方體翻轉的動畫結束,再彈出「全屏文字提示」,同時停止播放。
當「自動播放」布局階段時,div#message應顯示相關文字。
三,turnUndo()和turnRedo()的問題
當遊戲走到第二局時,第二局的布局階段的第一回合,即雙方各放一個「角遊戲板」的回合,並不能用turnUndo()和turnRedo()來復現,請修復這個問題。
需要明確給出所要修改的地方和所要替換的代碼。
代码: 全选
這三個問題分別涉及到不同層面的狀態管理與非同步時序問題。
以下是針對這三個問題的修復方案,請在你的 `csinger2462.html` 中找到對應的程式碼並進行替換:
### 一、「顯示箭頭」功能的問題修復
**原因分析**:原本的「顯示箭頭」機制為求方便,直接呼叫了真實的 `walkUndo()` 來強行退回回合起點,導致了歷史紀錄堆疊 (`historyUndoStack` 與 `historyRedoStack`) 狀態錯亂。當你關閉箭頭時,狀態沒有恢復,後續的撤銷自然會影響到錯位的立方體。
**修復方式**:提取文字顯示為獨立函式,並加上「進入時記錄退回次數,關閉時自動重做補回」的機制。
**1. 建立 `updateArrowModeMessage` 函式**
請在 `function drawArrows() { ... }` 的上方(或同一個區域),新增以下函式:
```javascript
function updateArrowModeMessage() {
let currentTurnNumber = 1;
let lastPlayer = null;
for (let i = 0; i < historyUndoStack.length; i++) {
let s = historyUndoStack[i];
if (s.gameplayActive) {
if (lastPlayer === null) lastPlayer = s.currentPlayer;
else if (s.currentPlayer !== lastPlayer) {
currentTurnNumber++;
lastPlayer = s.currentPlayer;
}
}
}
const teamStr = currentPlayer === 'blue'
? (window.currentLang === 'zh' ? '藍' : 'Blue')
: (window.currentLang === 'zh' ? '綠' : 'Green');
const msgStr = window.currentLang === 'zh'
? `展示行走路徑:第${currentTurnNumber}回合,輪到${teamStr}方`
: `Showing Path: Turn ${currentTurnNumber}, ${teamStr}'s turn`;
uiMsg.style.display = 'block';
uiMsg.innerText = msgStr;
}
```
**2. 在 `turnUndo` 與 `turnRedo` 底部呼叫**
在 `function turnUndo() { ... }` 與 `function turnRedo() { ... }` 兩個函式的**最末端**(即 `updateUndoRedoButtons();` 之後),分別加上同一段判定:
```javascript
updateUndoRedoButtons();
// --- 新增:若為箭頭模式且在遊戲階段,刷新箭頭與文字 ---
if (window.isArrowMode && gameplayActive) {
updateArrowModeMessage();
drawArrows();
}
} // 這是原本函式的結束大括號
```
**3. 替換按鈕點擊事件**
找到 `document.querySelectorAll('.io-option').forEach(opt => { ... }` 中的 `if (action === 'arrow') { ... }` 區塊,以及 `btnCloseArrow` 的點擊事件,將這兩段替換為以下程式碼:
```javascript
// 替換 btnCloseArrow 點擊事件
const btnCloseArrow = document.getElementById('btn-close-arrow');
if (btnCloseArrow) {
btnCloseArrow.addEventListener('click', () => {
window.isArrowMode = false;
document.getElementById('btn-ai-blue').style.display = '';
document.getElementById('btn-ai-green').style.display = '';
document.getElementById('ai-strength-trigger').style.display = '';
document.getElementById('btn-settings').style.display = '';
document.querySelector('#history-controls .divider').style.display = '';
document.getElementById('btn-walk-undo').style.display = '';
document.getElementById('btn-walk-redo').style.display = '';
document.getElementById('btn-io-dropdown-trigger').style.display = '';
btnCloseArrow.style.display = 'none';
clearArrows();
// 自動將剛才退回的步數補回去,恢復實際遊戲狀態
if (window.arrowModeUndoCount && window.arrowModeUndoCount > 0) {
for (let i = 0; i < window.arrowModeUndoCount; i++) {
walkRedo();
}
window.arrowModeUndoCount = 0;
}
if (gameplayActive) {
let snap = historyUndoStack[historyUndoStack.length - 1] || saveSnapshot();
if (snap.subPhase === 'walk') showWalkOptions();
else showMicrophonePlacementOptions();
}
});
}
```
```javascript
// 在 IO 選單中,替換 action === 'arrow' 區塊
} else if (action === 'arrow') {
window.isArrowMode = true;
// 如果不是在回合開頭,自動撤銷到回合起點,並「記錄撤銷次數」
window.arrowModeUndoCount = 0;
while (gameplayActive && walkCount > 1) {
if (historyUndoStack.length === 0) break;
walkUndo();
window.arrowModeUndoCount++;
}
document.getElementById('btn-ai-blue').style.display = 'none';
document.getElementById('btn-ai-green').style.display = 'none';
document.getElementById('ai-strength-trigger').style.display = 'none';
document.getElementById('btn-settings').style.display = 'none';
document.querySelector('#history-controls .divider').style.display = 'none';
document.getElementById('btn-walk-undo').style.display = 'none';
document.getElementById('btn-walk-redo').style.display = 'none';
document.getElementById('btn-io-dropdown-trigger').style.display = 'none';
let btnCloseArrow = document.getElementById('btn-close-arrow');
if (btnCloseArrow) btnCloseArrow.style.display = 'flex';
updateArrowModeMessage();
drawArrows();
}
```
---
### 二、「播放動畫」功能的問題修復
**原因分析**:恢復快照 (`restoreSnapshot`) 時,遇到勝負局面會立刻呼叫 `triggerRoundEnd()` 彈出結算視窗,遮蓋了延遲翻轉的動畫。佈局階段沒有設定 `uiMsg.innerText` 則是漏寫了階段對應邏輯。
**1. 修改 `restoreSnapshot` 中的結算觸發**
找到 `function restoreSnapshot(snap) {` 中段的以下程式碼:
```javascript
if (!gameplayActive) {
let hasWinner = snap.cubesData.some(c => c.score >= 6);
if (hasWinner) {
if (!window.IS_FAST_FORWARD) triggerRoundEnd();
return;
}
```
請將這段替換為:
```javascript
if (!gameplayActive) {
let hasWinner = snap.cubesData.some(c => c.score >= 6);
if (hasWinner) {
if (!window.IS_FAST_FORWARD) {
// 延遲1.5秒,等待勝利翻轉動畫完成後再彈出提示
setTimeout(() => {
let currentSnap = historyUndoStack[historyUndoStack.length - 1];
if (currentSnap && !currentSnap.gameplayActive && currentSnap.cubesData.some(c => c.score >= 6)) {
if (!document.getElementById('round-end-overlay') && !document.getElementById('final-summary-overlay')) {
triggerRoundEnd();
}
}
}, 1500);
} else {
if (!document.getElementById('round-end-overlay') && !document.getElementById('final-summary-overlay')) {
triggerRoundEnd();
}
}
return;
}
```
**2. 修改 `updatePlaybackUI` 以顯示佈局文字**
找到 `function updatePlaybackUI() {` 裡面的這段:
```javascript
if (snap) {
console.log('snap', snap);
if (!snap.gameplayActive) {
dispRound = 0;
dispStep = '0';
} else {
```
請將這段替換為:
```javascript
if (snap) {
if (!snap.gameplayActive) {
dispRound = 0;
dispStep = '0';
// 補齊佈局階段的文字顯示
let msg = '';
let teamName = '';
if (snap.currentPhaseFn === startPhase2) {
if (snap.gameRound === 1) msg = window.currentLang === 'zh' ? '藍方行動:選擇並放置角遊戲板' : 'Blue Turn: Select & place corner board';
else msg = window.currentLang === 'zh' ? '綠方行動:選擇並放置角遊戲板(左上角或右上角)' : 'Green Turn: Place corner board (Top-Left or Top-Right)';
} else if (snap.currentPhaseFn === startPhase3) {
if (snap.gameRound === 1) msg = window.currentLang === 'zh' ? '綠方行動:放置對角遊戲板' : 'Green Turn: Place opposite corner';
else msg = window.currentLang === 'zh' ? '藍方行動:放置對角遊戲板' : 'Blue Turn: Place opposite corner';
} else if (snap.currentPhaseFn === startPhase4) {
teamName = snap.gameRound === 1 ? (window.currentLang === 'zh' ? '藍方' : 'Blue') : (window.currentLang === 'zh' ? '綠方' : 'Green');
msg = window.currentLang === 'zh' ? `${teamName}行動:在中心板上放置一個圓柱體` : `${teamName} Turn: Place cylinder on center board`;
} else if (snap.currentPhaseFn === startPhase5) {
teamName = snap.gameRound === 1 ? (window.currentLang === 'zh' ? '綠方' : 'Green') : (window.currentLang === 'zh' ? '藍方' : 'Blue');
msg = window.currentLang === 'zh' ? `${teamName}行動:在中心板上放置一個圓柱體 (避開同行/同列/斜相鄰)` : `${teamName} Turn: Place cylinder on center board (Avoid row/col/diag)`;
} else if (snap.currentPhaseFn === startPhase6) {
const isBlueTurn = snap.gameRound === 1 ? snap.edgeTurn % 2 !== 0 : snap.edgeTurn % 2 === 0;
teamName = isBlueTurn ? (window.currentLang === 'zh' ? '藍方' : 'Blue') : (window.currentLang === 'zh' ? '綠方' : 'Green');
msg = window.currentLang === 'zh' ? `${teamName}行動:放置邊遊戲板及圓柱體` : `${teamName} Turn: Place edge board & cylinder`;
} else if (snap.currentPhaseFn === startPhase7 || snap.currentPhaseFn === startPhase8) {
const isBlue = (snap.currentPhaseFn === startPhase7 && snap.gameRound === 1) || (snap.currentPhaseFn === startPhase8 && snap.gameRound === 2);
teamName = isBlue ? (window.currentLang === 'zh' ? '藍方' : 'Blue') : (window.currentLang === 'zh' ? '綠方' : 'Green');
msg = window.currentLang === 'zh' ? `${teamName}行動:放置本方歌手` : `${teamName} Turn: Place own singer`;
}
if (msg) {
uiMsg.style.display = 'block';
uiMsg.innerText = msg;
}
} else {
```
---
### 三、`turnUndo()` 和 `turnRedo()` 佈局階段無效問題
**原因分析**:這是一個隱蔽的疏漏。在第二局的 `startPhase2`(綠方先選角板)與第二局的 `startPhase3` 裡的 `createMarker` 點擊回呼函式(Callback)中,開發者忘了呼叫 `pushAction();`,這導致點擊放置時系統根本沒有拍攝快照存進歷史陣列,因此無從撤銷。
**修復方式**:為缺漏的地方補上 `pushAction();`。
**1. 修改 `startPhase2**`
找到 `function startPhase2()` 中的這段程式碼:
```javascript
showMessage('綠方行動:選擇並放置角遊戲板(左上角或右上角)');
createMarker(1, 1, 0xa5ff00, () => {
recordMove(1, 1);
clearMarkers();
// ...
});
createMarker(6, 1, 0xa5ff00, () => {
recordMove(6, 1);
clearMarkers();
// ...
});
```
補上 `pushAction()`,替換為:
```javascript
showMessage('綠方行動:選擇並放置角遊戲板(左上角或右上角)');
createMarker(1, 1, 0xa5ff00, () => {
pushAction(); // <--- 補上這裡
recordMove(1, 1);
clearMarkers();
orangeCornerType = 'LT';
placeCornerBoard('LT');
startPhase3();
});
createMarker(6, 1, 0xa5ff00, () => {
pushAction(); // <--- 補上這裡
recordMove(6, 1);
clearMarkers();
orangeCornerType = 'RT';
placeCornerBoard('RT');
startPhase3();
});
```
**2. 修改 `startPhase3**`
找到 `function startPhase3()`,替換裡面兩個 `createMarker` 的內容:
```javascript
function startPhase3() {
currentPhaseFn = startPhase3;
if (gameRound === 1) {
showMessage('綠方行動:放置對角遊戲板');
orangeCornerType = blueCornerType === 'LB' ? 'RT' : 'LT';
const centerPos = orangeCornerType === 'RT' ? { c: 6, r: 1 } : { c: 1, r: 1 };
createMarker(centerPos.c, centerPos.r, 0xa5ff00, () => {
pushAction(); // <--- 補上這裡
recordMove(centerPos.c, centerPos.r);
clearMarkers();
placeCornerBoard(orangeCornerType);
startPhase4();
});
} else {
showMessage('藍方行動:放置對角遊戲板');
blueCornerType = orangeCornerType === 'LT' ? 'RB' : 'LB';
const centerPos = blueCornerType === 'RB' ? { c: 6, r: 6 } : { c: 1, r: 6 };
createMarker(centerPos.c, centerPos.r, 0x00008b, () => {
pushAction(); // <--- 補上這裡
recordMove(centerPos.c, centerPos.r);
clearMarkers();
placeCornerBoard(blueCornerType);
startPhase4();
});
}
triggerAIIfReady();
}
```