代码: 全选
這裡為你整理了所有需要修復與改進的代碼塊。我們將依序解決撤銷重做 BUG、更新規則多語言與 HTML、修復結算面板多語言,以及全面優化 SVG 導出的動畫與數值精度。
### 1. 修改 `I18N` 字典與 `toggleLanguage` (解決第二點)
我們需要在中文和英文的 `rule` 屬性中加入第二段落的計分規則。為了讓換行符號 `<br><br>` 生效,還需要將切換語言時的 `innerText` 修改為 `innerHTML`。
**替換程式碼:** (大約在第 746 行)
```javascript
// --- 多語言字典 ---
const I18N = {
zh: {
title: '三菱棋',
sub: '一個抽象策略遊戲,在二維與三維之間',
ruleTitle: '規則簡介',
rule: '雙方輪流落子,先手方第一手必須下藍色棋子,後手方第一手必須下紅色棋子,此後雙方輪流行動,分數高者獲勝。<br><br>計分規則:雙方相對而坐,則我方的上面是對方的下面。從各方視角望去,藍色棋子視為一個立方體的頂面,紅色和橙色棋子視為側面,一個立方體記一分。',
startBtn: '開始遊戲',
// ... (保留中間其他翻譯) ...
```
**(大約在第 813 行)**
```javascript
en: {
title: 'Trirhombus',
sub: 'An abstract strategy game between 2D and 3D',
ruleTitle: 'Rules Overview',
rule: 'Players take turns. The first player must place a blue piece first. The second player must place a red piece first. Play alternates thereafter. Highest score wins.<br><br>Scoring Rules: Players sit opposite each other, so your "up" is the opponent\'s "down". From each perspective, blue pieces are seen as the top face of a cube, while red and orange pieces are the side faces. Each perceived cube scores one point.',
startBtn: 'Start Game',
// ... (保留中間其他翻譯) ...
```
**(大約在第 1358 行 - `toggleLanguage` 函數中)**
將原本的 `.innerText` 替換為 `.innerHTML`:
```javascript
document.getElementById('text-title').innerText = getText('title');
document.getElementById('text-sub').innerText = getText('sub');
document.getElementById('text-rule-title').innerText = getText('ruleTitle');
document.getElementById('rule-content').innerHTML = getText('rule'); // 這裡改用 innerHTML
document.getElementById('text-start-btn').innerText = getText('startBtn');
```
---
### 2. 更新 HTML 中初始顯示的規則文字 (解決第二點)
請找到 `<div id="start-screen">` 內部的 `#rule-content` 元素,並同步更新它的初始 HTML:
**替換程式碼:** (大約在第 356 行)
```html
<div class="rules-content" id="rule-content">
雙方輪流落子,先手方第一手必須下藍色棋子,後手方第一手必須下紅色棋子,此後雙方輪流行動,分數高者獲勝。<br><br>計分規則:雙方相對而坐,則我方的上面是對方的下面。從各方視角望去,藍色棋子視為一個立方體的頂面,紅色和橙色棋子視為側面,一個立方體記一分。
</div>
```
---
### 3. 修復後退(Undo)時棋子不消失的問題 (解決第一點)
由於你在盤面上新增了小白點 (`last-move-indicator`) 作為提示,它成為了 `etanidrop` 的 `lastChild`,導致原本依賴 `lastChild` 檢查 `.tiledropped` 的刪除邏輯失效。我們改為精準選取最後一個 `tiledropped` 元素。
**替換程式碼:** (在 `undoMove()` 函數中,大約第 1121 行)
```javascript
let scores = gameLogic.undo();
p1CurrentScore = scores.p1Score;
p2CurrentScore = scores.p2Score;
piecesLeft[move.player][move.pid]++;
historyIndex--;
let dropG = document.getElementById('etanidrop');
let tiles = dropG.querySelectorAll('.tiledropped');
if (tiles.length > 0) {
dropG.removeChild(tiles[tiles.length - 1]);
}
isFirstMove = historyIndex < roundStart;
```
---
### 4. 修復結算視窗的語言顯示問題 (解決第三點)
把寫死的 `P1(我方)` 等字眼替換為動態獲取 `I18N` 的值。
**替換程式碼:** (在 `showRoundEnd()` 函數中,大約第 1419 行)
```javascript
if (currentRound === 1) {
p1Round1Score = s1;
p2Round1Score = s2;
p1TotalScore = s1;
p2TotalScore = s2;
title.innerText = getText('gameOver') + ' - Round 1';
scoresDiv.innerHTML = `${getText('round1Score')}<br>${getText('p1You')}: ${s1} - ${getText('p2Opp')}: ${s2}`;
actionsDiv.innerHTML = `
<button class="btn-secondary" onclick="finalEnd()">${getText('endGameBtn')}</button>
<button class="btn-primary" onclick="startRound2()">${getText('swapBtn')}</button>
`;
} else {
p1TotalScore = p1Round1Score + s1;
p2TotalScore = p2Round1Score + s2;
title.innerText = getText('gameOver') + ' - Final';
let winnerText =
p1TotalScore > p2TotalScore
? getText('winnerMe')
: p2TotalScore > p1TotalScore
? getText('winnerOpp')
: getText('draw');
scoresDiv.innerHTML = `
${getText('round2Score')} ${getText('p1You')}: ${s1} - ${getText('p2Opp')}: ${s2}<br>
<hr style="margin:15px 0;">
<strong>${getText('totalScore')} ${getText('p1You')}: ${p1TotalScore} - ${getText('p2Opp')}: ${p2TotalScore}</strong><br>
<span style="color:var(--p2-color)">${winnerText}</span>
`;
actionsDiv.innerHTML = `<button class="btn-primary" onclick="restartGame()">${getText('restartBtn')}</button>`;
}
```
---
### 5. 優化 SVG 動畫導出與數值修復 (解決第四點)
加入過濾多餘小數點的格式化函數 `fmt()`,動態計算文本動畫相對於當前視圖 `pan` 及 `scale` 的坐標,並為所有棋子加入專屬的初始退場動畫。
**替換程式碼:** (覆蓋整個 `updateDlPreview` 函數,大約第 1217 行)
```javascript
function updateDlPreview() {
const originalSvg = document.getElementById('etani');
let svgClone = originalSvg.cloneNode(true);
svgClone.setAttribute('id', 'etani-preview');
// 定義數字格式化,最多 3 位小數並自動去掉末尾多餘的 0
const fmt = v => Number(v.toFixed(3));
let gridOpt = document.getElementById('dl-opt-grid').checked;
let numOpt = document.getElementById('dl-opt-num').checked;
let animOpt = document.getElementById('dl-opt-anim').checked;
// 1. 處理網格
if (!gridOpt) {
let etboard = svgClone.querySelector('.etboard');
let gridPath = etboard.querySelector('path');
if (gridPath) gridPath.remove();
}
// 2. 獲取當前局的所有合法移動
let roundStart = currentRound === 2 ? 36 : 0;
let activeMoves = moveHistory.slice(roundStart, historyIndex + 1);
// 3. 處理數字與動畫:重構 etdrop 內的棋子,確保順序與 ID 正確
let etdrop = svgClone.querySelector('.etdrop');
// 保留 board (含網格與紫點)
let etboard = etdrop.querySelector('.etboard');
etdrop.innerHTML = '';
etdrop.appendChild(etboard);
// 加入自定義字體
if (numOpt || animOpt) {
let styleEl = document.createElement('style');
styleEl.innerHTML = `
@font-face {
font-family: 'ZT Nature';
src: url('data:font/woff2;charset=utf-8;base64,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') format('woff2');
font-weight: normal;
font-style: normal;
font-display: swap;
}
`;
etboard.appendChild(styleEl);
}
// 準備全域動畫基底
if (animOpt) {
etdrop.setAttribute('opacity', '0');
let baseAnim = document.createElementNS('http://www.w3.org/2000/svg', 'animate');
baseAnim.setAttribute('id', 'tr0');
baseAnim.setAttribute('begin', '0;trend.end');
baseAnim.setAttribute('attributeName', 'opacity');
baseAnim.setAttribute('values', '0;0;1');
baseAnim.setAttribute('fill', 'freeze');
baseAnim.setAttribute('dur', '2s');
etdrop.appendChild(baseAnim);
}
// 重繪所有目前局的棋子
activeMoves.forEach((m, i) => {
let px = fmt(m.px);
let py = fmt(m.py);
let useNode = document.createElementNS('http://www.w3.org/2000/svg', 'use');
useNode.setAttribute('href', `#tile${m.pid}`);
useNode.setAttribute('class', 'tiledropped');
useNode.setAttribute('fill', TILE_COLORS[m.pid]);
useNode.setAttribute('transform', `translate(${px}, ${py})`);
if (animOpt) {
let outY = fmt(m.player === 1 ? (480 - panY) / currentScale + 100 : -panY / currentScale - 100);
// 加入初始動畫,讓棋子從目前的位置瞬間移出視窗(模擬退場的起始態)
let initAnim = document.createElementNS('http://www.w3.org/2000/svg', 'animateTransform');
initAnim.setAttribute('attributeName', 'transform');
initAnim.setAttribute('attributeType', 'XML');
initAnim.setAttribute('type', 'translate');
initAnim.setAttribute('from', `${px},${py}`);
initAnim.setAttribute('to', `${px},${outY}`);
initAnim.setAttribute('dur', '1s');
initAnim.setAttribute('fill', 'freeze');
initAnim.setAttribute('begin', 'tr0.begin');
useNode.appendChild(initAnim);
let animNode = document.createElementNS('http://www.w3.org/2000/svg', 'animateTransform');
animNode.setAttribute('id', `tr${i + 1}`);
animNode.setAttribute('attributeName', 'transform');
animNode.setAttribute('attributeType', 'XML');
animNode.setAttribute('type', 'translate');
animNode.setAttribute('from', `${px},${outY}`);
animNode.setAttribute('to', `${px},${py}`);
animNode.setAttribute('dur', '1s');
animNode.setAttribute('fill', 'freeze');
animNode.setAttribute('begin', `tr${i}.end`);
useNode.appendChild(animNode);
}
etdrop.appendChild(useNode);
// 處理數字
if (numOpt) {
let halfWidth = 31.1769;
let cx1 = m.t1.isRight ? (m.t1.idx - 1) * halfWidth + 10.392 : (m.t1.idx - 1) * halfWidth + 20.784;
let cy1 = m.t1.N * 18;
let isRight2 = gameLogic.isRight(m.t2.idx, m.t2.N);
let cx2 = isRight2 ? (m.t2.idx - 1) * halfWidth + 10.392 : (m.t2.idx - 1) * halfWidth + 20.784;
let cy2 = m.t2.N * 18;
let pieceCx = (cx1 + cx2) / 2;
let pieceCy = (cy1 + cy2) / 2;
let numStr = String(i + 1);
let offsetX1 = -7,
offsetY1 = 8;
let offsetX2 = -14,
offsetY2 = 8;
let dx = numStr.length === 1 ? offsetX1 : offsetX2;
let dy = numStr.length === 1 ? offsetY1 : offsetY2;
let textNode = document.createElementNS('http://www.w3.org/2000/svg', 'text');
textNode.setAttribute('x', fmt(pieceCx + dx).toString());
textNode.setAttribute('y', fmt(pieceCy + dy).toString());
textNode.setAttribute('stroke', 'none');
textNode.setAttribute('fill', 'black');
textNode.setAttribute('font-size', '24');
textNode.setAttribute('font-family', 'ZT Nature');
textNode.textContent = numStr;
etdrop.appendChild(textNode);
}
});
// 4. 動畫結算邏輯
if (animOpt && activeMoves.length > 0) {
let lastTrId = `tr${activeMoves.length}`;
let trmeBase = document.createElementNS('http://www.w3.org/2000/svg', 'animate');
trmeBase.setAttribute('id', 'trme');
trmeBase.setAttribute('begin', `${lastTrId}.end+1s`);
trmeBase.setAttribute('dur', '4s');
etdrop.appendChild(trmeBase);
let trheBase = document.createElementNS('http://www.w3.org/2000/svg', 'animate');
trheBase.setAttribute('id', 'trhe');
trheBase.setAttribute('begin', 'trme.end+1s');
trheBase.setAttribute('dur', '4s');
etdrop.appendChild(trheBase);
let finalScores = gameLogic.calculateScores();
let useElements = etdrop.querySelectorAll('.tiledropped');
// 計算無用棋子 (拔掉後對應玩家分數不變)
for (let i = 0; i < activeMoves.length; i++) {
let testLogic = new HexGridGame();
for (let j = 0; j < activeMoves.length; j++) {
if (i === j) continue;
let m = activeMoves[j];
testLogic.tryPlacePiece(m.pid + 1, { x: m.t1.idx, y: m.t1.N }, { x: m.t2.idx, y: m.t2.N }, true);
}
let s = testLogic.calculateScores();
let m = activeMoves[i];
let px = fmt(m.px);
let py = fmt(m.py);
let len = Math.hypot(m.px, m.py) || 1;
let outX = fmt(m.px + (m.px / len) * 400);
let outY2 = fmt(m.py + (m.py / len) * 400);
// 我方(P1)無用棋子
if (s.p1Score === finalScores.p1Score) {
let anim = document.createElementNS('http://www.w3.org/2000/svg', 'animateTransform');
anim.setAttribute('attributeName', 'transform');
anim.setAttribute('attributeType', 'XML');
anim.setAttribute('type', 'translate');
anim.setAttribute(
'values',
`${px},${py}; ${outX},${outY2}; ${outX},${outY2}; ${outX},${outY2}; ${px},${py}`
);
anim.setAttribute('dur', '4s');
anim.setAttribute('fill', 'freeze');
anim.setAttribute('begin', 'trme.begin');
useElements[i].appendChild(anim);
}
// 對方(P2)無用棋子
if (s.p2Score === finalScores.p2Score) {
let anim = document.createElementNS('http://www.w3.org/2000/svg', 'animateTransform');
anim.setAttribute('attributeName', 'transform');
anim.setAttribute('attributeType', 'XML');
anim.setAttribute('type', 'translate');
anim.setAttribute(
'values',
`${px},${py}; ${outX},${outY2}; ${outX},${outY2}; ${outX},${outY2}; ${px},${py}`
);
anim.setAttribute('dur', '4s');
anim.setAttribute('fill', 'freeze');
anim.setAttribute('begin', 'trhe.begin');
useElements[i].appendChild(anim);
}
}
// 添加分數文字動畫:動態計算相對於縮放及平移的畫面坐標
let p1TextX = fmt((480 - panX) / currentScale + 12 / currentScale);
let p1TextY = fmt((480 - panY) / currentScale - 12 / currentScale);
let p1SlideX = fmt(-144 / currentScale);
let p1Text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
p1Text.setAttribute('id', 'trmetxt');
p1Text.setAttribute('x', p1TextX.toString());
p1Text.setAttribute('y', p1TextY.toString());
p1Text.setAttribute('stroke', 'none');
p1Text.setAttribute('fill', 'black');
p1Text.setAttribute('font-size', '60');
p1Text.setAttribute('font-family', 'ZT Nature');
p1Text.textContent = finalScores.p1Score.toString();
let p1Anim = document.createElementNS('http://www.w3.org/2000/svg', 'animateTransform');
p1Anim.setAttribute('attributeName', 'transform');
p1Anim.setAttribute('attributeType', 'XML');
p1Anim.setAttribute('type', 'translate');
p1Anim.setAttribute('values', `0,0; ${p1SlideX},0; ${p1SlideX},0; ${p1SlideX},0; 0,0`);
p1Anim.setAttribute('dur', '4s');
p1Anim.setAttribute('fill', 'freeze');
p1Anim.setAttribute('begin', 'trme.begin');
p1Text.appendChild(p1Anim);
etboard.appendChild(p1Text);
let p2TextX = fmt(panX / currentScale + 12 / currentScale);
let p2TextY = fmt(panY / currentScale - 12 / currentScale);
let p2SlideX = fmt(-144 / currentScale);
let p2Text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
p2Text.setAttribute('id', 'trhetxt');
p2Text.setAttribute('x', p2TextX.toString());
p2Text.setAttribute('y', p2TextY.toString());
p2Text.setAttribute('stroke', 'none');
p2Text.setAttribute('fill', 'black');
p2Text.setAttribute('font-size', '60');
p2Text.setAttribute('font-family', 'ZT Nature');
p2Text.setAttribute('transform', 'rotate(180)');
p2Text.textContent = finalScores.p2Score.toString();
let p2Anim = document.createElementNS('http://www.w3.org/2000/svg', 'animateTransform');
p2Anim.setAttribute('attributeName', 'transform');
p2Anim.setAttribute('attributeType', 'XML');
p2Anim.setAttribute('type', 'translate');
p2Anim.setAttribute('values', `0,0; ${p2SlideX},0; ${p2SlideX},0; ${p2SlideX},0; 0,0`);
p2Anim.setAttribute('dur', '4s');
p2Anim.setAttribute('fill', 'freeze');
p2Anim.setAttribute('additive', 'sum'); // 疊加在 rotate(180) 上
p2Anim.setAttribute('begin', 'trhe.begin');
p2Text.appendChild(p2Anim);
etboard.appendChild(p2Text);
// 結尾漸隱
let endAnim = document.createElementNS('http://www.w3.org/2000/svg', 'animate');
endAnim.setAttribute('id', 'trend');
endAnim.setAttribute('begin', 'trhe.end+1s');
endAnim.setAttribute('attributeName', 'opacity');
endAnim.setAttribute('values', '1;1;0');
endAnim.setAttribute('fill', 'freeze');
endAnim.setAttribute('dur', '2s');
etdrop.appendChild(endAnim);
}
// 5. 序列化與渲染預覽
let serializer = new XMLSerializer();
currentDlSvgSource = serializer.serializeToString(svgClone);
if (!currentDlSvgSource.match(/^<svg[^>]+xmlns="http\:\/\/www\.w3\.org\/2000\/svg"/)) {
currentDlSvgSource = currentDlSvgSource.replace(/^<svg/, '<svg xmlns="http://www.w3.org/2000/svg"');
}
document.getElementById('dl-preview-container').innerHTML = '';
document.getElementById('dl-preview-container').appendChild(svgClone);
let sizeKb = new Blob([currentDlSvgSource]).size.toLocaleString();
document.getElementById('dl-size').innerText = sizeKb + ' Bytes';
}
```代码: 全选
好的,為你調整 **第 1 點(I18N 字典)** 與 **第 2 點(HTML 初始文字)** 的修改方案。我們將文字更新為包含「立方體可堆疊累加」,並將原本的 `<br><br>` 全部替換為更符合語意且結構乾淨的 `<p>` 標籤。
### 1. 修改 `I18N` 字典與 `toggleLanguage`
請將 JavaScript 中的 `I18N` 字典中關於 `rule` 的部分替換為以下程式碼(包含英文翻譯的更新):
**替換程式碼:** (大約在第 746 行起)
```javascript
// --- 多語言字典 ---
const I18N = {
zh: {
title: '三菱棋',
sub: '一個抽象策略遊戲,在二維與三維之間',
ruleTitle: '規則簡介',
rule: '<p>雙方輪流落子,先手方第一手必須下藍色棋子,後手方第一手必須下紅色棋子,此後雙方輪流行動,分數高者獲勝。</p><p>計分規則:雙方相對而坐,則我方的上面是對方的下面。從各方視角望去,藍色棋子視為一個立方體的頂面,紅色和橙色棋子視為側面,一個立方體記一分,立方體可堆疊累加。</p>',
startBtn: '開始遊戲',
// ... (保留中間其他中文翻譯) ...
```
**(大約在第 813 行起)**
```javascript
en: {
title: 'Trirhombus',
sub: 'An abstract strategy game between 2D and 3D',
ruleTitle: 'Rules Overview',
rule: '<p>Players take turns. The first player must place a blue piece first. The second player must place a red piece first. Play alternates thereafter. Highest score wins.</p><p>Scoring Rules: Players sit opposite each other, so your "up" is the opponent\'s "down". From each perspective, blue pieces are seen as the top face of a cube, while red and orange pieces are the side faces. Each perceived cube scores one point, and cubes can be stacked and accumulated.</p>',
startBtn: 'Start Game',
// ... (保留中間其他英文翻譯) ...
```
*註:請確保在 `toggleLanguage` 函數中,`document.getElementById('rule-content')` 依然是使用 `.innerHTML` 賦值(如前一次修改所示),這樣 `<p>` 標籤才能正確渲染。*
---
### 2. 更新 HTML 中初始顯示的規則文字
同步將 HTML 結構中初始顯示的文字改用 `<p>` 標籤包裹:
**替換程式碼:** (大約在第 356 行)
```html
<div class="rules-content" id="rule-content">
<p>雙方輪流落子,先手方第一手必須下藍色棋子,後手方第一手必須下紅色棋子,此後雙方輪流行動,分數高者獲勝。</p>
<p>計分規則:雙方相對而坐,則我方的上面是對方的下面。從各方視角望去,藍色棋子視為一個立方體的頂面,紅色和橙色棋子視為側面,一個立方體記一分,立方體可堆疊累加。</p>
</div>
```
*(提示:如果使用 `<p>` 標籤後發現段落上下的間距過大或過小,可以在 CSS 的 `#rule-content p` 中加入 `margin: 10px 0;` 進行微調。)*