代码: 全选
這是一個非常有創意的更新,尤其是加入 SVG SMIL 動畫來進行覆盤與計分演示,這讓下載出的棋盤圖具備了極高的觀賞性與實用價值。
以下是針對你提出的各項需求所做的代碼修復與替換。請按照指示找到對應的區塊並進行替換:
### 一、 多語言(I18N)擴充與修正
我們需要在 `I18N` 字典中補齊你提到的那些中英文文本。
**尋找** `const I18N = {` 區塊,並將其中的 `zh` 與 `en` 內容替換/擴充為以下內容:
```javascript
// --- 多語言字典 ---
const I18N = {
zh: {
title: '三菱棋',
sub: '一個抽象策略遊戲,在二維與三維之間',
ruleTitle: '規則簡介',
rule: '雙方輪流落子,先手方第一手必須下藍色棋子,後手方第一手必須下紅色棋子,此後雙方輪流行動,分數高者獲勝。',
startBtn: '開始遊戲',
firstBluePrompt: '先手方第一手必須下藍色棋子',
secondRedPrompt: '後手方第一手必須下紅色棋子',
yourTurn: '輪到我方落子',
oppTurn: '輪到對方落子',
autoTurn: '輪到我方落子',
noHole: '禁止產生空洞!',
mustConnect: '必須與現有棋子相連',
invalidDrop: '此處無法落子',
gameOver: '遊戲結束',
endGameBtn: '結束遊戲',
swapBtn: '雙方交換先手下第二局',
restartBtn: '重新開始',
round1Score: '第一局比分:',
round2Score: '第二局比分:',
totalScore: '總分:',
winnerMe: '我方獲勝!',
winnerOpp: '對方獲勝!',
draw: '平局!',
autoRotateON: '自動旋轉已開啟',
autoRotateOFF: '自動旋轉已關閉',
aiThinking: 'AI 思考中...',
aiAnalysisTitle: 'MCTS AI 分析與設定',
tabSimple: '簡單',
tabHard: '困難',
tabExpert: '專家',
tabCustom: '自訂',
thinkTime: '思考時間 (秒)',
mctsC: '探索常數C(越大隨機性越強)',
heuristicN: '智能預判步數 (局末介入)',
heuristicChecks: '預判檢查數 (局部洗牌)',
pureMinimaxN: '必殺算法 局末觸發步數',
runSearch: '執行盤面搜尋分析',
ready: '準備就緒',
exportTitle: '棋譜 導出 / 導入',
exportLabel: '導出當前棋譜:',
copyBtn: '複製',
importLabel: '導入棋譜:',
importBtn: '導入',
ptypeTitle: '選擇玩家類型',
typeHuman: '人類 (Human)',
typeSimple: '簡單 AI (Simple)',
typeHard: '困難 AI (Hard)',
typeExpert: '專家 AI (Expert)',
typeCustom: '自訂 AI (Custom)',
copySuccess: '已複製到剪貼簿!',
importSuccess: '導入成功!',
importFail: '導入失敗:格式錯誤或資料損毀',
debugOn: 'Debug 模式 ON',
debugOff: 'Debug 模式 OFF',
playerSwitched: '玩家 {0} 切換為: {1}',
gameNotStarted: '遊戲尚未開始或已結束!',
noValidMoves: '無合法著法',
gameStateTitle: '遊戲狀態',
gameStateTurn: '第 {0} 手棋,輪到{1}落子',
currentScore: '當前比分',
p1You: 'P1(我方)',
p2Opp: 'P2(對方)',
continueGame: '繼續遊戲',
me: '我方',
opp: '對方',
// AI狀態文字
minimaxSearch: 'Minimax 搜尋中...',
time: '時間',
depth: '深度',
nodes: '節點',
searchDone: '搜尋完成',
totalNodes: '總搜尋量',
bestResult: '最佳結果',
winRate: '當前勝率',
simCount: '次數',
p1Lead: 'P1領先',
p2Lead: 'P2領先',
tie: '平局',
// 下載選單文字
dlTitle: '下載棋盤 SVG',
dlGrid: '網格',
dlNumbers: '數字',
dlAnim: '動畫',
dlBtn: '下載'
},
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.',
startBtn: 'Start Game',
firstBluePrompt: 'The first player must place a BLUE piece.',
secondRedPrompt: 'The second player must place a RED piece.',
yourTurn: 'Your Turn',
oppTurn: "Opponent's Turn",
autoTurn: 'Your Turn',
noHole: 'Holes are not allowed!',
mustConnect: 'Must connect to existing pieces.',
invalidDrop: 'Cannot drop here.',
gameOver: 'Game Over',
endGameBtn: 'End Game',
swapBtn: 'Swap roles for Round 2',
restartBtn: 'Restart Game',
round1Score: 'Round 1 Score: ',
round2Score: 'Round 2 Score: ',
totalScore: 'Total Score: ',
winnerMe: 'You Win!',
winnerOpp: 'Opponent Wins!',
draw: 'Draw!',
autoRotateON: 'Auto-Rotate ON',
autoRotateOFF: 'Auto-Rotate OFF',
aiThinking: 'AI is thinking...',
aiAnalysisTitle: 'MCTS AI Analysis & Settings',
tabSimple: 'Simple',
tabHard: 'Hard',
tabExpert: 'Expert',
tabCustom: 'Custom',
thinkTime: 'Thinking Time (s)',
mctsC: 'Exploration Constant C',
heuristicN: 'Heuristic Steps (Endgame)',
heuristicChecks: 'Heuristic Checks (Shuffle)',
pureMinimaxN: 'Endgame Minimax Trigger',
runSearch: 'Run Board Search Analysis',
ready: 'Ready',
exportTitle: 'Export / Import Match',
exportLabel: 'Export current match:',
copyBtn: 'Copy',
importLabel: 'Import match:',
importBtn: 'Import',
ptypeTitle: 'Select Player Type',
typeHuman: 'Human',
typeSimple: 'Simple AI',
typeHard: 'Hard AI',
typeExpert: 'Expert AI',
typeCustom: 'Custom AI',
copySuccess: 'Copied to clipboard!',
importSuccess: 'Import successful!',
importFail: 'Import failed',
debugOn: 'Debug Mode ON',
debugOff: 'Debug Mode OFF',
playerSwitched: 'Player {0} switched to: {1}',
gameNotStarted: 'Game has not started or has ended!',
noValidMoves: 'No valid moves',
gameStateTitle: 'Game Status',
gameStateTurn: "Move {0}, {1}'s turn",
currentScore: 'Current Score',
p1You: 'P1 (You)',
p2Opp: 'P2 (Opp)',
continueGame: 'Continue Game',
me: 'Your',
opp: 'Opponent',
// AI狀態文字
minimaxSearch: 'Minimax Searching...',
time: 'Time',
depth: 'Depth',
nodes: 'Nodes',
searchDone: 'Search Done',
totalNodes: 'Total Nodes',
bestResult: 'Best Result',
winRate: 'Win Rate',
simCount: 'Sims',
p1Lead: 'P1 Leads',
p2Lead: 'P2 Leads',
tie: 'Tie',
// 下載選單文字
dlTitle: 'Download Board SVG',
dlGrid: 'Grid',
dlNumbers: 'Numbers',
dlAnim: 'Animation',
dlBtn: 'Download'
}
};
```
---
### 二、 修復狀態文字與 UI 邏輯
請找到 `showGameStatus()`,並修改其內部的標題指派,讓它支援多語言。
**替換為:**
```javascript
function showGameStatus() {
const screen = document.getElementById('game-over-screen');
const title = document.getElementById('go-title');
const scoresDiv = document.getElementById('go-scores');
const actionsDiv = document.getElementById('go-actions');
if (gameState === 'playing') {
let roundStart = currentRound === 2 ? 36 : 0;
let currentMove = historyIndex - roundStart + 2;
let turnText = currentPlayer === 1 ? getText('me') : getText('opp');
// 【修復】使用多語言翻譯並拼接
title.innerText = getText('gameStateTitle') + ' (Round ' + currentRound + ')';
scoresDiv.innerHTML = `${getText('gameStateTurn').replace('{0}', currentMove).replace('{1}', turnText)}<br><br>${getText('currentScore')}<br>${getText('p1You')}: ${p1CurrentScore} - ${getText('p2Opp')}: ${p2CurrentScore}`;
actionsDiv.innerHTML = `<button class="btn-primary" onclick="minimizeGameOver()">${getText('continueGame')}</button>`;
} else {
showRoundEnd();
return;
}
screen.style.display = 'flex';
}
```
接著修正 AI 思考時,按鈕不可點擊的 Bug。
**找到並替換以下三個函數(`forceHumanPlayer`, `openPlayerTypeMenu`, `toggleAutoRotate`):**
```javascript
function forceHumanPlayer() {
abortAiSearch = true;
isAiThinking = false; // 【修復】重置思考狀態,使棋子能再次被點擊
playerTypes[1] = 'human';
playerTypes[2] = 'human';
document.getElementById('ptype-1').innerHTML = aiConfigs['human'].icon;
document.getElementById('ptype-2').innerHTML = aiConfigs['human'].icon;
document.getElementById('ptype-1').style.color = '#0056b3';
document.getElementById('ptype-2').style.color = '#a70000';
setAiThinkingState(false);
updateUI(); // 【修復】觸發 UI 更新,使棋子恢復實體與點擊效果
}
function openPlayerTypeMenu(player) {
if (isAiThinking || isAutoRotate) return; // 【修復】AI思考時或自動旋轉狀態下不可點擊
settingPlayerTypeFor = player;
document.getElementById('ptype-modal').style.display = 'flex';
const typeKeys = ['human', 'simple', 'hard', 'expert', 'custom'];
const btnContainer = document.getElementById('ptype-btn-container');
btnContainer.innerHTML = '';
for (let type of typeKeys) {
let typeName = getText('type' + type.charAt(0).toUpperCase() + type.slice(1));
btnContainer.innerHTML += `
<button class="btn-secondary" onclick="setPlayerType('${type}')" style="margin: 0; padding: 10px; display: flex; align-items: center; justify-content: center; gap: 8px;">
<svg viewBox="0 0 24 24" style="width: 24px; height: 24px; fill: #333;">${aiConfigs[type].icon || ''}</svg>
${typeName}
</button>`;
}
}
function toggleAutoRotate() {
if (isAiThinking) return; // 【修復】AI思考時自動旋轉按鈕無效
isAutoRotate = !isAutoRotate;
if (isAutoRotate) {
forceHumanPlayer(); // 【修復】開啟旋轉狀態時,強制切換為人類
}
updateLayoutForTurn();
showNotification(isAutoRotate ? getText('autoRotateON') : getText('autoRotateOFF'));
const btns = document.querySelectorAll('#auto-rotate-btn');
btns.forEach(b => (isAutoRotate ? b.classList.add('active-tool') : b.classList.remove('active-tool')));
}
```
接著找到 `triggerAITurn`,並替換其內部被硬編碼的中文字串:
```javascript
if (totalPiecesLeft <= pureMinimaxN) {
let res = await runIterativeDeepening(
rootState,
currentPlayer,
timeLimit,
async (timeSpent, nodes, depth) => {
box.innerText = `${getText('minimaxSearch')}\n${getText('time')}: ${(timeSpent / 1000).toFixed(1)}s / ${(timeLimit / 1000).toFixed(0)}s\n${getText('depth')}: ${depth} | ${getText('nodes')}: ${nodes}`;
await new Promise(r => setTimeout(r, 0));
}
);
let finalTime = ((Date.now() - startTime) / 1000).toFixed(1);
if (res.bestMove !== null) {
let scoreText = res.score > 0 ? `${getText('p1Lead')} ${res.score}` : res.score < 0 ? `${getText('p2Lead')} ${-res.score}` : `${getText('tie')} 0`;
box.innerText = `${getText('searchDone')}: ${finalTime}s\n${getText('totalNodes')}: ${res.nodes}\n${getText('bestResult')}: ${scoreText}`;
// ...下方程式碼不變...
```
(同樣在 `triggerAITurn` 的 MCTS 迴圈處修改:)
```javascript
let now = Date.now();
if (now - lastUiTime > 100) {
box.innerText = `${getText('aiThinking')}\n${getText('time')}: ${((now - startTime) / 1000).toFixed(1)}s / ${(timeLimit / 1000).toFixed(0)}s\n${getText('simCount')}: ${iterations}`;
await new Promise(r => setTimeout(r, 0));
lastUiTime = now;
}
}
let bestChild = null, mostVisits = -1;
for (let c of root.children) {
if (c.visits > mostVisits) { mostVisits = c.visits; bestChild = c; }
}
let finalTime = ((Date.now() - startTime) / 1000).toFixed(1);
if (bestChild) {
let winRate = (((bestChild.wins / bestChild.visits + 1) / 2) * 100).toFixed(1);
box.innerText = `${getText('searchDone')}: ${finalTime}s\n${getText('simCount')}: ${iterations}\n${getText('winRate')}: ${winRate}%`;
// ...下方程式碼不變...
```
---
### 三、 添加下載專屬 UI Modal
**請在 `#game-container` 區塊外的尾部**(與 `#export-modal` 同層級,例如 `#export-modal` 後方)添加這個彈窗介面的 HTML:
```html
<div id="download-modal" class="fullscreen-modal" style="display: none; z-index: 300;">
<div class="modal-content" style="width: 90%; max-width: 600px; height: 90%; display: flex; flex-direction: column; align-items: center; padding: 20px; box-sizing: border-box;">
<svg class="close-btn" onclick="closeDownloadModal()" viewBox="0 0 24 24">
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
</svg>
<h3 id="dl-title" style="margin-top: 0;">下載棋盤 SVG</h3>
<div id="dl-preview-container" style="flex: 1; width: 100%; min-height: 0; display: flex; justify-content: center; align-items: center; border: 1px solid #ccc; border-radius: 8px; margin-bottom: 20px; overflow: hidden; background: #fff;">
</div>
<div style="display: flex; gap: 30px; margin-bottom: 20px; font-size: 1.1rem; font-weight: bold; flex-wrap: wrap; justify-content: center;">
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
<input type="checkbox" id="dl-opt-grid" checked onchange="updateDlPreview()"> <span id="dl-lbl-grid">網格</span>
</label>
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
<input type="checkbox" id="dl-opt-num" onchange="onDlOptChange('num')"> <span id="dl-lbl-num">數字</span>
</label>
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
<input type="checkbox" id="dl-opt-anim" onchange="onDlOptChange('anim')"> <span id="dl-lbl-anim">動畫</span>
</label>
</div>
<button class="btn-primary" id="dl-btn" onclick="confirmDownloadSVG()" style="margin:0;">下載</button>
<div id="dl-size" style="margin-top: 10px; color: #666; font-size: 0.9rem;"></div>
</div>
</div>
```
---
### 四、 SVG 動畫與下載邏輯
**找到原有的 `downloadBoardSVG()` 函數,並將它替換為以下完整的下載邏輯區塊:**
```javascript
// === 下載與 SVG 處理邏輯 ===
let currentDlSvgSource = "";
function downloadBoardSVG() {
document.getElementById('dl-title').innerText = getText('dlTitle');
document.getElementById('dl-lbl-grid').innerText = getText('dlGrid');
document.getElementById('dl-lbl-num').innerText = getText('dlNumbers');
document.getElementById('dl-lbl-anim').innerText = getText('dlAnim');
document.getElementById('dl-btn').innerText = getText('dlBtn');
document.getElementById('download-modal').style.display = 'flex';
updateDlPreview();
}
function closeDownloadModal() {
document.getElementById('download-modal').style.display = 'none';
}
function onDlOptChange(changedType) {
let numOpt = document.getElementById('dl-opt-num');
let animOpt = document.getElementById('dl-opt-anim');
if (changedType === 'num' && numOpt.checked) animOpt.checked = false;
if (changedType === 'anim' && animOpt.checked) numOpt.checked = false;
updateDlPreview();
}
function updateDlPreview() {
const originalSvg = document.getElementById('etani');
let svgClone = originalSvg.cloneNode(true);
svgClone.setAttribute('id', 'etani-preview');
svgClone.style.width = '100%';
svgClone.style.height = '100%';
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('在此填入你的字體URL') 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 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(${m.px}, ${m.py})`);
if (animOpt) {
let fromY = m.player === 1 ? m.py + 400 : m.py - 400;
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', `${m.px},${fromY}`);
animNode.setAttribute('to', `${m.px},${m.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', pieceCx + dx);
textNode.setAttribute('y', pieceCy + dy);
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) {
// 在 etdrop 建立兩個 Dummy animation 控制時間軸,避免陣列為空時時間軸斷裂
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 len = Math.hypot(m.px, m.py) || 1;
let outX = m.px + (m.px / len) * 400;
let outY = 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', `${m.px},${m.py}; ${outX},${outY}; ${outX},${outY}; ${outX},${outY}; ${m.px},${m.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', `${m.px},${m.py}; ${outX},${outY}; ${outX},${outY}; ${outX},${outY}; ${m.px},${m.py}`);
anim.setAttribute('dur', '4s');
anim.setAttribute('fill', 'freeze');
anim.setAttribute('begin', 'trhe.begin');
useElements[i].appendChild(anim);
}
}
// 添加分數文字動畫
let p1Text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
p1Text.setAttribute('id', 'trmetxt');
p1Text.setAttribute('x', '252'); // 視圖外距右邊界12 (240+12)
p1Text.setAttribute('y', '228'); // 視圖內距下邊界12 (240-12)
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; -144,0; -144,0; -144,0; 0,0');
p1Anim.setAttribute('dur', '4s');
p1Anim.setAttribute('fill', 'freeze');
p1Anim.setAttribute('begin', 'trme.begin');
p1Text.appendChild(p1Anim);
etboard.appendChild(p1Text);
let p2Text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
p2Text.setAttribute('id', 'trhetxt');
p2Text.setAttribute('x', '252');
p2Text.setAttribute('y', '228');
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; -144,0; -144,0; -144,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';
}
function confirmDownloadSVG() {
let numOpt = document.getElementById('dl-opt-num').checked;
let animOpt = document.getElementById('dl-opt-anim').checked;
let d = new Date();
let pad = (n) => n.toString().padStart(2, '0');
let dateStr = `${d.getFullYear()}${pad(d.getMonth()+1)}${pad(d.getDate())}_${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
let suffixStr = animOpt ? (currentLang === 'zh' ? '動畫' : 'animation') : (numOpt ? (currentLang === 'zh' ? '數字' : 'number') : '');
let filename = `trirhombus${suffixStr ? '_' + suffixStr : ''}_${dateStr}.svg`;
const a = document.createElement('a');
a.href = URL.createObjectURL(new Blob([currentDlSvgSource], { type: 'image/svg+xml;charset=utf-8' }));
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
```