0. 作品介紹
封面
1. AI 提示詞
把這段指令貼給 AI,讓它幫你生成程式碼。
1.我想要幫學生做一個球員卡
2.幫我用Google 試算表建立
3.我希望有些資料我可以直接在試算表更改首頁可以直接呈現
4.需求說明(姓名、傳球、接球、守備、行動力、決策力)(可以放學生照片)
5.要把它變成程式碼我要發佈
6.請完整敘述操作步驟 越詳細越好
7.作業環境
8.Google試算表
9.Google Apps Script
10.我看不懂程式碼,請給我後端gs完整程式碼、前端index完整程式碼
11.補充:我有使用雲端硬碟回收照片或PDF或文件
12.背景我希望要有未來感、科技感
13.雷達圖最高分數10分。
2.幫我用Google 試算表建立
3.我希望有些資料我可以直接在試算表更改首頁可以直接呈現
4.需求說明(姓名、傳球、接球、守備、行動力、決策力)(可以放學生照片)
5.要把它變成程式碼我要發佈
6.請完整敘述操作步驟 越詳細越好
7.作業環境
8.Google試算表
9.Google Apps Script
10.我看不懂程式碼,請給我後端gs完整程式碼、前端index完整程式碼
11.補充:我有使用雲端硬碟回收照片或PDF或文件
12.背景我希望要有未來感、科技感
13.雷達圖最高分數10分。
2. 資料表規則
請照下面規則建立分頁與欄位。
第1分頁: 分頁1
A 欄: 隊伍
B 欄: 姓名
C 欄: 守備位置
D 欄: 個人口號
E 欄: 照片ID
F 欄: 傳球
G 欄: 接球
H 欄: 守備
I 欄: 行動
J 欄: 決策
K 欄: 隊伍LOGO
L 欄: 合照
📄 解析結果(自動表格)
分頁:分頁1
| A 欄 | B 欄 | C 欄 | D 欄 | E 欄 | F 欄 | G 欄 | H 欄 | I 欄 | J 欄 | K 欄 | L 欄 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 隊伍 | 姓名 | 守備位置 | 個人口號 | 照片ID | 傳球 | 接球 | 守備 | 行動 | 決策 | 隊伍LOGO | 合照 |
3. 開啟 Apps Script
從試算表開 Apps Script,才能直接用 SpreadsheetApp 讀寫資料。
- 試算表上方選單:擴充功能 → Apps Script
- 刪掉預設的程式碼(或全部選取覆蓋)
-
保留/建立檔案:
- Code.gs
- index.html
4. 貼上後端(Code.gs)
把後端程式貼到 Apps Script 的 Code.gs。
function doGet(e) {
return HtmlService.createHtmlOutputFromFile('index')
.setTitle('第一屆枋寮世界盃 - 球員卡系統')
.addMetaTag('viewport', 'width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}
function getPlayerData() {
try {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName('球員資料') || ss.getSheets()[0];
const data = sheet.getDataRange().getValues();
const players = [];
const parseScore = (val) => {
let n = parseFloat(val);
return isNaN(n) ? 0 : Math.min(10, Math.max(0, n));
};
for (let i = 1; i < data.length; i++) {
let row = data[i];
if (!row[1]) continue;
const scores = [parseScore(row[5]), parseScore(row[6]), parseScore(row[7]), parseScore(row[8]), parseScore(row[9])];
const total = scores.reduce((a, b) => a + b, 0);
let rank, tier;
if (total >= 40) { rank = 'S'; tier = 'BLACK_GOLD'; }
else if (total >= 30) { rank = 'A'; tier = 'PLATINUM'; }
else if (total >= 20) { rank = 'B'; tier = 'GOLD_CARD'; }
else { rank = 'C'; tier = 'NORMAL'; }
players.push({
team: (row[0] || '未知').toString(),
name: row[1].toString(),
position: (row[2] || '選手').toString(),
slogan: (row[3] || '').toString(),
photo: formatDriveUrl(row[4], 'player'),
pass: scores[0],
catch: scores[1],
field: scores[2],
mobility: scores[3],
decision: scores[4],
teamLogo: formatDriveUrl(row[10], 'logo'),
teamPhoto: formatDriveUrl(row[11], 'team'),
rank: rank,
tier: tier
});
}
return players;
} catch (e) {
return { error: e.message };
}
}
function formatDriveUrl(url, type) {
const defaults = {
logo: 'https://via.placeholder.com/200x200?text=LOGO',
player: 'https://via.placeholder.com/400x500?text=NO+PHOTO',
team: 'https://via.placeholder.com/800x400?text=TEAM+PHOTO'
};
const defaultImg = defaults[type] || defaults.player;
if (!url || typeof url !== 'string' || !url.includes('drive.google.com')) return defaultImg;
const match = url.match(/[-\w]{25,}/);
return match ? 'https://drive.google.com/thumbnail?id=' + match[0] + '&sz=w1000' : defaultImg;
} 貼到 Apps Script 的 Code.gs 檔案
5. 貼上前端(index.html)
把前端程式貼到 Apps Script 的 index.html。
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@400;700;900&family=Orbitron:wght@500;900&display=swap');
:root {
--neon-blue: #00f3ff;
--neon-purple: #bc13fe;
--dark-bg: #03060e;
}
body, html { height: 100vh; width: 100vw; margin: 0; padding: 0; background: var(--dark-bg); color: #fff; font-family: 'Noto Sans TC', sans-serif; overflow: hidden; }
.bg-overlay { position: fixed; inset: 0; z-index: -1; background: radial-gradient(circle at center, rgba(3,6,14,0.7) 0%, #03060e 100%); }
.layer { display: none; height: 100vh; width: 100vw; padding: 20px; box-sizing: border-box; position: absolute; flex-direction: column; align-items: center; }
.layer.active { display: flex; }
.scroll-content { width: 100%; max-width: 1200px; flex: 1; overflow-y: auto; padding: 10px 0 20px 0; }
.grid-box { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; }
#particles-canvas { position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 0; pointer-events: none; }
@keyframes scan { 0% { top: -10%; } 100% { top: 110%; } }
.scan-line { position: fixed; left: 0; width: 100%; height: 2px; background: linear-gradient(90deg, transparent, var(--neon-blue), transparent); opacity: 0.25; z-index: 1; pointer-events: none; animation: scan 6s linear infinite; }
@keyframes flicker { 0%,100% { opacity: 1; } 92% { opacity: 1; } 93% { opacity: 0.4; } 95% { opacity: 1; } 97% { opacity: 0.6; } 98% { opacity: 1; } }
@keyframes title-glow {
0%,100% { text-shadow: 0 0 20px var(--neon-blue), 0 0 40px var(--neon-blue); }
50% { text-shadow: 0 0 40px var(--neon-blue), 0 0 80px var(--neon-blue), 0 0 120px var(--neon-blue); }
}
.layer1-title { font-family: 'Orbitron'; font-size: clamp(2.5rem,8vh,6.5rem); text-align: center; margin-top: 0; animation: flicker 5s infinite, title-glow 3s ease-in-out infinite; position: relative; z-index: 2; }
@keyframes typing { from { width: 0; } to { width: 26ch; } }
@keyframes blink { 50% { border-color: transparent; } }
@keyframes hide-cursor { to { border-color: transparent; } }
.layer1-sub {
font-family: 'Orbitron'; font-size: 0.85rem; letter-spacing: 6px;
color: var(--neon-blue); opacity: 0.7; margin-top: 12px;
overflow: hidden; white-space: nowrap; width: 0;
border-right: 2px solid var(--neon-blue);
animation: typing 2.5s steps(30) 0.5s forwards, blink 0.8s step-end 0.5s 4, hide-cursor 0s 4s forwards;
position: relative; z-index: 2;
}
@keyframes pulse-border {
0%,100% { box-shadow: 0 0 15px var(--neon-blue), 0 0 30px rgba(0,243,255,0.3); }
50% { box-shadow: 0 0 30px var(--neon-blue), 0 0 60px rgba(0,243,255,0.5), 0 0 90px rgba(0,243,255,0.2); }
}
.enter-btn {
margin-top: 5vh; padding: 18px 60px; border: 2px solid var(--neon-blue);
background: rgba(0,243,255,0.05); color: var(--neon-blue); border-radius: 50px; cursor: pointer;
font-family: 'Orbitron'; font-size: 1.6rem; font-weight: bold;
animation: pulse-border 2s ease-in-out infinite;
transition: background 0.3s, transform 0.2s; position: relative; z-index: 2;
}
.enter-btn:hover { background: rgba(0,243,255,0.15); transform: scale(1.05); }
@keyframes data-scroll { 0% { transform: translateX(100%); } 100% { transform: translateX(-100%); } }
.status-bar { position: fixed; bottom: 0; left: 0; right: 0; height: 28px; background: rgba(0,243,255,0.05); border-top: 1px solid rgba(0,243,255,0.2); overflow: hidden; z-index: 2; display: flex; align-items: center; }
.status-text { white-space: nowrap; font-family: 'Orbitron'; font-size: 0.7rem; color: var(--neon-blue); opacity: 0.6; letter-spacing: 3px; animation: data-scroll 18s linear infinite; }
@keyframes gold-flow { 0% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; } }
.card {
position: relative; border-radius: 15px; padding: 3px;
background: linear-gradient(135deg, #b8860b, #ffd700, #fffacd, #d4af37, #b8860b);
background-size: 300% 300%; cursor: pointer; transition: transform 0.3s, box-shadow 0.3s;
box-shadow: 0 4px 20px rgba(212,175,55,0.3);
}
.card:hover { transform: translateY(-6px); animation: gold-flow 2s ease infinite; box-shadow: 0 8px 40px rgba(255,215,0,0.7); }
.card-inner { background: #0d0d0d; border-radius: 13px; padding: 15px; text-align: center; }
.card-inner img { width: 100%; height: 220px; object-fit: contain; border-radius: 8px; }
.player-card {
background: #111; border: 3px solid #f5c400; border-radius: 16px;
cursor: pointer; transition: transform 0.3s, box-shadow 0.3s;
overflow: hidden; display: flex; flex-direction: column;
box-shadow: 0 0 18px rgba(245,196,0,0.3);
}
.player-card:hover { transform: translateY(-6px); box-shadow: 0 0 30px rgba(245,196,0,0.6); }
.player-badges { display: flex; flex-wrap: wrap; gap: 4px; padding: 6px 8px 8px; min-height: 0; }
.badge-tag { font-size: 0.6rem; font-weight: 900; letter-spacing: 1px; padding: 3px 7px; border-radius: 20px; border: 1px solid currentColor; white-space: nowrap; animation: badge-glow 2s ease-in-out infinite; }
@keyframes badge-glow { 0%,100% { box-shadow: 0 0 4px currentColor; } 50% { box-shadow: 0 0 10px currentColor, 0 0 20px currentColor; } }
.badge-pass { color: #00e5ff; background: rgba(0,229,255,0.1); }
.badge-catch { color: #69ff47; background: rgba(105,255,71,0.1); }
.badge-field { color: #ff9100; background: rgba(255,145,0,0.1); }
.badge-mobility { color: #ffeb3b; background: rgba(255,235,59,0.1); }
.badge-decision { color: #e040fb; background: rgba(224,64,251,0.1); }
.badge-allstar { color: #ffd700; background: rgba(255,215,0,0.15); }
.player-card-badge { background: #f5c400; color: #000; font-weight: 900; font-size: 0.9rem; padding: 8px 12px; text-align: center; font-family: 'Noto Sans TC', sans-serif; letter-spacing: 1px; flex-shrink: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.player-card-photo { width: 100%; height: 280px; object-fit: cover; object-position: top center; display: block; flex-shrink: 0; background: #1a1a2e; }
.player-card-no-photo { width: 100%; height: 280px; display: flex; flex-direction: column; align-items: center; justify-content: center; background: linear-gradient(135deg, #0d0d1a, #1a1a2e); flex-shrink: 0; gap: 8px; }
.player-card-no-photo .no-photo-icon { font-size: 3.5rem; opacity: 0.25; }
.player-card-no-photo .no-photo-label { font-family: 'Orbitron'; font-size: 0.6rem; letter-spacing: 3px; color: rgba(0,243,255,0.25); }
.search-wrap { width: 100%; max-width: 1200px; flex-shrink: 0; margin: 0 0 12px 0; position: relative; }
.search-wrap input { width: 100%; box-sizing: border-box; background: rgba(0,243,255,0.05); border: 1.5px solid rgba(0,243,255,0.4); border-radius: 50px; padding: 10px 20px 10px 44px; color: #fff; font-family: 'Orbitron', sans-serif; font-size: 0.95rem; outline: none; transition: border-color 0.3s, box-shadow 0.3s; }
.search-wrap input::placeholder { color: rgba(0,243,255,0.4); }
.search-wrap input:focus { border-color: var(--neon-blue); box-shadow: 0 0 12px rgba(0,243,255,0.3); }
.search-icon { position: absolute; left: 16px; top: 50%; transform: translateY(-50%); color: rgba(0,243,255,0.5); font-size: 1rem; pointer-events: none; }
.search-empty { text-align: center; font-family: 'Orbitron'; color: rgba(0,243,255,0.4); font-size: 0.9rem; letter-spacing: 3px; margin-top: 40px; }
.team-overview { width: 100%; max-width: 1200px; flex-shrink: 0; background: rgba(0,243,255,0.04); border: 1px solid rgba(0,243,255,0.2); border-radius: 14px; padding: 14px 20px; margin-bottom: 12px; box-sizing: border-box; }
.team-overview-title { font-family: 'Orbitron'; font-size: 0.75rem; letter-spacing: 4px; color: var(--neon-blue); opacity: 0.7; margin-bottom: 10px; }
.overview-bars { display: grid; grid-template-columns: repeat(5, 1fr); gap: 10px; }
.bar-item { display: flex; flex-direction: column; gap: 5px; }
.bar-label { font-family: 'Orbitron'; font-size: 0.65rem; letter-spacing: 1px; color: rgba(0,243,255,0.7); text-align: center; }
.bar-track { background: rgba(255,255,255,0.08); border-radius: 50px; height: 8px; overflow: hidden; }
@keyframes bar-fill { from { width: 0%; } to { width: var(--bar-w); } }
.bar-fill { height: 100%; border-radius: 50px; background: linear-gradient(90deg, var(--neon-blue), var(--neon-purple)); width: 0%; animation: bar-fill 1s ease forwards; box-shadow: 0 0 6px var(--neon-blue); }
.bar-val { font-family: 'Orbitron'; font-size: 0.7rem; color: #fff; text-align: center; opacity: 0.85; }
.topps-card { position: relative; width: 420px; flex-shrink: 0; border-radius: 18px; overflow: hidden; box-shadow: 0 30px 80px rgba(0,0,0,0.9); border: 3px solid rgba(255,255,255,0.15); }
.topps-BLACK_GOLD { border-color: #d4af37; box-shadow: 0 30px 80px rgba(212,175,55,0.4); }
.topps-PLATINUM { border-color: #e0e0e0; box-shadow: 0 30px 80px rgba(220,220,220,0.4); }
.topps-GOLD_CARD { border-color: #ffd700; box-shadow: 0 30px 80px rgba(255,215,0,0.4); }
.topps-NORMAL { border-color: var(--neon-blue); box-shadow: 0 30px 80px rgba(0,243,255,0.3); }
.topps-photo { width: 100%; aspect-ratio: 3/4; object-fit: cover; object-position: top; display: block; }
.topps-info { position: absolute; bottom: 0; left: 0; right: 0; padding: 60px 20px 20px; background: linear-gradient(to top, rgba(0,0,0,0.95) 60%, transparent 100%); }
.topps-name-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; }
.topps-name { font-family: 'Orbitron', 'Noto Sans TC', sans-serif; font-size: clamp(1.4rem,3vw,2rem); font-weight: 900; color: #fff; text-shadow: 0 2px 8px rgba(0,0,0,0.8); letter-spacing: 1px; }
.topps-rank-badge { font-family: 'Orbitron'; font-size: 2.2rem; font-weight: 900; padding: 4px 14px; border-radius: 8px; line-height: 1; }
.rank-BLACK_GOLD { background: #d4af37; color: #000; }
.rank-PLATINUM { background: #e0e0e0; color: #000; }
.rank-GOLD_CARD { background: #ffd700; color: #000; }
.rank-NORMAL { background: var(--neon-blue); color: #000; }
.topps-divider { height: 3px; border-radius: 2px; margin: 8px 0; }
.div-BLACK_GOLD { background: linear-gradient(90deg, #d4af37, transparent); }
.div-PLATINUM { background: linear-gradient(90deg, #e0e0e0, transparent); }
.div-GOLD_CARD { background: linear-gradient(90deg, #ffd700, transparent); }
.div-NORMAL { background: linear-gradient(90deg, var(--neon-blue), transparent); }
.topps-pos { font-size: 1rem; font-weight: 900; letter-spacing: 2px; text-transform: uppercase; opacity: 0.85; margin-bottom: 4px; }
.topps-slogan { font-size: 0.9rem; font-style: italic; opacity: 0.7; margin-top: 4px; }
.topps-radar-panel { width: 360px; flex-shrink: 0; display: flex; flex-direction: column; justify-content: center; padding: 10px 0; }
.radar-title { font-family: 'Orbitron'; font-size: 1.3rem; letter-spacing: 5px; font-weight: 900; opacity: 1; text-align: center; margin-bottom: 16px; color: var(--neon-blue); text-shadow: 0 0 12px var(--neon-blue); }
#radarChart-wrap { width: 100%; height: 360px; position: relative; }
#layer4 { justify-content: flex-start; }
.topps-layout { width: 100%; flex: 1; overflow-y: auto; display: flex; justify-content: center; align-items: center; gap: 40px; padding: 0 30px 30px; box-sizing: border-box; flex-wrap: wrap; }
.btn-back { flex-shrink: 0; align-self: flex-start; background: transparent; color: var(--neon-purple); border: 2px solid var(--neon-purple); padding: 10px 30px; border-radius: 50px; cursor: pointer; margin-bottom: 12px; font-family: 'Orbitron'; font-weight: bold; transition: 0.3s; }
.btn-back:hover { background: var(--neon-purple); color: #fff; }
#loading { position: fixed; inset: 0; background: #000; z-index: 9999; display: flex; align-items: center; justify-content: center; font-family: 'Orbitron'; color: var(--neon-blue); font-size: 1.5rem; letter-spacing: 5px; }
#error-msg { display: none; position: fixed; inset: 0; background: #000; z-index: 9999; flex-direction: column; align-items: center; justify-content: center; font-family: 'Orbitron'; color: #ff4444; font-size: 1.2rem; text-align: center; padding: 20px; gap: 20px; }
#error-msg button { padding: 12px 40px; border: 2px solid #ff4444; background: transparent; color: #ff4444; border-radius: 50px; cursor: pointer; font-family: 'Orbitron'; font-size: 1rem; }
#error-msg button:hover { background: #ff4444; color: #fff; }
.live-ticker { position: fixed; bottom: 28px; left: 0; right: 0; height: 32px; background: rgba(188,19,254,0.08); border-top: 1px solid rgba(188,19,254,0.3); border-bottom: 1px solid rgba(188,19,254,0.3); overflow: hidden; z-index: 3; display: none; align-items: center; }
.live-ticker.active { display: flex; }
.ticker-label { flex-shrink: 0; background: var(--neon-purple); color: #fff; font-family: 'Orbitron'; font-size: 0.65rem; font-weight: 900; letter-spacing: 2px; padding: 0 12px; height: 100%; display: flex; align-items: center; }
@keyframes ticker-scroll { 0% { transform: translateX(100vw); } 100% { transform: translateX(-100%); } }
.ticker-text { white-space: nowrap; font-family: 'Orbitron'; font-size: 0.7rem; color: #e0a0ff; letter-spacing: 2px; animation: ticker-scroll 28s linear infinite; }
.vs-btn { position: fixed; bottom: 68px; right: 24px; z-index: 10; background: linear-gradient(135deg, var(--neon-purple), #6600cc); border: none; color: #fff; font-family: 'Orbitron'; font-weight: 900; font-size: 0.85rem; letter-spacing: 2px; padding: 12px 20px; border-radius: 50px; cursor: pointer; box-shadow: 0 0 20px rgba(188,19,254,0.6); transition: transform 0.2s, box-shadow 0.2s; display: none; }
.vs-btn.active { display: block; }
.vs-btn:hover { transform: scale(1.08); box-shadow: 0 0 35px rgba(188,19,254,0.9); }
.player-card.vs-selected { border-color: var(--neon-purple) !important; box-shadow: 0 0 25px rgba(188,19,254,0.8) !important; }
.player-card.vs-selected .player-card-badge { background: var(--neon-purple); color: #fff; }
.vs-overlay { position: fixed; inset: 0; z-index: 2000; display: none; align-items: center; justify-content: center; background: #000; overflow: hidden; }
.vs-overlay.active { display: flex; }
.vs-stage { position: relative; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; }
.vs-card-wrap { position: absolute; display: flex; flex-direction: column; align-items: center; gap: 10px; z-index: 5; }
.vs-card-wrap.card-a { left: -260px; }
.vs-card-wrap.card-b { right: -260px; }
@keyframes slide-in-a { 0% { left: -260px; } 60% { left: calc(50% - 160px); } 75% { left: calc(50% - 148px); } 100% { left: calc(50% - 155px); } }
@keyframes slide-in-b { 0% { right: -260px; } 60% { right: calc(50% - 160px); } 75% { right: calc(50% - 148px); } 100% { right: calc(50% - 155px); } }
.vs-card-wrap.card-a.sliding { animation: slide-in-a 0.9s cubic-bezier(0.22,1,0.36,1) forwards; }
.vs-card-wrap.card-b.sliding { animation: slide-in-b 0.9s cubic-bezier(0.22,1,0.36,1) forwards; }
.vs-card { width: 130px; height: 172px; border-radius: 12px; overflow: hidden; position: relative; border: 3px solid #ffd700; box-shadow: 0 0 30px rgba(255,215,0,0.5); }
.vs-card img { width: 100%; height: 100%; object-fit: cover; object-position: top; display: block; }
.vs-card-name { font-family: 'Noto Sans TC'; font-weight: 900; font-size: 0.95rem; color: #fff; text-shadow: 0 0 8px rgba(255,255,255,0.5); text-align: center; }
.vs-card-rank { font-family: 'Orbitron'; font-size: 0.7rem; letter-spacing: 2px; color: rgba(255,255,255,0.5); text-align: center; }
.vs-explosion { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 0; height: 0; pointer-events: none; z-index: 10; }
.vs-spark { position: absolute; width: 6px; height: 6px; border-radius: 50%; transform-origin: center; }
@keyframes spark-fly { 0% { transform: translate(-50%,-50%) rotate(var(--angle)) translateX(0) scale(1); opacity: 1; } 100% { transform: translate(-50%,-50%) rotate(var(--angle)) translateX(var(--dist)) scale(0); opacity: 0; } }
.vs-flash { position: absolute; inset: 0; background: #fff; opacity: 0; pointer-events: none; z-index: 8; }
@keyframes flash-bang { 0% { opacity: 0; } 10% { opacity: 1; } 100% { opacity: 0; } }
.vs-flash.bang { animation: flash-bang 0.5s ease forwards; }
@keyframes vs-shake { 0%,100% { transform: translate(0,0); } 20% { transform: translate(-8px, 4px); } 40% { transform: translate(8px, -4px); } 60% { transform: translate(-6px, 6px); } 80% { transform: translate(6px, -2px); } }
.vs-stage.shaking { animation: vs-shake 0.4s ease; }
.vs-result { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 20px; opacity: 0; pointer-events: none; z-index: 20; background: rgba(0,0,0,0.88); }
@keyframes result-in { from { opacity: 0; transform: scale(0.85); } to { opacity: 1; transform: scale(1); } }
.vs-result.show { opacity: 1; pointer-events: all; animation: result-in 0.5s cubic-bezier(0.22,1,0.36,1) forwards; }
.vs-result-title { font-family: 'Orbitron'; font-size: 0.8rem; letter-spacing: 6px; color: var(--neon-purple); opacity: 0.8; }
.vs-result-fighters { display: flex; align-items: flex-end; justify-content: center; gap: 16px; flex-wrap: wrap; }
.vs-result-fighter { display: flex; flex-direction: column; align-items: center; gap: 8px; min-width: 110px; max-width: 150px; }
.vs-result-fighter img { width: 100px; height: 132px; object-fit: cover; object-position: top; border-radius: 12px; border: 3px solid #555; }
.vs-result-fighter.winner img { border-color: #ffd700; box-shadow: 0 0 30px rgba(255,215,0,0.8); }
.vs-result-fighter.loser img { filter: grayscale(60%); opacity: 0.6; }
.vs-result-fighter-name { font-family: 'Noto Sans TC'; font-weight: 900; font-size: 1rem; }
.vs-result-fighter.winner .vs-result-fighter-name { color: #ffd700; text-shadow: 0 0 12px #ffd700; }
.vs-result-fighter.loser .vs-result-fighter-name { color: rgba(255,255,255,0.4); }
.vs-winner-pct { font-family: 'Orbitron'; font-size: 2.6rem; font-weight: 900; color: #ffd700; text-shadow: 0 0 24px #ffd700; }
.vs-loser-pct { font-family: 'Orbitron'; font-size: 1.6rem; font-weight: 900; color: rgba(255,255,255,0.35); }
.vs-result-crown { font-size: 1.8rem; }
.vs-vs-mid { font-family: 'Orbitron'; font-size: 1.6rem; font-weight: 900; color: var(--neon-purple); text-shadow: 0 0 16px var(--neon-purple); align-self: center; }
.vs-bar-wrap { width: min(380px, 90vw); }
.vs-bar-track { background: rgba(255,255,255,0.08); border-radius: 50px; height: 12px; overflow: hidden; }
@keyframes vs-fill-a { from { width: 0%; } to { width: var(--pct-a); } }
@keyframes vs-fill-b { from { width: 0%; } to { width: var(--pct-b); } }
.vs-bar-a { height: 100%; border-radius: 50px 0 0 50px; background: linear-gradient(90deg, #ffd700, #ffaa00); animation: vs-fill-a 1s ease forwards; width: 0%; float: left; }
.vs-bar-b { height: 100%; border-radius: 0 50px 50px 0; background: linear-gradient(90deg, #bc13fe, #6600cc); animation: vs-fill-b 1s ease forwards; width: 0%; float: right; }
.vs-detail { display: grid; grid-template-columns: 1fr auto 1fr; gap: 5px 12px; font-size: 0.85rem; width: min(340px,90vw); }
.vs-detail-a { text-align: right; color: #ffd700; font-weight: 700; }
.vs-detail-label { text-align: center; color: rgba(255,255,255,0.4); font-family: 'Orbitron'; font-size: 0.65rem; letter-spacing: 1px; }
.vs-detail-b { text-align: left; color: #e0a0ff; font-weight: 700; }
.vs-close { background: transparent; border: 2px solid var(--neon-purple); color: var(--neon-purple); font-family: 'Orbitron'; font-size: 0.9rem; padding: 10px 40px; border-radius: 50px; cursor: pointer; transition: 0.2s; }
.vs-close:hover { background: var(--neon-purple); color: #fff; }
.vs-hint { position: fixed; bottom: 110px; right: 24px; font-family: 'Orbitron'; font-size: 0.65rem; letter-spacing: 2px; color: rgba(188,19,254,0.7); display: none; z-index: 10; }
.vs-hint.active { display: block; }
.hall-section { margin-bottom: 32px; }
.hall-section-title { font-family: 'Orbitron'; font-size: 1rem; letter-spacing: 4px; font-weight: 900; margin-bottom: 16px; padding-bottom: 8px; border-bottom: 2px solid rgba(255,255,255,0.1); display: flex; align-items: center; gap: 10px; }
.hall-podium { display: flex; align-items: flex-end; justify-content: center; gap: 16px; }
.hall-player { display: flex; flex-direction: column; align-items: center; gap: 8px; cursor: pointer; transition: transform 0.25s; }
.hall-player:hover { transform: translateY(-6px); }
.hall-player-photo { object-fit: cover; object-position: top center; border-radius: 12px; display: block; border: 3px solid transparent; }
.hall-rank-1 .hall-player-photo { width: 110px; height: 145px; border-color: #ffd700; box-shadow: 0 0 20px rgba(255,215,0,0.6); }
.hall-rank-2 .hall-player-photo { width: 88px; height: 116px; border-color: #c0c0c0; box-shadow: 0 0 14px rgba(192,192,192,0.4); }
.hall-rank-3 .hall-player-photo { width: 76px; height: 100px; border-color: #cd7f32; box-shadow: 0 0 10px rgba(205,127,50,0.4); }
.hall-medal { font-size: 1.6rem; line-height: 1; }
.hall-rank-2 .hall-medal, .hall-rank-3 .hall-medal { font-size: 1.2rem; }
.hall-player-name { font-family: 'Noto Sans TC'; font-weight: 900; font-size: 0.85rem; text-align: center; }
.hall-player-team { font-size: 0.7rem; opacity: 0.5; font-family: 'Orbitron'; letter-spacing: 1px; }
.hall-player-score { font-family: 'Orbitron'; font-weight: 900; font-size: 1.1rem; padding: 2px 12px; border-radius: 20px; background: rgba(255,255,255,0.08); }
.hall-rank-1 .hall-player-score { color: #ffd700; }
.hall-rank-2 .hall-player-score { color: #c0c0c0; }
.hall-rank-3 .hall-player-score { color: #cd7f32; }
.pack-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.95); z-index: 2000; display: none; align-items: center; justify-content: center; flex-direction: column; gap: 20px; }
.pack-overlay.active { display: flex; }
@keyframes pack-shake { 0%,100% { transform: rotate(0deg) scale(1); } 10% { transform: rotate(-4deg) scale(1.05); } 20% { transform: rotate(4deg) scale(1.08); } 30% { transform: rotate(-6deg) scale(1.1); } 40% { transform: rotate(6deg) scale(1.12); } 50% { transform: rotate(-4deg) scale(1.1); } 60% { transform: rotate(4deg) scale(1.05); } 70% { transform: rotate(-2deg) scale(1); } }
@keyframes pack-explode { 0% { transform: scale(1); opacity: 1; } 100% { transform: scale(3); opacity: 0; } }
.pack-card-scene { perspective: 900px; width: 280px; height: 370px; }
.pack-card-flip { width: 100%; height: 100%; position: relative; transform-style: preserve-3d; transition: transform 0.75s cubic-bezier(0.4,0,0.2,1); }
.pack-card-flip.flipped { transform: rotateY(180deg); }
.pack-card-back, .pack-card-face { position: absolute; inset: 0; border-radius: 16px; overflow: hidden; backface-visibility: hidden; -webkit-backface-visibility: hidden; }
.pack-card-face { transform: rotateY(180deg); }
.pack-card-back svg { width: 100%; height: 100%; display: block; }
@keyframes card-reveal { 0% { opacity: 0; transform: scale(0.9); } 100% { opacity: 1; transform: scale(1); } }
.pack-icon { font-size: 6rem; cursor: pointer; user-select: none; filter: drop-shadow(0 0 30px #ffd700); }
.pack-icon.shaking { animation: pack-shake 0.8s ease forwards; }
.pack-icon.exploding { animation: pack-explode 0.4s ease forwards; }
.pack-hint { font-family: 'Orbitron'; font-size: 0.85rem; letter-spacing: 4px; color: rgba(255,215,0,0.7); }
.pack-card-wrap { display: none; flex-direction: column; align-items: center; gap: 16px; animation: card-reveal 0.4s ease forwards; }
.pack-card-wrap.show { display: flex; }
@keyframes holo-sweep { 0% { background-position: -200% center; } 100% { background-position: 200% center; } }
.pack-holo-overlay { position: absolute; inset: 0; border-radius: 16px; pointer-events: none; background-size: 200% 100%; animation: holo-sweep 2s ease-in-out infinite; }
.pack-card-img { width: 280px; height: 370px; object-fit: cover; object-position: top; border-radius: 16px; border: 3px solid #ffd700; box-shadow: 0 0 40px rgba(255,215,0,0.6); display: block; }
.pack-card-name { font-family: 'Orbitron'; font-size: 1.2rem; font-weight: 900; color: #ffd700; text-shadow: 0 0 12px #ffd700; }
.pack-card-info { font-size: 0.85rem; opacity: 0.7; letter-spacing: 2px; }
.pack-again-btn { padding: 12px 40px; background: linear-gradient(135deg, #d4af37, #ffd700); color: #000; border: none; border-radius: 50px; cursor: pointer; font-family: 'Orbitron'; font-weight: 900; font-size: 0.9rem; transition: 0.2s; }
.pack-again-btn:hover { transform: scale(1.06); }
.pack-close-btn { padding: 10px 30px; background: transparent; color: rgba(255,255,255,0.5); border: 1px solid rgba(255,255,255,0.2); border-radius: 50px; cursor: pointer; font-family: 'Orbitron'; font-size: 0.8rem; transition: 0.2s; }
.pack-close-btn:hover { color: #fff; border-color: #fff; }
/* ══ 戰術板 ══ */
#layer6 { justify-content: flex-start; overflow: hidden; }
.tactical-container { width: 100%; max-width: 1400px; flex: 1; display: flex; gap: 12px; overflow: hidden; box-sizing: border-box; padding-bottom: 10px; }
.tactical-sidebar { width: 190px; flex-shrink: 0; display: flex; flex-direction: column; gap: 8px; overflow-y: auto; }
.tactical-sidebar-section { background: rgba(255,180,0,0.04); border: 1px solid rgba(255,180,0,0.25); border-radius: 10px; padding: 10px; }
.tactical-sidebar-title { font-family: 'Orbitron'; font-size: 0.6rem; letter-spacing: 3px; color: #ffb300; opacity: 0.9; margin-bottom: 8px; }
.tac-tool-btn { width: 100%; padding: 7px 10px; margin-bottom: 5px; border-radius: 8px; background: rgba(255,180,0,0.06); border: 1px solid rgba(255,180,0,0.25); color: #fff; font-family: 'Noto Sans TC'; font-size: 0.75rem; cursor: pointer; transition: all 0.2s; text-align: left; }
.tac-tool-btn:hover, .tac-tool-btn.active { background: rgba(255,180,0,0.22); border-color: #ffb300; box-shadow: 0 0 10px rgba(255,180,0,0.35); }
.tac-color-row { display: flex; gap: 6px; flex-wrap: wrap; }
.tac-color-btn { width: 26px; height: 26px; border-radius: 50%; cursor: pointer; border: 2px solid transparent; transition: all 0.2s; }
.tac-color-btn.active { border-color: #fff; transform: scale(1.25); box-shadow: 0 0 8px currentColor; }
.tac-size-row { display: flex; gap: 6px; }
.tac-size-btn { flex: 1; padding: 5px 0; border-radius: 20px; cursor: pointer; border: 1px solid rgba(255,180,0,0.3); background: rgba(255,180,0,0.06); color: #fff; font-family: 'Orbitron'; font-size: 0.6rem; transition: 0.2s; text-align:center; }
.tac-size-btn.active { background: rgba(255,180,0,0.25); border-color: #ffb300; }
.tac-shape-btn { width: 100%; padding: 7px 10px; margin-bottom: 5px; border-radius: 8px; background: rgba(0,180,80,0.06); border: 1px solid rgba(0,180,80,0.3); color: #fff; font-family: 'Noto Sans TC'; font-size: 0.75rem; cursor: pointer; transition: all 0.2s; text-align: left; }
.tac-shape-btn:hover, .tac-shape-btn.active { background: rgba(0,220,100,0.2); border-color: #00e064; }
.tactical-board-wrap { flex: 1; position: relative; border-radius: 14px; overflow: hidden; border: 2px solid rgba(255,180,0,0.4); box-shadow: 0 0 30px rgba(255,180,0,0.12); }
#tactical-canvas { display: block; width: 100%; height: 100%; cursor: crosshair; }
.tac-action-btn { padding: 8px 14px; border-radius: 8px; cursor: pointer; font-family: 'Orbitron'; font-size: 0.62rem; letter-spacing: 1px; transition: 0.2s; border: 1px solid; }
/* ══ 計分板 ══ */
/* ★ 關鍵修改:layer7 改為 overflow-y: auto,讓整頁可滾動 */
#layer7 { justify-content: flex-start; overflow-y: auto; }
.bb-wrap {
width: 100%; max-width: 1200px;
display: grid;
grid-template-columns: 280px 1fr 260px;
gap: 10px;
box-sizing: border-box;
padding-bottom: 30px;
}
.bb-top-card {
grid-column: 1 / 4;
background: linear-gradient(135deg,rgba(255,60,120,0.06),rgba(0,243,255,0.04));
border: 2px solid rgba(255,60,120,0.3); border-radius: 16px; padding: 12px 20px;
}
.bb-inning-bar { display:flex; align-items:center; justify-content:center; gap:7px; margin-bottom:10px; flex-wrap:wrap; }
.bb-inning-label { font-family:'Orbitron'; font-size:0.62rem; letter-spacing:2px; color:rgba(255,255,255,0.35); }
.bb-inning-btn { padding:4px 12px; border-radius:20px; border:1px solid rgba(255,60,120,0.3); background:rgba(255,60,120,0.04); color:rgba(255,255,255,0.4); font-family:'Orbitron'; font-size:0.62rem; cursor:pointer; transition:0.2s; }
.bb-inning-btn.active { background:rgba(255,60,120,0.2); border-color:#ff3c78; color:#ff3c78; box-shadow:0 0 8px rgba(255,60,120,0.4); }
.bb-score-row { display:grid; grid-template-columns:1fr auto 1fr; gap:10px; align-items:center; }
.bb-team-block { display:flex; flex-direction:column; align-items:center; gap:6px; }
.bb-team-name-input { background:transparent; border:none; border-bottom:2px solid rgba(255,60,120,0.3); color:#fff; font-family:'Orbitron'; font-size:0.9rem; font-weight:900; text-align:center; letter-spacing:3px; outline:none; width:100%; padding:3px 0; transition:0.2s; }
.bb-team-name-input:focus { border-color:#ff3c78; }
.bb-big-score { font-family:'Orbitron'; font-size:clamp(2.5rem,4.5vw,4rem); font-weight:900; line-height:1; text-align:center; min-width:100px; }
.bb-team-home .bb-big-score { color:#00f3ff; text-shadow:0 0 25px #00f3ff; }
.bb-team-away .bb-big-score { color:#ffb300; text-shadow:0 0 25px #ffb300; }
.bb-manual-btns { display:flex; gap:5px; }
.bb-manual-btn { padding:5px 10px; border-radius:50px; border:1.5px solid; background:transparent; color:#fff; font-family:'Orbitron'; font-weight:900; font-size:0.72rem; cursor:pointer; transition:0.2s; }
.bb-team-home .bb-manual-btn { border-color:rgba(0,243,255,0.4); }
.bb-team-home .bb-manual-btn:hover { background:rgba(0,243,255,0.15); }
.bb-team-away .bb-manual-btn { border-color:rgba(255,180,0,0.4); }
.bb-team-away .bb-manual-btn:hover { background:rgba(255,180,0,0.15); }
.bb-vs-col { text-align:center; display:flex; flex-direction:column; align-items:center; gap:7px; }
.bb-vs-text { font-family:'Orbitron'; font-size:1.1rem; font-weight:900; color:rgba(255,255,255,0.08); }
.bb-bso-wrap { display:flex; gap:10px; justify-content:center; }
.bb-bso-col { display:flex; flex-direction:column; align-items:center; gap:3px; }
.bb-bso-label { font-family:'Orbitron'; font-size:0.52rem; letter-spacing:2px; color:rgba(255,255,255,0.35); }
.bb-bso-lights { display:flex; gap:4px; }
.bb-light { width:15px; height:15px; border-radius:50%; border:2px solid rgba(255,255,255,0.15); background:rgba(255,255,255,0.05); transition:all 0.2s; }
.bb-light.on-strike { background:#ff4444; border-color:#ff4444; box-shadow:0 0 8px #ff4444; }
.bb-light.on-out { background:#ffd700; border-color:#ffd700; box-shadow:0 0 8px #ffd700; }
.bb-bases-wrap { position:relative; width:76px; height:76px; }
.bb-base { position:absolute; width:18px; height:18px; border:2px solid rgba(255,255,255,0.25); background:rgba(255,255,255,0.05); transform:rotate(45deg); transition:all 0.22s; }
.bb-base.on { background:#ffd700; border-color:#ffd700; box-shadow:0 0 12px #ffd700; }
#base-2nd { top:2px; left:28px; }
#base-3rd { top:28px; left:2px; }
#base-1st { top:28px; left:54px; }
#base-home{ top:54px; left:28px; background:rgba(255,255,255,0.12); border-color:rgba(255,255,255,0.35); }
.bb-side-label { font-family:'Orbitron'; font-size:0.55rem; letter-spacing:2px; color:rgba(255,255,255,0.3); }
/* 左欄:打擊順序 — ★ 移除 overflow:hidden,讓清單完整展開 */
.bb-lineup-col {
grid-column:1; grid-row:2;
background:rgba(0,0,0,0.3); border:1px solid rgba(255,255,255,0.07);
border-radius:14px; padding:12px; display:flex; flex-direction:column; gap:8px;
}
.bb-col-title { font-family:'Orbitron'; font-size:0.62rem; letter-spacing:3px; color:rgba(255,255,255,0.3); flex-shrink:0; }
.bb-lineup-tabs { display:flex; gap:5px; flex-shrink:0; }
.bb-lineup-tab { flex:1; padding:6px; border-radius:20px; border:1px solid rgba(255,255,255,0.12); background:rgba(255,255,255,0.03); color:rgba(255,255,255,0.35); font-family:'Orbitron'; font-size:0.62rem; cursor:pointer; transition:0.2s; text-align:center; }
.bb-lineup-tab.active { background:rgba(255,60,120,0.18); border-color:#ff3c78; color:#ff3c78; }
/* ★ 隊伍選擇放大 */
.bb-team-sel { width:100%; background:rgba(0,0,0,0.5); border:1px solid rgba(255,60,120,0.3); border-radius:10px; color:#fff; font-family:'Noto Sans TC'; font-size:1rem; padding:10px 12px; outline:none; cursor:pointer; flex-shrink:0; }
/* ★ 打擊順序清單不再有獨立滾軸 */
.bb-lineup-list { display:flex; flex-direction:column; gap:3px; }
.bb-lineup-row { display:flex; align-items:center; gap:6px; padding:8px 10px; background:rgba(255,255,255,0.03); border-radius:7px; border:1.5px solid transparent; cursor:pointer; transition:all 0.18s; }
.bb-lineup-row:hover { background:rgba(255,60,120,0.07); border-color:rgba(255,60,120,0.25); }
.bb-lineup-row.batting-now { background:rgba(255,60,120,0.14); border-color:#ff3c78; box-shadow:0 0 8px rgba(255,60,120,0.25); }
.bb-lineup-num { font-family:'Orbitron'; font-size:0.68rem; color:rgba(255,255,255,0.28); width:18px; text-align:center; flex-shrink:0; }
.bb-lineup-name { flex:1; font-size:0.88rem; font-weight:700; }
.bb-lineup-arrow { font-size:0.85rem; flex-shrink:0; opacity:0; }
.bb-lineup-row.batting-now .bb-lineup-arrow { opacity:1; }
.bb-advance-btn { width:100%; padding:8px; border-radius:8px; border:1px solid rgba(255,60,120,0.25); background:rgba(255,60,120,0.06); color:#ff3c78; font-family:'Orbitron'; font-size:0.6rem; cursor:pointer; transition:0.2s; flex-shrink:0; }
.bb-advance-btn:hover { background:rgba(255,60,120,0.16); }
/* 中欄 — ★ 移除 overflow:hidden */
.bb-center-col { grid-column:2; grid-row:2; display:flex; flex-direction:column; gap:8px; }
.bb-action-panel { background:rgba(255,60,120,0.04); border:1px solid rgba(255,60,120,0.25); border-radius:14px; padding:11px; flex-shrink:0; }
.bb-batter-label { font-family:'Orbitron'; font-size:0.6rem; letter-spacing:2px; color:rgba(255,255,255,0.4); margin-bottom:8px; }
.bb-batter-label span { color:#ffd700; font-weight:900; font-size:0.85rem; letter-spacing:1px; }
.bb-act-group-lbl { font-family:'Orbitron'; font-size:0.48rem; letter-spacing:2px; color:rgba(255,255,255,0.22); margin-bottom:4px; }
.bb-act-row { display:flex; gap:5px; margin-bottom:7px; flex-wrap:wrap; }
.bb-act-btn { flex:1; min-width:48px; padding:8px 4px; border-radius:9px; border:1.5px solid; background:transparent; color:#fff; font-family:'Noto Sans TC'; font-weight:900; font-size:0.8rem; cursor:pointer; transition:all 0.15s; text-align:center; white-space:nowrap; }
.bb-act-btn:active { transform:scale(0.9); }
.act-1b { border-color:rgba(0,232,122,0.5); color:#00e87a; } .act-1b:hover { background:rgba(0,232,122,0.13); }
.act-2b { border-color:rgba(0,243,255,0.5); color:#00f3ff; } .act-2b:hover { background:rgba(0,243,255,0.13); }
.act-3b { border-color:rgba(188,19,254,0.5); color:#bc13fe; } .act-3b:hover { background:rgba(188,19,254,0.13); }
.act-hr { border-color:rgba(255,215,0,0.6); color:#ffd700; background:rgba(255,215,0,0.04); } .act-hr:hover { background:rgba(255,215,0,0.18); box-shadow:0 0 12px rgba(255,215,0,0.35); }
.act-bb { border-color:rgba(255,180,0,0.45); color:#ffb300; } .act-bb:hover { background:rgba(255,180,0,0.13); }
.act-e { border-color:rgba(188,19,254,0.4); color:#e060ff; } .act-e:hover { background:rgba(188,19,254,0.1); }
.act-k { border-color:rgba(255,68,68,0.5); color:#ff6666; } .act-k:hover { background:rgba(255,68,68,0.13); }
.act-go { border-color:rgba(255,68,68,0.4); color:#ff9090; } .act-go:hover { background:rgba(255,68,68,0.1); }
.act-fo { border-color:rgba(255,68,68,0.4); color:#ffa0a0; } .act-fo:hover { background:rgba(255,68,68,0.1); }
.act-dp { border-color:rgba(255,30,30,0.65); color:#ff3333; font-size:0.7rem; } .act-dp:hover { background:rgba(255,30,30,0.16); }
.bb-act-divider { font-family:'Orbitron'; font-size:0.45rem; letter-spacing:3px; color:rgba(255,255,255,0.15); text-align:center; margin:0 0 5px; }
/* ★ 各局得分表格 — 移除 flex:1 和 min-height:0 */
.bb-inning-table-card { background:rgba(0,0,0,0.28); border:1px solid rgba(255,255,255,0.07); border-radius:14px; padding:10px; overflow-x:auto; }
.bb-table-title { font-family:'Orbitron'; font-size:0.6rem; letter-spacing:4px; color:rgba(255,255,255,0.3); margin-bottom:8px; }
.bb-table { width:100%; border-collapse:collapse; font-family:'Orbitron'; font-size:0.75rem; min-width:400px; }
.bb-table th { padding:5px; text-align:center; color:rgba(255,255,255,0.3); font-size:0.58rem; letter-spacing:1px; border-bottom:1px solid rgba(255,255,255,0.08); }
.bb-table td { padding:5px; text-align:center; border-bottom:1px solid rgba(255,255,255,0.04); }
.bb-table td.team-cell { text-align:left; font-size:0.7rem; font-family:'Noto Sans TC'; padding-left:7px; }
.bb-table td.rhe-cell { font-weight:900; font-size:0.88rem; }
.bb-table td.home-rhe { color:#00f3ff; }
.bb-table td.away-rhe { color:#ffb300; }
.bb-inn-cell { cursor:pointer; border-radius:5px; transition:0.15s; min-width:24px; }
.bb-inn-cell:hover { background:rgba(255,255,255,0.07); }
.bb-inn-cell.cur { background:rgba(255,60,120,0.14); color:#ff3c78; }
/* 右欄 — ★ 移除 overflow:hidden */
.bb-log-col { grid-column:3; grid-row:2; background:rgba(0,0,0,0.28); border:1px solid rgba(255,255,255,0.07); border-radius:14px; padding:10px; display:flex; flex-direction:column; gap:7px; }
/* ★ 比賽紀錄清單不再有獨立滾軸 */
.bb-log-list { display:flex; flex-direction:column; gap:3px; }
.bb-log-item { display:flex; align-items:flex-start; gap:7px; padding:7px 9px; background:rgba(255,255,255,0.02); border-radius:7px; border-left:3px solid; flex-shrink:0; animation:bb-log-in 0.2s ease; }
@keyframes bb-log-in { from{opacity:0;transform:translateX(5px)} to{opacity:1;transform:translateX(0)} }
.log-hit { border-color:#00f3ff; }
.log-score { border-color:#ffd700; }
.log-hr { border-color:#ffd700; background:rgba(255,215,0,0.03); }
.log-out { border-color:#ff4444; }
.log-sys { border-color:rgba(255,255,255,0.12); }
.bb-log-inn { font-family:'Orbitron'; font-size:0.52rem; color:rgba(255,255,255,0.25); white-space:nowrap; padding-top:2px; min-width:20px; }
.bb-log-body { flex:1; }
.bb-log-player { font-weight:700; font-size:0.82rem; margin-bottom:2px; }
.bb-log-tag { display:inline-block; padding:2px 7px; border-radius:20px; font-size:0.66rem; font-weight:700; }
.tag-hit { background:rgba(0,243,255,0.1); color:#00f3ff; border:1px solid rgba(0,243,255,0.22); }
.tag-score { background:rgba(255,215,0,0.1); color:#ffd700; border:1px solid rgba(255,215,0,0.28); }
.tag-hr { background:rgba(255,215,0,0.16); color:#ffd700; border:1px solid rgba(255,215,0,0.4); }
.tag-out { background:rgba(255,68,68,0.1); color:#ff7070; border:1px solid rgba(255,68,68,0.25); }
.tag-sys { background:rgba(255,255,255,0.04); color:rgba(255,255,255,0.35); border:1px solid rgba(255,255,255,0.1); font-size:0.6rem; }
.bb-out-dots { display:flex; gap:3px; margin-top:2px; }
.bb-out-dot { width:7px; height:7px; border-radius:50%; border:1.5px solid rgba(255,215,0,0.25); }
.bb-out-dot.on { background:#ffd700; border-color:#ffd700; box-shadow:0 0 4px #ffd700; }
/* Banners */
@keyframes bb-score-pop { 0%{transform:scale(1)} 35%{transform:scale(1.38)} 100%{transform:scale(1)} }
.bb-big-score.popping { animation:bb-score-pop 0.35s cubic-bezier(0.22,1,0.36,1); }
@keyframes bb-banner-in { 0%{opacity:0;transform:translate(-50%,-50%) scale(0.7)} 12%{opacity:1;transform:translate(-50%,-50%) scale(1)} 80%{opacity:1} 100%{opacity:0;transform:translate(-50%,-50%) scale(1)} }
.bb-banner { position:fixed; top:48%; left:50%; transform:translate(-50%,-50%); pointer-events:none; z-index:9998; display:none; text-align:center; white-space:nowrap; font-family:'Orbitron'; font-weight:900; }
.bb-banner.show { display:block; animation:bb-banner-in 1.6s ease forwards; }
.bb-banner-out { font-size:1.7rem; color:#ff4444; text-shadow:0 0 20px #ff4444; }
.bb-banner-hr { font-size:2.2rem; color:#ffd700; text-shadow:0 0 40px #ffd700,0 0 80px #ffd700; }
.bb-banner-side { font-size:1.4rem; color:#ffb300; text-shadow:0 0 16px #ffb300; background:rgba(0,0,0,0.88); padding:12px 36px; border-radius:12px; border:2px solid rgba(255,180,0,0.4); }
.bb-banner-score { font-size:1.2rem; color:#00f3ff; text-shadow:0 0 18px #00f3ff; }
/* ══ 競賽規則 ══ */
#layer8 { justify-content: flex-start; }
.rules-wrap { width: 100%; max-width: 900px; flex: 1; overflow-y: auto; padding-bottom: 30px; display: flex; flex-direction: column; gap: 14px; }
.rules-section { background: rgba(0,255,180,0.03); border: 1px solid rgba(0,255,180,0.2); border-radius: 14px; padding: 18px 22px; transition: border-color 0.2s; }
.rules-section:hover { border-color: rgba(0,255,180,0.45); }
.rules-section-title { font-family: 'Orbitron'; font-size: 0.85rem; font-weight: 900; letter-spacing: 3px; color: #00ffb3; margin-bottom: 14px; display: flex; align-items: center; gap: 10px; text-shadow: 0 0 10px rgba(0,255,180,0.5); }
.rules-section-title .rule-icon { font-size: 1.3rem; }
.rules-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 10px; }
.rule-card { background: rgba(0,0,0,0.35); border-radius: 10px; padding: 12px 14px; border-left: 3px solid rgba(0,255,180,0.4); }
.rule-card-label { font-family: 'Orbitron'; font-size: 0.6rem; letter-spacing: 2px; color: #00ffb3; opacity: 0.7; margin-bottom: 5px; }
.rule-card-value { font-size: 1.05rem; font-weight: 900; color: #fff; line-height: 1.3; }
.rule-card-sub { font-size: 0.75rem; color: rgba(255,255,255,0.5); margin-top: 4px; line-height: 1.5; }
.rules-list { display: flex; flex-direction: column; gap: 7px; }
.rule-item { display: flex; gap: 10px; align-items: flex-start; padding: 8px 12px; background: rgba(0,0,0,0.25); border-radius: 8px; font-size: 0.88rem; line-height: 1.6; color: rgba(255,255,255,0.85); }
.rule-item .ri-num { font-family: 'Orbitron'; font-size: 0.65rem; color: #00ffb3; background: rgba(0,255,180,0.1); border-radius: 50%; width: 22px; height: 22px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; margin-top: 1px; }
.rule-item.highlight { border-left: 3px solid #ffd700; background: rgba(255,215,0,0.06); color: #fff; font-weight: 700; }
.rule-item.highlight .ri-num { color: #ffd700; background: rgba(255,215,0,0.15); }
.rule-tag { display: inline-block; padding: 2px 8px; border-radius: 20px; font-family: 'Orbitron'; font-size: 0.6rem; letter-spacing: 1px; border: 1px solid; margin: 2px 3px 2px 0; }
.tag-out-r { color: #ff4444; border-color: rgba(255,68,68,0.4); background: rgba(255,68,68,0.08); }
.tag-safe { color: #00ff88; border-color: rgba(0,255,136,0.4); background: rgba(0,255,136,0.08); }
.tag-score-r{ color: #ffd700; border-color: rgba(255,215,0,0.4); background: rgba(255,215,0,0.08); }
.tag-foul { color: #ff9500; border-color: rgba(255,149,0,0.4); background: rgba(255,149,0,0.08); }
.tag-rule { color: #00f3ff; border-color: rgba(0,243,255,0.4); background: rgba(0,243,255,0.08); }
/* ══ 手機響應式 ══ */
@media (max-width: 768px) {
.grid-box { grid-template-columns: repeat(2, 1fr); gap: 12px; }
.bb-wrap { grid-template-columns: 1fr; }
.bb-top-card { grid-column: 1; }
.bb-lineup-col { grid-column: 1; grid-row: auto; }
.bb-center-col { grid-column: 1; grid-row: auto; }
.bb-log-col { grid-column: 1; grid-row: auto; }
.tactical-container { flex-direction: column; }
.tactical-sidebar { width: 100%; flex-direction: row; flex-wrap: wrap; overflow-y: visible; }
.topps-layout { flex-direction: column; align-items: center; }
.topps-card { width: 100%; max-width: 360px; }
.topps-radar-panel { width: 100%; max-width: 360px; }
.overview-bars { grid-template-columns: repeat(3, 1fr); }
}
@media (max-width: 480px) {
.grid-box { grid-template-columns: 1fr; gap: 10px; }
.overview-bars { grid-template-columns: repeat(2, 1fr); }
.enter-btn { font-size: 1.2rem; padding: 14px 40px; }
}
</style>
</head>
<body>
<div class="bg-overlay"></div>
<canvas id="particles-canvas"></canvas>
<div class="scan-line" id="scan-line"></div>
<div class="status-bar" id="status-bar">
<div class="status-text">◈ SYSTEM ONLINE ◈ DATABASE CONNECTED ◈ PLAYERS LOADED ◈ FANG LIAO WORLD CUP SEASON 1 ◈ ALL TEAMS READY ◈ INITIALIZING MATCH DATA ◈ SYSTEM ONLINE ◈ DATABASE CONNECTED ◈ PLAYERS LOADED ◈ FANG LIAO WORLD CUP ◈</div>
</div>
<div class="live-ticker" id="live-ticker">
<div class="ticker-label">⚡ LIVE</div>
<div class="ticker-text" id="ticker-text"></div>
</div>
<div class="vs-hint" id="vs-hint"></div>
<button class="vs-btn" id="vs-btn" onclick="toggleVsMode()">⚔ VERSUS</button>
<!-- VS 對撞舞台 -->
<div class="vs-overlay" id="vs-overlay">
<div class="vs-stage" id="vs-stage">
<div class="vs-flash" id="vs-flash"></div>
<div class="vs-explosion" id="vs-explosion"></div>
<div class="vs-card-wrap card-a" id="vs-card-a">
<div class="vs-card" id="vs-card-img-a"><img id="vs-img-a" src="" alt=""></div>
<div class="vs-card-name" id="vs-name-a"></div>
<div class="vs-card-rank" id="vs-rank-a"></div>
</div>
<div class="vs-card-wrap card-b" id="vs-card-b">
<div class="vs-card" id="vs-card-img-b"><img id="vs-img-b" src="" alt=""></div>
<div class="vs-card-name" id="vs-name-b"></div>
<div class="vs-card-rank" id="vs-rank-b"></div>
</div>
<div class="vs-result" id="vs-result">
<div class="vs-result-title">◈ BATTLE RESULT ◈</div>
<div class="vs-result-fighters">
<div class="vs-result-fighter" id="vs-rf-a">
<div class="vs-result-crown" id="vs-crown-a"></div>
<img id="vs-rimg-a" src="" alt="">
<div class="vs-result-fighter-name" id="vs-rname-a"></div>
<div id="vs-rpct-a"></div>
</div>
<div class="vs-vs-mid">VS</div>
<div class="vs-result-fighter" id="vs-rf-b">
<div class="vs-result-crown" id="vs-crown-b"></div>
<img id="vs-rimg-b" src="" alt="">
<div class="vs-result-fighter-name" id="vs-rname-b"></div>
<div id="vs-rpct-b"></div>
</div>
</div>
<div class="vs-bar-wrap"><div class="vs-bar-track" id="vs-bar-track"></div></div>
<div class="vs-detail" id="vs-detail"></div>
<button class="vs-close" onclick="closeVersus()">關閉</button>
</div>
</div>
</div>
<!-- 抽卡包彈窗 -->
<div class="pack-overlay" id="pack-overlay" onclick="event.stopPropagation()">
<div id="pack-icon-wrap">
<div class="pack-icon" id="pack-icon" onclick="openPack()">🎴</div>
<div class="pack-hint">點擊卡包抽出球員</div>
</div>
<div class="pack-card-wrap" id="pack-card-wrap">
<div class="pack-card-scene">
<div class="pack-card-flip" id="pack-card-flip">
<div class="pack-card-back">
<svg viewBox="0 0 220 290" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="bgGrad" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" style="stop-color:#0a0800"/><stop offset="50%" style="stop-color:#1a1200"/><stop offset="100%" style="stop-color:#080600"/></linearGradient>
<linearGradient id="goldGrad" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" style="stop-color:#b8860b"/><stop offset="40%" style="stop-color:#ffd700"/><stop offset="60%" style="stop-color:#fffacd"/><stop offset="100%" style="stop-color:#b8860b"/></linearGradient>
<linearGradient id="centerGrad" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" style="stop-color:#1a1200"/><stop offset="100%" style="stop-color:#2a1e00"/></linearGradient>
</defs>
<rect width="220" height="290" rx="14" fill="url(#bgGrad)"/>
<rect x="6" y="6" width="208" height="278" rx="11" fill="none" stroke="url(#goldGrad)" stroke-width="2.5"/>
<rect x="11" y="11" width="198" height="268" rx="9" fill="none" stroke="#b8860b" stroke-width="0.8" opacity="0.5"/>
<g fill="url(#goldGrad)">
<polygon points="6,6 28,6 6,28" opacity="0.9"/><rect x="6" y="6" width="16" height="2.5" rx="1"/><rect x="6" y="6" width="2.5" height="16" rx="1"/><circle cx="20" cy="20" r="3.5" fill="none" stroke="url(#goldGrad)" stroke-width="1.5"/>
<polygon points="214,6 192,6 214,28" opacity="0.9"/><rect x="198" y="6" width="16" height="2.5" rx="1"/><rect x="211.5" y="6" width="2.5" height="16" rx="1"/><circle cx="200" cy="20" r="3.5" fill="none" stroke="url(#goldGrad)" stroke-width="1.5"/>
<polygon points="6,284 28,284 6,262" opacity="0.9"/><rect x="6" y="281.5" width="16" height="2.5" rx="1"/><rect x="6" y="268" width="2.5" height="16" rx="1"/><circle cx="20" cy="270" r="3.5" fill="none" stroke="url(#goldGrad)" stroke-width="1.5"/>
<polygon points="214,284 192,284 214,262" opacity="0.9"/><rect x="198" y="281.5" width="16" height="2.5" rx="1"/><rect x="211.5" y="268" width="2.5" height="16" rx="1"/><circle cx="200" cy="270" r="3.5" fill="none" stroke="url(#goldGrad)" stroke-width="1.5"/>
</g>
<circle cx="110" cy="145" r="72" fill="url(#centerGrad)" stroke="url(#goldGrad)" stroke-width="2"/>
<circle cx="110" cy="145" r="64" fill="none" stroke="#b8860b" stroke-width="0.8" opacity="0.6"/>
<circle cx="110" cy="145" r="55" fill="none" stroke="url(#goldGrad)" stroke-width="1.2" opacity="0.4"/>
<g stroke="url(#goldGrad)" stroke-width="1" fill="none" opacity="0.7">
<line x1="110" y1="73" x2="110" y2="87"/><line x1="110" y1="203" x2="110" y2="217"/>
<line x1="38" y1="145" x2="52" y2="145"/><line x1="168" y1="145" x2="182" y2="145"/>
<line x1="59" y1="94" x2="69" y2="104"/><line x1="151" y1="94" x2="161" y2="104"/>
<line x1="59" y1="196" x2="69" y2="186"/><line x1="151" y1="196" x2="161" y2="186"/>
</g>
<g transform="translate(110,145)">
<polygon points="0,-38 22,-15 0,10 -22,-15" fill="url(#goldGrad)" opacity="0.9"/>
<polygon points="0,-28 16,-10 0,5 -16,-10" fill="#0a0800"/>
<polygon points="0,-20 10,-6 0,6 -10,-6" fill="url(#goldGrad)" opacity="0.7"/>
<path d="M-18,18 Q0,38 18,18 Q8,28 0,26 Q-8,28 -18,18Z" fill="url(#goldGrad)" opacity="0.8"/>
<path d="M-10,14 Q0,22 10,14" fill="none" stroke="url(#goldGrad)" stroke-width="1.5"/>
<circle cx="-30" cy="0" r="3" fill="url(#goldGrad)"/><circle cx="30" cy="0" r="3" fill="url(#goldGrad)"/>
<circle cx="0" cy="-42" r="3" fill="url(#goldGrad)"/><circle cx="0" cy="16" r="2" fill="url(#goldGrad)"/>
</g>
<text x="110" y="42" text-anchor="middle" font-family="Orbitron,sans-serif" font-size="8" fill="url(#goldGrad)" letter-spacing="4" opacity="0.8">FANG LIAO</text>
<line x1="40" y1="47" x2="180" y2="47" stroke="url(#goldGrad)" stroke-width="0.8" opacity="0.4"/>
<line x1="40" y1="243" x2="180" y2="243" stroke="url(#goldGrad)" stroke-width="0.8" opacity="0.4"/>
<text x="110" y="257" text-anchor="middle" font-family="Orbitron,sans-serif" font-size="8" fill="url(#goldGrad)" letter-spacing="4" opacity="0.8">WORLD CUP</text>
<text x="110" y="271" text-anchor="middle" font-family="Orbitron,sans-serif" font-size="6.5" fill="#b8860b" letter-spacing="3" opacity="0.6">SEASON I</text>
</svg>
</div>
<div class="pack-card-face" style="position:relative;">
<img class="pack-card-img" id="pack-card-img" src="" alt="">
</div>
</div>
</div>
<div class="pack-card-name" id="pack-card-name"></div>
<div class="pack-card-info" id="pack-card-info"></div>
<div style="display:flex;gap:12px;margin-top:4px;">
<button class="pack-again-btn" onclick="resetPack()">🎴 再抽一張</button>
<button class="pack-close-btn" onclick="document.getElementById('pack-overlay').classList.remove('active')">關閉</button>
</div>
</div>
</div>
<!-- 文字輸入浮層 -->
<div id="tac-text-input-wrap" style="display:none;position:fixed;inset:0;z-index:500;align-items:center;justify-content:center;background:rgba(0,0,0,0.6);">
<div style="background:#1a1a2e;border:2px solid #ffb300;border-radius:14px;padding:20px 24px;display:flex;flex-direction:column;gap:12px;min-width:280px;">
<div style="font-family:'Orbitron';font-size:0.75rem;letter-spacing:3px;color:#ffb300;">輸入文字</div>
<input id="tac-text-input" type="text" placeholder="輸入內容..." maxlength="20"
style="background:rgba(255,180,0,0.06);border:1px solid rgba(255,180,0,0.4);border-radius:8px;padding:8px 12px;color:#fff;font-family:'Noto Sans TC';font-size:1rem;outline:none;"
onkeydown="if(event.key==='Enter') confirmFloatingText()">
<div style="display:flex;gap:8px;">
<button onclick="confirmFloatingText()" style="flex:1;padding:8px;border-radius:8px;border:none;background:#ffb300;color:#000;font-family:'Orbitron';font-weight:900;font-size:0.8rem;cursor:pointer;">確認 → 點擊放置</button>
<button onclick="document.getElementById('tac-text-input-wrap').style.display='none';tacFloatingText=null;" style="padding:8px 14px;border-radius:8px;border:1px solid rgba(255,255,255,0.2);background:transparent;color:rgba(255,255,255,0.5);font-size:0.8rem;cursor:pointer;">取消</button>
</div>
</div>
</div>
<div id="loading">INITIALIZING DATABASE...</div>
<div id="error-msg">
<div id="error-text">載入失敗</div>
<button onclick="location.reload()">重新載入</button>
</div>
<!-- Layer 1:主頁 -->
<div id="layer1" class="layer" style="justify-content:center; gap:24px;">
<h1 class="layer1-title">第一屆<br>枋寮世界盃</h1>
<div class="layer1-sub">FANG LIAO WORLD CUP · SEASON 1</div>
<div style="display:flex; flex-direction:column; gap:16px; align-items:center; margin-top:8px;">
<button class="enter-btn" onclick="showLayer(2)">ENTER DATABASE</button>
<!-- 第一排:賽程表 + 名人堂 + 抽卡包 -->
<div style="display:flex; gap:16px; flex-wrap:wrap; justify-content:center;">
<button class="enter-btn" onclick="showLayer(9)" style="font-size:1.1rem; padding:12px 36px; background:rgba(0,220,255,0.05); border-color:#00dcff; color:#00dcff; animation:none; box-shadow:0 0 20px rgba(0,220,255,0.3);">📅 賽程表</button>
<button class="enter-btn" onclick="showLayer(5)" style="font-size:1.1rem; padding:12px 36px; background:rgba(255,215,0,0.05); border-color:#ffd700; color:#ffd700; animation:none; box-shadow:0 0 20px rgba(255,215,0,0.3);">🏆 名人堂</button>
<button class="enter-btn" onclick="showPackOverlay()" style="font-size:1.1rem; padding:12px 36px; background:rgba(255,150,0,0.05); border-color:#ff9500; color:#ff9500; animation:none; box-shadow:0 0 20px rgba(255,149,0,0.3);">🎴 抽卡包</button>
</div>
<!-- 第二排:戰術板 + 計分板 + 競賽規則 -->
<div style="display:flex; gap:16px; flex-wrap:wrap; justify-content:center;">
<button class="enter-btn" onclick="showLayer(6)" style="font-size:1.1rem; padding:12px 36px; background:rgba(255,180,0,0.05); border-color:#ffb300; color:#ffb300; animation:none; box-shadow:0 0 20px rgba(255,180,0,0.3);">⚾ 戰術板</button>
<button class="enter-btn" onclick="showLayer(7)" style="font-size:1.1rem; padding:12px 36px; background:rgba(255,60,120,0.05); border-color:#ff3c78; color:#ff3c78; animation:none; box-shadow:0 0 20px rgba(255,60,120,0.3);">🏅 計分板</button>
<button class="enter-btn" onclick="showLayer(8)" style="font-size:1.1rem; padding:12px 36px; background:rgba(0,255,180,0.05); border-color:#00ffb3; color:#00ffb3; animation:none; box-shadow:0 0 20px rgba(0,255,180,0.3);">📖 競賽規則</button>
</div>
</div>
</div>
<!-- Layer 9:賽程表 -->
<div id="layer9" class="layer" style="justify-content:flex-start;">
<div style="width:100%; max-width:1000px; display:flex; align-items:center; justify-content:space-between; flex-shrink:0; margin-bottom:12px;">
<button class="btn-back" onclick="showLayer(1)" style="margin:0;">◀ HOME</button>
<h2 style="font-family:'Orbitron'; color:#00dcff; margin:0; font-size:1.1rem; letter-spacing:4px; text-shadow:0 0 12px #00dcff;">📅 賽程表</h2>
<div style="width:80px;"></div>
</div>
<div style="width:100%; max-width:1000px; flex:1; overflow-y:auto; padding-bottom:30px;">
<!-- 新增賽事按鈕 -->
<div style="display:flex; gap:10px; margin-bottom:16px; flex-wrap:wrap; align-items:center;">
<button onclick="openAddMatch()" style="padding:8px 20px; border-radius:20px; border:1px solid rgba(0,220,255,0.5); background:rgba(0,220,255,0.08); color:#00dcff; font-family:'Orbitron'; font-size:0.7rem; cursor:pointer; transition:0.2s;" onmouseover="this.style.background='rgba(0,220,255,0.2)'" onmouseout="this.style.background='rgba(0,220,255,0.08)'">+ 新增賽事</button>
<select id="schedule-filter" onchange="renderSchedule()" style="background:rgba(255,255,255,0.06); border:1px solid rgba(255,255,255,0.2); color:#fff; border-radius:20px; padding:7px 14px; font-family:'Orbitron'; font-size:0.65rem; outline:none; cursor:pointer;">
<option value="all">全部場次</option>
<option value="pending">未開始</option>
<option value="done">已完成</option>
</select>
</div>
<!-- 賽程列表 -->
<div id="schedule-list"></div>
</div>
</div>
<!-- 新增/編輯賽事彈窗 -->
<div id="match-modal" style="display:none; position:fixed; inset:0; z-index:3000; align-items:center; justify-content:center; background:rgba(0,0,0,0.75);">
<div style="background:#0d1117; border:2px solid rgba(0,220,255,0.4); border-radius:16px; padding:24px; width:min(420px,90vw); display:flex; flex-direction:column; gap:14px;">
<div style="font-family:'Orbitron'; font-size:0.85rem; letter-spacing:3px; color:#00dcff;" id="match-modal-title">新增賽事</div>
<div style="display:grid; grid-template-columns:1fr 1fr; gap:10px;">
<div style="display:flex; flex-direction:column; gap:5px;">
<label style="font-size:0.65rem; color:rgba(255,180,0,0.7); font-family:'Orbitron';">主隊</label>
<input id="m-home" placeholder="主隊名稱" style="background:rgba(255,255,255,0.06); border:1px solid rgba(255,255,255,0.15); border-radius:8px; padding:8px 10px; color:#fff; font-family:'Noto Sans TC'; font-size:0.85rem; outline:none;">
</div>
<div style="display:flex; flex-direction:column; gap:5px;">
<label style="font-size:0.65rem; color:rgba(255,180,0,0.7); font-family:'Orbitron';">客隊</label>
<input id="m-away" placeholder="客隊名稱" style="background:rgba(255,255,255,0.06); border:1px solid rgba(255,255,255,0.15); border-radius:8px; padding:8px 10px; color:#fff; font-family:'Noto Sans TC'; font-size:0.85rem; outline:none;">
</div>
</div>
<div style="display:grid; grid-template-columns:1fr 1fr; gap:10px;">
<div style="display:flex; flex-direction:column; gap:5px;">
<label style="font-size:0.65rem; color:rgba(255,255,255,0.4); font-family:'Orbitron';">日期</label>
<input id="m-date" type="date" style="background:rgba(255,255,255,0.06); border:1px solid rgba(255,255,255,0.15); border-radius:8px; padding:8px 10px; color:#fff; font-family:'Orbitron'; font-size:0.8rem; outline:none;">
</div>
<div style="display:flex; flex-direction:column; gap:5px;">
<label style="font-size:0.65rem; color:rgba(255,255,255,0.4); font-family:'Orbitron';">時間</label>
<input id="m-time" type="time" style="background:rgba(255,255,255,0.06); border:1px solid rgba(255,255,255,0.15); border-radius:8px; padding:8px 10px; color:#fff; font-family:'Orbitron'; font-size:0.8rem; outline:none;">
</div>
</div>
<div style="display:flex; flex-direction:column; gap:5px;">
<label style="font-size:0.65rem; color:rgba(255,255,255,0.4); font-family:'Orbitron';">場地</label>
<input id="m-venue" placeholder="場地名稱(選填)" style="background:rgba(255,255,255,0.06); border:1px solid rgba(255,255,255,0.15); border-radius:8px; padding:8px 10px; color:#fff; font-family:'Noto Sans TC'; font-size:0.85rem; outline:none;">
</div>
<div style="display:grid; grid-template-columns:1fr auto 1fr; gap:8px; align-items:center;">
<div style="display:flex; flex-direction:column; gap:5px;">
<label style="font-size:0.65rem; color:rgba(0,243,255,0.7); font-family:'Orbitron'; text-align:center;">主隊得分</label>
<input id="m-hscore" type="number" min="0" placeholder="-" style="background:rgba(0,243,255,0.06); border:1px solid rgba(0,243,255,0.2); border-radius:8px; padding:8px 10px; color:#00f3ff; font-family:'Orbitron'; font-size:1rem; outline:none; text-align:center;">
</div>
<div style="font-family:'Orbitron'; color:rgba(255,255,255,0.3); font-size:1rem; text-align:center;">VS</div>
<div style="display:flex; flex-direction:column; gap:5px;">
<label style="font-size:0.65rem; color:rgba(255,180,0,0.7); font-family:'Orbitron'; text-align:center;">客隊得分</label>
<input id="m-ascore" type="number" min="0" placeholder="-" style="background:rgba(255,180,0,0.06); border:1px solid rgba(255,180,0,0.2); border-radius:8px; padding:8px 10px; color:#ffb300; font-family:'Orbitron'; font-size:1rem; outline:none; text-align:center;">
</div>
</div>
<div style="display:flex; gap:10px; margin-top:4px;">
<button onclick="saveMatch()" style="flex:1; padding:10px; border-radius:10px; border:none; background:#00dcff; color:#000; font-family:'Orbitron'; font-weight:900; font-size:0.85rem; cursor:pointer;">儲存</button>
<button onclick="closeMatchModal()" style="padding:10px 20px; border-radius:10px; border:1px solid rgba(255,255,255,0.2); background:transparent; color:rgba(255,255,255,0.5); font-size:0.85rem; cursor:pointer;">取消</button>
</div>
</div>
</div>
<!-- Layer 5:名人堂 -->
<div id="layer5" class="layer" style="justify-content:flex-start;">
<button class="btn-back" onclick="showLayer(1)">◀ HOME</button>
<h2 style="flex-shrink:0; font-family:'Orbitron'; color:#ffd700; border-bottom:3px solid #ffd700; margin:0 0 20px 0; width:100%; max-width:1200px; padding-bottom:10px; text-shadow:0 0 16px #ffd700; letter-spacing:6px;">🏆 HALL OF FAME</h2>
<div style="width:100%; max-width:1200px; flex:1; overflow-y:auto; padding-bottom:40px;">
<div id="leaderboard-content"></div>
</div>
</div>
<!-- Layer 2:隊伍 -->
<div id="layer2" class="layer" style="justify-content:flex-start;">
<button class="btn-back" onclick="showLayer(1)">◀ HOME</button>
<h2 style="flex-shrink:0; font-family:'Orbitron'; color:var(--neon-blue); border-bottom:3px solid var(--neon-blue); margin:0 0 10px 0; width:100%; max-width:1200px; padding-bottom:10px; text-shadow:0 0 12px var(--neon-blue);">參賽隊伍</h2>
<div class="search-wrap">
<span class="search-icon">🔍</span>
<input id="team-search" type="text" placeholder="SEARCH TEAM...">
</div>
<div class="scroll-content"><div id="team-grid" class="grid-box"></div></div>
</div>
<!-- Layer 3:球員列表 -->
<div id="layer3" class="layer" style="justify-content:flex-start;">
<button class="btn-back" onclick="showLayer(2)" style="flex-shrink:0;">◀ TEAMS</button>
<div style="width:100%; max-width:1200px; flex:1; overflow-y:auto; padding-bottom:30px; box-sizing:border-box;">
<h2 id="team-title" style="font-family:'Orbitron'; color:var(--neon-blue); border-bottom:3px solid var(--neon-blue); margin:0 0 10px 0; padding-bottom:10px;"></h2>
<div id="team-group-photo-wrap" style="width:100%; margin-bottom:12px; border-radius:14px; overflow:hidden; border:2px solid rgba(0,243,255,0.25); display:none;">
<img id="team-group-photo" src="" alt="全隊合照" style="width:100%; max-height:400px; display:block; object-fit:contain; object-position:center; background:#0a0a0a;">
</div>
<div class="team-overview" id="team-overview">
<div class="team-overview-title">◈ TEAM POWER OVERVIEW</div>
<div class="overview-bars" id="overview-bars"></div>
</div>
<div class="search-wrap" style="margin:0 0 12px 0;">
<span class="search-icon">🔍</span>
<input id="player-search" type="text" placeholder="SEARCH PLAYER...">
</div>
<div id="player-grid" class="grid-box"></div>
</div>
</div>
<!-- Layer 4:球員詳情 -->
<div id="layer4" class="layer">
<button class="btn-back" onclick="showLayer(3)">◀ ROSTER</button>
<div class="topps-layout">
<div id="topps-card" class="topps-card topps-NORMAL">
<img id="det-photo" class="topps-photo" src="" alt="球員照片">
<div class="topps-info">
<div class="topps-name-row">
<div id="det-name" class="topps-name"></div>
<div id="det-rank-badge" class="topps-rank-badge rank-NORMAL">S</div>
</div>
<div id="topps-div" class="topps-divider div-NORMAL"></div>
<div id="det-pos" class="topps-pos"></div>
<div id="det-slogan" class="topps-slogan"></div>
</div>
</div>
<div class="topps-radar-panel">
<div class="radar-title">PLAYER STATS</div>
<div id="radarChart-wrap"><canvas id="radarChart"></canvas></div>
</div>
</div>
</div>
<!-- Layer 6:戰術板 -->
<div id="layer6" class="layer">
<div style="width:100%; max-width:1400px; display:flex; align-items:center; justify-content:space-between; flex-shrink:0; margin-bottom:10px;">
<button class="btn-back" onclick="showLayer(1)" style="margin:0;">◀ HOME</button>
<h2 style="font-family:'Orbitron'; color:#ffb300; margin:0; font-size:1.1rem; letter-spacing:4px; text-shadow:0 0 12px #ffb300;">⚾ BASEBALL TACTICAL BOARD</h2>
<div style="display:flex; gap:8px;">
<button class="tac-action-btn" onclick="clearTactical()" style="color:#ff4444; border-color:rgba(255,68,68,0.4); background:rgba(255,68,68,0.06);">🗑 清除</button>
<button class="tac-action-btn" onclick="undoTactical()" style="color:#ffd700; border-color:rgba(255,215,0,0.4); background:rgba(255,215,0,0.06);">↩ 上一步</button>
</div>
</div>
<div class="tactical-container">
<div class="tactical-sidebar">
<div class="tactical-sidebar-section">
<div class="tactical-sidebar-title">◈ 繪圖工具</div>
<button class="tac-tool-btn active" id="tool-pen" onclick="setTacTool('pen')">✏️ 畫筆</button>
<button class="tac-tool-btn" id="tool-arrow" onclick="setTacTool('arrow')">➡️ 箭頭</button>
<button class="tac-tool-btn" id="tool-erase" onclick="setTacTool('erase')">🧹 橡皮擦</button>
</div>
<div class="tactical-sidebar-section">
<div class="tactical-sidebar-title">◈ 顏色</div>
<div class="tac-color-row">
<div class="tac-color-btn active" style="background:#ff3333;" onclick="setTacColor('#ff3333',this)" title="紅"></div>
<div class="tac-color-btn" style="background:#00f3ff;" onclick="setTacColor('#00f3ff',this)" title="藍"></div>
<div class="tac-color-btn" style="background:#ffd700;" onclick="setTacColor('#ffd700',this)" title="黃"></div>
<div class="tac-color-btn" style="background:#00ff80;" onclick="setTacColor('#00ff80',this)" title="綠"></div>
<div class="tac-color-btn" style="background:#ff7700;" onclick="setTacColor('#ff7700',this)" title="橙"></div>
<div class="tac-color-btn" style="background:#fff;" onclick="setTacColor('#ffffff',this)" title="白"></div>
</div>
</div>
<div class="tactical-sidebar-section">
<div class="tactical-sidebar-title">◈ 線條粗細</div>
<div class="tac-size-row">
<button class="tac-size-btn" onclick="setTacSize(2,this)">細</button>
<button class="tac-size-btn active" onclick="setTacSize(4,this)">中</button>
<button class="tac-size-btn" onclick="setTacSize(8,this)">粗</button>
</div>
</div>
<div class="tactical-sidebar-section">
<div class="tactical-sidebar-title">◈ 球員標記</div>
<button class="tac-shape-btn" id="tool-batter" onclick="setTacTool('batter')">🟡 打者</button>
<button class="tac-shape-btn" id="tool-runner" onclick="setTacTool('runner')">🔵 跑者</button>
<button class="tac-shape-btn" id="tool-fielder" onclick="setTacTool('fielder')">🔴 守備員</button>
<button class="tac-shape-btn" id="tool-ball" onclick="setTacTool('ball')">⚪ 球</button>
</div>
<div class="tactical-sidebar-section">
<div class="tactical-sidebar-title">◈ 文字</div>
<button class="tac-tool-btn" id="tool-text" onclick="setTacTool('text')">🔤 加入文字</button>
</div>
<div class="tactical-sidebar-section">
<div class="tactical-sidebar-title">◈ 場景</div>
<button class="tac-tool-btn" onclick="drawBaseballField('full')">全場</button>
<button class="tac-tool-btn" onclick="drawBaseballField('infield')">內野特寫</button>
</div>
</div>
<div class="tactical-board-wrap">
<canvas id="tactical-canvas"></canvas>
</div>
</div>
</div>
<!-- Layer 7:計分板(自動化版) -->
<div id="layer7" class="layer">
<!-- Flash Banners -->
<div class="bb-banner bb-banner-out" id="bb-banner-out"></div>
<div class="bb-banner bb-banner-hr" id="bb-banner-hr">🏠 HOME RUN 🏠</div>
<div class="bb-banner bb-banner-side" id="bb-banner-side">三出局!攻守交換</div>
<div class="bb-banner bb-banner-score" id="bb-banner-score"></div>
<div style="width:100%; max-width:1200px; display:flex; align-items:center; justify-content:space-between; flex-shrink:0; margin-bottom:8px;">
<button class="btn-back" onclick="showLayer(1)" style="margin:0;">◀ HOME</button>
<h2 style="font-family:'Orbitron'; color:#ff3c78; margin:0; font-size:1.1rem; letter-spacing:4px; text-shadow:0 0 12px #ff3c78;">⚾ BASEBALL SCOREBOARD</h2>
<button class="tac-action-btn" onclick="resetScoreboard()" style="color:#ff4444; border-color:rgba(255,68,68,0.4); background:rgba(255,68,68,0.06);">🔄 重置</button>
</div>
<div class="bb-wrap">
<!-- TOP:大計分 + BSO + 壘包 -->
<div class="bb-top-card">
<div class="bb-inning-bar">
<span class="bb-inning-label">局次:</span>
<button class="bb-inning-btn active" onclick="bbSetInning(1)" id="inning-btn-1">1</button>
<button class="bb-inning-btn" onclick="bbSetInning(2)" id="inning-btn-2">2</button>
<button class="bb-inning-btn" onclick="bbSetInning(3)" id="inning-btn-3">3</button>
<button class="bb-inning-btn" onclick="bbSetInning(4)" id="inning-btn-4">4</button>
<button class="bb-inning-btn" onclick="bbSetInning(5)" id="inning-btn-5">5</button>
<button class="bb-inning-btn" onclick="bbSetInning(6)" id="inning-btn-6">6</button>
<button class="bb-inning-btn" onclick="bbSetInning(7)" id="inning-btn-7">EX</button>
</div>
<div class="bb-score-row">
<div class="bb-team-block bb-team-home">
<div style="font-family:'Orbitron'; font-size:0.52rem; letter-spacing:3px; color:rgba(0,243,255,0.45);">主隊</div>
<input class="bb-team-name-input" id="bb-home-name" value="主隊" maxlength="10" oninput="bbRenderInningTable()">
<div class="bb-big-score" id="bb-home-score">0</div>
<div class="bb-manual-btns">
<button class="bb-manual-btn" onclick="bbManualScore('home',-1)">−</button>
<button class="bb-manual-btn" onclick="bbManualScore('home',1)">+1</button>
</div>
</div>
<div class="bb-vs-col">
<div class="bb-vs-text">VS</div>
<div class="bb-bso-wrap">
<div class="bb-bso-col">
<div class="bb-bso-label">STRIKE</div>
<div class="bb-bso-lights" id="bb-strike-lights"></div>
</div>
<div class="bb-bso-col">
<div class="bb-bso-label">OUT</div>
<div class="bb-bso-lights" id="bb-out-lights"></div>
</div>
</div>
<div class="bb-bases-wrap">
<div class="bb-base" id="base-2nd" title="二壘"></div>
<div class="bb-base" id="base-3rd" title="三壘"></div>
<div class="bb-base" id="base-1st" title="一壘"></div>
<div class="bb-base" id="base-home" title="本壘"></div>
</div>
<div class="bb-side-label" id="bb-side-label">主隊攻擊中</div>
</div>
<div class="bb-team-block bb-team-away">
<div style="font-family:'Orbitron'; font-size:0.52rem; letter-spacing:3px; color:rgba(255,180,0,0.45);">客隊</div>
<input class="bb-team-name-input" id="bb-away-name" value="客隊" maxlength="10" oninput="bbRenderInningTable()">
<div class="bb-big-score" id="bb-away-score">0</div>
<div class="bb-manual-btns">
<button class="bb-manual-btn" onclick="bbManualScore('away',-1)">−</button>
<button class="bb-manual-btn" onclick="bbManualScore('away',1)">+1</button>
</div>
</div>
</div>
</div>
<!-- LEFT:打擊順序 -->
<div class="bb-lineup-col">
<div class="bb-col-title">◈ 打擊順序</div>
<div class="bb-lineup-tabs">
<button class="bb-lineup-tab active" onclick="bbSwitchLineup('home')" id="lineup-tab-home">主隊</button>
<button class="bb-lineup-tab" onclick="bbSwitchLineup('away')" id="lineup-tab-away">客隊</button>
</div>
<select class="bb-team-sel" id="bb-team-sel" onchange="bbOnTeamChange()">
<option value="">-- 選隊伍 --</option>
</select>
<div class="bb-lineup-list" id="bb-lineup-list"></div>
<button class="bb-advance-btn" onclick="bbManualAdvance()">▶ 手動推進打者</button>
</div>
<!-- CENTER:動作面板 + 各局得分 -->
<div class="bb-center-col">
<div class="bb-action-panel">
<div class="bb-batter-label">打者:<span id="bb-batter-display">---</span></div>
<div class="bb-act-group-lbl">── 安打 / 上壘 ──</div>
<div class="bb-act-row">
<button class="bb-act-btn act-1b" onclick="bbDoPlay('1b')">一壘打</button>
<button class="bb-act-btn act-2b" onclick="bbDoPlay('2b')">二壘打</button>
<button class="bb-act-btn act-3b" onclick="bbDoPlay('3b')">三壘打</button>
<button class="bb-act-btn act-hr" onclick="bbDoPlay('hr')">🏠 全壘打</button>
</div>
<div class="bb-act-divider">─── OUT ───</div>
<div class="bb-act-row">
<button class="bb-act-btn act-k" onclick="bbDoPlay('k')">三振 K</button>
<button class="bb-act-btn act-go" onclick="bbDoPlay('go')">滾地 Out</button>
<button class="bb-act-btn act-fo" onclick="bbDoPlay('fo')">飛球 Out</button>
<button class="bb-act-btn act-dp" onclick="bbDoPlay('dp')">雙殺 DP</button>
</div>
</div>
<div class="bb-inning-table-card">
<div class="bb-table-title">◈ 各局得分</div>
<table class="bb-table">
<thead><tr>
<th>隊伍</th>
<th>1</th><th>2</th><th>3</th><th>4</th><th>5</th><th>6</th><th>EX</th>
<th style="border-left:2px solid rgba(255,255,255,0.1);">R</th>
</tr></thead>
<tbody id="bb-inning-tbody"></tbody>
</table>
</div>
</div>
<!-- RIGHT:比賽紀錄 -->
<div class="bb-log-col">
<div class="bb-col-title">◈ 比賽紀錄</div>
<div class="bb-log-list" id="bb-log-list">
<div style="text-align:center;font-family:'Orbitron';font-size:0.62rem;color:rgba(255,255,255,0.15);padding:16px;">尚無紀錄</div>
</div>
</div>
</div>
</div>
<!-- Layer 8:競賽規則 -->
<div id="layer8" class="layer">
<div style="width:100%; max-width:900px; display:flex; align-items:center; justify-content:space-between; flex-shrink:0; margin-bottom:12px;">
<button class="btn-back" onclick="showLayer(1)" style="margin:0;">◀ HOME</button>
<h2 style="font-family:'Orbitron'; color:#00ffb3; margin:0; font-size:1.1rem; letter-spacing:4px; text-shadow:0 0 12px #00ffb3;">📖 樂樂棒球競賽規則</h2>
<div style="font-family:'Orbitron'; font-size:0.6rem; color:rgba(0,255,180,0.4); letter-spacing:2px;">中華民國樂樂棒球推廣協會</div>
</div>
<div class="rules-wrap">
<div class="rules-section">
<div class="rules-section-title"><span class="rule-icon">🏟️</span> 比賽場地</div>
<div class="rules-grid">
<div class="rule-card"><div class="rule-card-label">壘間距離</div><div class="rule-card-value">14 公尺</div><div class="rule-card-sub">運動場使用 11號T用球</div></div>
<div class="rule-card"><div class="rule-card-label">外野距離</div><div class="rule-card-value">40 公尺以上</div><div class="rule-card-sub">界外線到球場外線 8m 以上</div></div>
<div class="rule-card"><div class="rule-card-label">打擊圈</div><div class="rule-card-value">半徑 3 公尺</div><div class="rule-card-sub">以本壘板後角為中心設置</div></div>
<div class="rule-card"><div class="rule-card-label">擊球座位置</div><div class="rule-card-value">本壘板後方 50cm~1m</div><div class="rule-card-sub">劃線寬度均為 5cm</div></div>
</div>
</div>
<div class="rules-section">
<div class="rules-section-title"><span class="rule-icon">👥</span> 守備位置(9位)</div>
<div class="rules-grid">
<div class="rule-card"><div class="rule-card-value" style="font-size:0.85rem; line-height:1.8;">本壘手・一壘手・二壘手・三壘手<br>第一游擊手(可守投手丘)<br>第二游擊手<br>左外野手・中外野手・右外野手</div></div>
</div>
</div>
<div class="rules-section">
<div class="rules-section-title"><span class="rule-icon">⚾</span> 比賽進行</div>
<div class="rules-list">
<div class="rule-item highlight"><div class="ri-num">★</div><div>全隊打擊者都擊球完畢後,才進行攻防交替(每隊攻防兩次)</div></div>
<div class="rule-item"><div class="ri-num">1</div><div>殘留在壘上的跑壘員,下一局可繼續從殘壘接跑(最後一局除外)</div></div>
<div class="rule-item"><div class="ri-num">2</div><div>比賽採積分制:<span class="rule-tag tag-score-r">勝 2分</span><span class="rule-tag tag-rule">和 1分</span><span class="rule-tag tag-out-r">敗 0分</span></div></div>
<div class="rule-item"><div class="ri-num">3</div><div>預賽採和局制,決賽需出勝負。積分相同時依序比較:① 兩隊勝負關係 ② 總得分較多者 ③ 總失分較少者 ④ 抽籤</div></div>
<div class="rule-item highlight"><div class="ri-num">★</div><div>突破僵局賽:滿壘開始(第一、二、三棒分站三、二、一壘),第四~六棒打擊,得分多者勝</div></div>
</div>
</div>
<div class="rules-section">
<div class="rules-section-title"><span class="rule-icon">🏃</span> 得分與跑壘</div>
<div class="rules-list">
<div class="rule-item highlight"><div class="ri-num">★</div><div>跑壘員在一局結束前,按照 <strong>一壘 → 二壘 → 三壘 → 本壘</strong> 順序觸壘,判得一分</div></div>
<div class="rule-item"><div class="ri-num">1</div><div>打擊者擊球後成為跑壘員,必須按順序依次跑壘</div></div>
<div class="rule-item"><div class="ri-num">2</div><div>跑壘員進行滑壘時,<span class="rule-tag tag-out-r">通過二壘、三壘壘包時不得離壘</span>,若離壘可觸殺出局</div></div>
<div class="rule-item"><div class="ri-num">3</div><div>兩名以上跑壘員不得同時占有同一壘。先上壘者擁有壘權</div></div>
</div>
</div>
<div class="rules-section">
<div class="rules-section-title"><span class="rule-icon">🏏</span> 打擊規則</div>
<div class="rules-list">
<div class="rule-item"><div class="ri-num">1</div><div><span class="rule-tag tag-foul">好球</span> 揮棒落空・擊中球座(界外球)・擊成界外球・有觸球意圖・<strong>主軸腳移動兩步以上</strong></div></div>
<div class="rule-item highlight"><div class="ri-num">★</div><div>打擊者必須在主審發出「開始」口令後 <strong>10秒內</strong> 擊球,否則判好球</div></div>
<div class="rule-item"><div class="ri-num">2</div><div><span class="rule-tag tag-out-r">打擊者出局</span> 第二個好球後再犯好球條件・界外高飛球被捕接・使用違規球棒・打擊犯規</div></div>
<div class="rule-item"><div class="ri-num">3</div><div><span class="rule-tag tag-safe">擊球無效</span> 主審已宣布暫停時擊球 → 不計次數,繼續重打</div></div>
</div>
</div>
<div class="rules-section">
<div class="rules-section-title"><span class="rule-icon">📐</span> 界內球 / 界外球</div>
<div class="rules-list">
<div class="rule-item"><div class="ri-num">1</div><div><span class="rule-tag tag-safe">界內球</span> 球停在界內區・球從內場彈跳到外場前通過一壘或三壘上空・球觸及一壘或三壘壘包</div></div>
<div class="rule-item"><div class="ri-num">2</div><div><span class="rule-tag tag-foul">界外球</span> 界內球以外的所有球。落在打擊圈內的球是界外球(但打擊圈線包含在界內球區域)</div></div>
<div class="rule-item"><div class="ri-num">3</div><div>界內或界外的判定,由<strong>球與邊界線的關係</strong>決定,不是以防守隊員觸球時的位置決定</div></div>
</div>
</div>
<div class="rules-section">
<div class="rules-section-title"><span class="rule-icon">🚫</span> 跑壘員出局情況</div>
<div class="rules-list">
<div class="rule-item"><div class="ri-num">1</div><div>跑壘員離開壘包,被防守隊員觸殺時</div></div>
<div class="rule-item"><div class="ri-num">2</div><div>為了逃避觸殺,跑離了距離壘間線兩側 <strong>1m 以上</strong>的範圍時</div></div>
<div class="rule-item"><div class="ri-num">3</div><div>高飛球或平飛球被野手捕接住時</div></div>
<div class="rule-item"><div class="ri-num">4</div><div>跑壘員在跑到一壘之前,防守隊員持球觸碰一壘壘包時</div></div>
<div class="rule-item"><div class="ri-num">5</div><div>跑壘員超越了尚未出局的前一個跑壘員時</div></div>
<div class="rule-item highlight"><div class="ri-num">★</div><div>跑壘員進行滑壘時,通過二壘、三壘壘包時不得離壘,若離壘可觸殺出局</div></div>
</div>
</div>
<div class="rules-section">
<div class="rules-section-title"><span class="rule-icon">✅</span> 安全上壘(特殊情況)</div>
<div class="rules-list">
<div class="rule-item"><div class="ri-num">1</div><div>本壘手或其他防守隊員妨礙打擊者擊球時 → 打擊者安全上一壘</div></div>
<div class="rule-item"><div class="ri-num">2</div><div>防守隊員捕球後送球越過界外球界線 → 原有壘基礎上安全上到下一壘</div></div>
<div class="rule-item"><div class="ri-num">3</div><div>跑壘受到妨礙時 → 原則上安全上到下一壘</div></div>
<div class="rule-item"><div class="ri-num">4</div><div>防守球員故意用分指手套、合指手套、球帽等拋碰送球 → 安全上到下兩個壘;故意拋碰界內球 → 安全上到下三個壘</div></div>
</div>
</div>
<div class="rules-section">
<div class="rules-section-title"><span class="rule-icon">👨⚖️</span> 裁判員</div>
<div class="rules-list">
<div class="rule-item"><div class="ri-num">1</div><div>原則上實行<strong>兩人制</strong>:主審(主判本壘及三壘周圍)+ 壘審(主判一壘及二壘周圍)</div></div>
<div class="rule-item"><div class="ri-num">2</div><div>壘審位置在一壘手後方之界外線上</div></div>
<div class="rule-item"><div class="ri-num">3</div><div>主審站在打擊者正側面,在打擊者或本壘手放好球後,宣佈「開始比賽」</div></div>
<div class="rule-item highlight"><div class="ri-num">★</div><div>只有該隊的<strong>隊長和副隊長</strong>能夠對裁判提出抗議</div></div>
</div>
</div>
</div>
</div>
<script>
let players = [];
let chart = null;
let currentTeam = '';
// ── 粒子背景 ──
const canvas = document.getElementById('particles-canvas');
const ctx = canvas.getContext('2d');
let particles = [];
function resizeCanvas() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; }
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
function createParticle() { return { x: Math.random()*canvas.width, y: Math.random()*canvas.height, vx: (Math.random()-0.5)*0.6, vy: (Math.random()-0.5)*0.6, size: Math.random()*1.8+0.5, opacity: Math.random()*0.6+0.1, color: Math.random()>0.7?'#bc13fe':'#00f3ff' }; }
for (let i=0;i<120;i++) particles.push(createParticle());
function drawParticles() {
ctx.clearRect(0,0,canvas.width,canvas.height);
if (!document.getElementById('layer1').classList.contains('active')) { requestAnimationFrame(drawParticles); return; }
particles.forEach((p,i) => {
p.x+=p.vx; p.y+=p.vy;
if(p.x<0||p.x>canvas.width) p.vx*=-1;
if(p.y<0||p.y>canvas.height) p.vy*=-1;
particles.forEach((q,j) => {
if(j<=i) return;
const dx=p.x-q.x, dy=p.y-q.y, dist=Math.sqrt(dx*dx+dy*dy);
if(dist<120) { ctx.beginPath(); ctx.strokeStyle=`rgba(0,243,255,${0.15*(1-dist/120)})`; ctx.lineWidth=0.5; ctx.moveTo(p.x,p.y); ctx.lineTo(q.x,q.y); ctx.stroke(); }
});
ctx.beginPath(); ctx.arc(p.x,p.y,p.size,0,Math.PI*2); ctx.globalAlpha=p.opacity; ctx.fillStyle=p.color; ctx.fill(); ctx.globalAlpha=1;
});
requestAnimationFrame(drawParticles);
}
drawParticles();
function toggleLayer1Deco(show) {
['scan-line','status-bar'].forEach(id => {
document.getElementById(id).style.display = show ? '' : 'none';
});
}
function showLayer(n) {
document.querySelectorAll('.layer').forEach(l => l.classList.remove('active'));
document.getElementById('layer'+n).classList.add('active');
toggleLayer1Deco(n===1);
if(n===2) { document.getElementById('team-search').value=''; renderTeams(); }
if(n===3) {
document.getElementById('player-search').value='';
renderPlayers(currentTeam);
if(!vsMode) { vsSelected=[]; document.getElementById('vs-btn').classList.add('active'); document.getElementById('vs-btn').textContent='⚔ VERSUS'; document.getElementById('vs-btn').style.background=''; document.getElementById('vs-hint').classList.remove('active'); }
else { document.getElementById('vs-btn').classList.add('active'); setTimeout(()=>reapplyVsSelected(),50); }
} else {
if(vsMode&&n===2) { document.getElementById('vs-btn').classList.add('active'); const hint=document.getElementById('vs-hint'); hint.classList.add('active'); hint.textContent=vsSelected.length===0?'選一個隊伍挑選第一位球員':`✓ ${vsSelected[0]},選一個隊伍挑第二位`; }
else { document.getElementById('vs-btn').classList.remove('active'); document.getElementById('vs-hint').classList.remove('active'); if(n!==2) vsMode=false; }
}
if(n===5) renderLeaderboard();
if(n===6) initTacticalBoard();
if(n===7) bbInitScoreboard();
if(n===9) renderSchedule();
}
window.onload = () => {
toggleLayer1Deco(false);
document.getElementById('team-search').addEventListener('input', e=>filterTeams(e.target.value.trim()));
document.getElementById('player-search').addEventListener('input', e=>filterPlayers(e.target.value.trim()));
google.script.run
.withSuccessHandler(data => {
if(!Array.isArray(data)) { showError(data&&data.error?data.error:'資料格式錯誤'); return; }
players = data;
document.getElementById('loading').style.display='none';
showLayer(1);
renderTeams();
initTicker();
})
.withFailureHandler(err => { showError('連線失敗:'+(err.message||err)); })
.getPlayerData();
};
function showError(msg) { document.getElementById('loading').style.display='none'; document.getElementById('error-text').innerText=msg; document.getElementById('error-msg').style.display='flex'; }
function escapeHtml(str) { if(!str) return ''; return String(str).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,'''); }
function renderTeams() {
const teams = {}; players.forEach(p=>{ if(!teams[p.team]) teams[p.team]=p.teamLogo; }); buildTeamCards(teams);
}
function filterTeams(keyword) {
const teams={}; players.forEach(p=>{ if(!teams[p.team]) teams[p.team]=p.teamLogo; });
if(!keyword) { buildTeamCards(teams); return; }
const filtered={}; Object.keys(teams).forEach(t=>{ if(t.toLowerCase().includes(keyword.toLowerCase())) filtered[t]=teams[t]; }); buildTeamCards(filtered);
}
function buildTeamCards(teams) {
const container=document.getElementById('team-grid'); container.innerHTML='';
const keys=Object.keys(teams);
if(keys.length===0) { container.innerHTML='<div class="search-empty">NO TEAMS FOUND</div>'; return; }
keys.forEach(t => {
const card=document.createElement('div'); card.className='card';
card.innerHTML=`<div class="card-inner"><img src="${escapeHtml(teams[t])}" onerror="this.src='https://via.placeholder.com/300'"><h3 style="font-family:'Orbitron'; margin-top:15px;">${escapeHtml(t)}</h3></div>`;
card.addEventListener('click',()=>{ currentTeam=t; document.getElementById('team-title').innerText=t; document.getElementById('player-search').value=''; renderPlayers(t); renderTeamOverview(t); renderTeamGroupPhoto(t); showLayer(3); });
container.appendChild(card);
});
}
function renderLeaderboard() {
const categories=[{key:'pass',label:'傳球王',icon:'🏆',color:'#00f3ff'},{key:'catch',label:'接球達人',icon:'🤲',color:'#bc13fe'},{key:'field',label:'防守鐵壁',icon:'🛡️',color:'#ffd700'},{key:'mobility',label:'速度之星',icon:'⚡',color:'#ff6b35'},{key:'decision',label:'決策大師',icon:'🧠',color:'#00ff88'}];
const container=document.getElementById('leaderboard-content');
container.innerHTML=categories.map(cat=>{
const top3=[...players].sort((a,b)=>b[cat.key]-a[cat.key]).slice(0,3);
const medals=['🥇','🥈','🥉']; const rankClass=['hall-rank-1','hall-rank-2','hall-rank-3'];
const order=[top3[1],top3[0],top3[2]].filter(Boolean);
const orderClass=top3[1]?[rankClass[1],rankClass[0],rankClass[2]]:[rankClass[0],rankClass[2]];
const orderMedal=top3[1]?[medals[1],medals[0],medals[2]]:[medals[0],medals[2]];
return `<div class="hall-section"><div class="hall-section-title" style="color:${cat.color};border-color:${cat.color}33;"><span style="font-size:1.4rem;">${cat.icon}</span> ${cat.label}</div><div class="hall-podium">${order.map((p,i)=>`<div class="hall-player ${orderClass[i]}" onclick="goToPlayer('${escapeHtml(p.name)}')"><div class="hall-medal">${orderMedal[i]}</div><img class="hall-player-photo" src="${escapeHtml(p.photo)}" onerror="this.style.opacity='0.2'" alt="${escapeHtml(p.name)}"><div class="hall-player-name">${escapeHtml(p.name)}</div><div class="hall-player-team">${escapeHtml(p.team)}</div><div class="hall-player-score">${p[cat.key]}</div></div>`).join('')}</div></div>`;
}).join('');
}
function goToPlayer(name) {
const p=players.find(x=>x.name===name); if(!p) return;
currentTeam=p.team; document.getElementById('team-title').innerText=p.team;
renderPlayers(p.team); renderTeamOverview(p.team); renderTeamGroupPhoto(p.team); showLayer(3);
setTimeout(()=>showDetail(name),100);
}
function renderTeamGroupPhoto(t) {
const wrap=document.getElementById('team-group-photo-wrap'); const img=document.getElementById('team-group-photo');
const tp=players.find(p=>p.team===t&&p.teamPhoto&&p.teamPhoto.trim());
if(tp) { img.src=tp.teamPhoto; img.onerror=()=>{ wrap.style.display='none'; }; wrap.style.display='block'; } else { wrap.style.display='none'; }
}
function renderTeamOverview(t) {
const tp=players.filter(p=>p.team===t); if(tp.length===0) return;
const labels=['傳球','接球','守備','行動','決策']; const keys=['pass','catch','field','mobility','decision'];
const avgs=keys.map(k=>{ const sum=tp.reduce((a,p)=>a+(Number(p[k])||0),0); return Math.round((sum/tp.length)*10)/10; });
document.getElementById('overview-bars').innerHTML=labels.map((lb,i)=>{ const pct=(avgs[i]/10*100).toFixed(1); return `<div class="bar-item"><div class="bar-label">${lb}</div><div class="bar-track"><div class="bar-fill" style="--bar-w:${pct}%;"></div></div><div class="bar-val">${avgs[i]}</div></div>`; }).join('');
}
function renderPlayers(t) { buildPlayerCards(players.filter(p=>p.team===t)); }
function filterPlayers(keyword) {
const tp=players.filter(p=>p.team===currentTeam);
if(!keyword) { buildPlayerCards(tp); return; }
buildPlayerCards(tp.filter(p=>p.name.toLowerCase().includes(keyword.toLowerCase())||p.position.toLowerCase().includes(keyword.toLowerCase())));
}
function getBadges(p) {
const badges=[];
if(p.pass>=9) badges.push({cls:'badge-pass',text:'精準砲火 🎯'});
if(p.catch>=9) badges.push({cls:'badge-catch',text:'穩接高手 🤲'});
if(p.field>=9) badges.push({cls:'badge-field',text:'銅牆鐵壁 🛡️'});
if(p.mobility>=9) badges.push({cls:'badge-mobility',text:'枋寮閃電 ⚡'});
if(p.decision>=9) badges.push({cls:'badge-decision',text:'棋盤大師 🧠'});
const total=p.pass+p.catch+p.field+p.mobility+p.decision;
if(total>=45) badges.push({cls:'badge-allstar',text:'全明星 ★'});
return badges;
}
function buildPlayerCards(list) {
const container=document.getElementById('player-grid'); container.innerHTML='';
if(list.length===0) { container.innerHTML='<div class="search-empty">NO PLAYERS FOUND</div>'; return; }
list.forEach(p=>{
const card=document.createElement('div'); card.className='player-card';
let photoHtml;
if(p.photo&&p.photo.trim()!=='') {
photoHtml=`<img class="player-card-photo" src="${escapeHtml(p.photo)}" alt="${escapeHtml(p.name)}" onerror="this.style.display='none';this.nextElementSibling.style.display='flex';"><div class="player-card-no-photo" style="display:none;"><div class="no-photo-icon">👤</div><div class="no-photo-label">NO PHOTO</div></div>`;
} else {
photoHtml=`<div class="player-card-no-photo"><div class="no-photo-icon">👤</div><div class="no-photo-label">NO PHOTO</div></div>`;
}
const badges=getBadges(p);
const badgesHtml=badges.length>0?`<div class="player-badges">${badges.map(b=>`<span class="badge-tag ${b.cls}">${b.text}</span>`).join('')}</div>`:'';
card.innerHTML=`<div class="player-card-badge">${escapeHtml(p.rank)} RANK|${escapeHtml(p.position)}|${escapeHtml(p.name)}</div>${photoHtml}${badgesHtml}`;
card.addEventListener('click',()=>{ if(vsMode){toggleVsSelect(p.name);return;} showDetail(p.name); });
card.addEventListener('contextmenu',e=>{e.preventDefault();if(vsMode)toggleVsSelect(p.name);});
let pressTimer;
card.addEventListener('touchstart',()=>{pressTimer=setTimeout(()=>{if(vsMode)toggleVsSelect(p.name);},500);});
card.addEventListener('touchend',()=>clearTimeout(pressTimer));
card.addEventListener('touchmove',()=>clearTimeout(pressTimer));
container.appendChild(card);
});
}
function showDetail(name) {
const p=players.find(x=>x.name===name); if(!p) return;
const toppsCard=document.getElementById('topps-card'); toppsCard.className='topps-card topps-'+p.tier;
document.getElementById('det-rank-badge').className='topps-rank-badge rank-'+p.tier; document.getElementById('det-rank-badge').innerText=p.rank;
document.getElementById('topps-div').className='topps-divider div-'+p.tier;
document.getElementById('det-photo').src=p.photo; document.getElementById('det-name').innerText=p.name;
document.getElementById('det-pos').innerText=p.position; document.getElementById('det-slogan').innerText=p.slogan?`" ${p.slogan} "`:'';
showLayer(4);
if(chart){chart.destroy();chart=null;}
document.getElementById('radarChart-wrap').innerHTML='<canvas id="radarChart"></canvas>';
const isDark=p.tier==='BLACK_GOLD'||p.tier==='NORMAL';
const mainColor=isDark?'#00f3ff':'#5e00d9'; const gridColor=isDark?'rgba(255,255,255,0.35)':'rgba(0,0,0,0.25)';
const radarCanvas=document.getElementById('radarChart');
const grad=radarCanvas.getContext('2d').createRadialGradient(radarCanvas.width/2,radarCanvas.height/2,0,radarCanvas.width/2,radarCanvas.height/2,radarCanvas.width/2);
if(isDark){grad.addColorStop(0,'rgba(0,243,255,0.9)');grad.addColorStop(0.5,'rgba(0,243,255,0.5)');grad.addColorStop(1,'rgba(0,243,255,0.1)');}
else{grad.addColorStop(0,'rgba(94,0,217,0.9)');grad.addColorStop(0.5,'rgba(94,0,217,0.5)');grad.addColorStop(1,'rgba(94,0,217,0.1)');}
const keys=['pass','catch','field','mobility','decision'];
const leagueAvg=keys.map(k=>{ const sum=players.reduce((a,x)=>a+(Number(x[k])||0),0); return players.length?Math.round(sum/players.length*10)/10:0; });
chart=new Chart(radarCanvas.getContext('2d'),{type:'radar',data:{labels:['傳球','接球','守備','行動','決策'],datasets:[{label:'全聯賽平均',data:leagueAvg,backgroundColor:'rgba(255,255,255,0.05)',borderColor:'rgba(255,255,255,0.25)',borderWidth:2,borderDash:[4,4],pointBackgroundColor:'rgba(255,255,255,0.3)',pointBorderColor:'transparent',pointRadius:4,fill:true},{label:p.name,data:[p.pass,p.catch,p.field,p.mobility,p.decision],backgroundColor:grad,borderColor:mainColor,borderWidth:6,pointBackgroundColor:mainColor,pointBorderColor:'#fff',pointRadius:7,pointHoverRadius:10,fill:true}]},options:{responsive:true,maintainAspectRatio:false,animation:{duration:1200,easing:'easeOutQuart'},scales:{r:{min:0,max:10,grid:{color:gridColor,lineWidth:2},angleLines:{color:gridColor,lineWidth:2},ticks:{display:false,stepSize:2},pointLabels:{color:mainColor,font:{size:19,weight:'900'},padding:20}}},plugins:{legend:{display:true,position:'bottom',labels:{color:'rgba(255,255,255,0.5)',font:{size:11,family:'Orbitron'},boxWidth:14,padding:12}}}}});
}
function initTicker() {
const msgs=[]; const categories=[{key:'pass',label:'傳球王',icon:'🏆'},{key:'catch',label:'接球達人',icon:'🤲'},{key:'field',label:'防守鐵壁',icon:'🛡️'},{key:'mobility',label:'速度之星',icon:'⚡'},{key:'decision',label:'決策大師',icon:'🧠'}];
categories.forEach(cat=>{ const top3=[...players].sort((a,b)=>b[cat.key]-a[cat.key]).slice(0,3); if(top3[0]){const names=top3.map((p,i)=>`${'🥇🥈🥉'[i]} ${p.name}(${p[cat.key]}分)`).join(' · '); msgs.push(`${cat.icon} ${cat.label}排行:${names}`);}});
const ranked=[...players].sort((a,b)=>{const avg=p=>p.pass+p.catch+p.field+p.mobility+p.decision;return avg(b)-avg(a);});
if(ranked[0]) msgs.push(`🌟 本屆綜合 MVP:${ranked[0].name}(${ranked[0].team})總分 ${ranked[0].pass+ranked[0].catch+ranked[0].field+ranked[0].mobility+ranked[0].decision} 分`);
if(ranked.length>=3) msgs.push(`🔥 頂尖三強:${ranked.slice(0,3).map(p=>p.name).join(' · ')} 稱霸全聯賽排行榜`);
document.getElementById('ticker-text').textContent=msgs.map(m=>`◈ ${m} `).join(' ');
document.getElementById('live-ticker').classList.add('active');
}
// ── Versus ──
let vsSelected=[], vsMode=false;
function reapplyVsSelected() { document.querySelectorAll('.player-card').forEach(card=>{ const badge=card.querySelector('.player-card-badge'); if(!badge) return; const cardName=badge.textContent.split('|')[2]||''; card.classList.toggle('vs-selected',vsSelected.includes(cardName.trim())); }); }
function toggleVsMode() {
vsMode=!vsMode; vsSelected=[]; document.querySelectorAll('.player-card').forEach(c=>c.classList.remove('vs-selected'));
const btn=document.getElementById('vs-btn'); const hint=document.getElementById('vs-hint');
if(vsMode){btn.textContent='✕ 取消';btn.style.background='linear-gradient(135deg,#555,#333)';hint.textContent='點選第一位球員';hint.classList.add('active');}
else{btn.textContent='⚔ VERSUS';btn.style.background='';hint.classList.remove('active');}
}
function toggleVsSelect(name) {
if(!vsMode) return; const idx=vsSelected.indexOf(name);
if(idx>-1) vsSelected.splice(idx,1); else { if(vsSelected.length>=2) return; vsSelected.push(name); }
document.querySelectorAll('.player-card').forEach(card=>{ const badge=card.querySelector('.player-card-badge'); if(!badge) return; const cardName=badge.textContent.split('|')[2]||''; card.classList.toggle('vs-selected',vsSelected.includes(cardName.trim())); });
const hint=document.getElementById('vs-hint');
if(vsSelected.length===0) hint.textContent='點選第一位球員';
else if(vsSelected.length===1) hint.textContent=`✓ ${vsSelected[0]},再選一位`;
else { hint.textContent='對戰開始!'; setTimeout(()=>triggerVersus(),400); }
}
function triggerVersus() {
if(vsSelected.length<2){document.getElementById('vs-hint').textContent='請先點選兩名球員!';return;}
const pa=players.find(p=>p.name===vsSelected[0]); const pb=players.find(p=>p.name===vsSelected[1]);
if(!pa||!pb) return;
const keys=['pass','catch','field','mobility','decision']; const labels=['傳球','接球','守備','行動','決策'];
const scoreA=keys.reduce((s,k)=>s+pa[k],0); const scoreB=keys.reduce((s,k)=>s+pb[k],0);
const total=scoreA+scoreB||1; const pctA=Math.round(scoreA/total*100); const pctB=100-pctA; const isAWin=pctA>=pctB;
document.getElementById('vs-img-a').src=pa.photo; document.getElementById('vs-name-a').textContent=pa.name; document.getElementById('vs-rank-a').textContent=pa.rank+' · '+pa.team;
document.getElementById('vs-img-b').src=pb.photo; document.getElementById('vs-name-b').textContent=pb.name; document.getElementById('vs-rank-b').textContent=pb.rank+' · '+pb.team;
document.getElementById('vs-card-img-a').style.borderColor='#ffd700'; document.getElementById('vs-card-img-b').style.borderColor='#bc13fe';
const cardA=document.getElementById('vs-card-a'); const cardB=document.getElementById('vs-card-b'); const stage=document.getElementById('vs-stage'); const result=document.getElementById('vs-result'); const flash=document.getElementById('vs-flash'); const explo=document.getElementById('vs-explosion');
cardA.className='vs-card-wrap card-a'; cardB.className='vs-card-wrap card-b'; cardA.style.opacity='1'; cardB.style.opacity='1'; result.classList.remove('show'); flash.classList.remove('bang'); explo.innerHTML=''; stage.classList.remove('shaking');
document.getElementById('vs-overlay').classList.add('active');
setTimeout(()=>{ cardA.classList.add('sliding'); cardB.classList.add('sliding'); },150);
setTimeout(()=>{
stage.classList.add('shaking'); setTimeout(()=>stage.classList.remove('shaking'),420); flash.classList.add('bang'); explo.innerHTML='';
const colors=['#ffd700','#ff6600','#bc13fe','#00f3ff','#fff','#ff3366'];
for(let i=0;i<28;i++){const s=document.createElement('div');s.className='vs-spark';const angle=(360/28)*i;const dist=80+Math.random()*120;const color=colors[Math.floor(Math.random()*colors.length)];s.style.cssText=`background:${color};box-shadow:0 0 6px ${color};--angle:${angle}deg;--dist:${dist}px;animation:spark-fly ${0.5+Math.random()*0.4}s ease-out forwards;`;explo.appendChild(s);}
setTimeout(()=>{cardA.style.transition='all 0.4s ease';cardB.style.transition='all 0.4s ease';cardA.style.opacity='0';cardB.style.opacity='0';},300);
setTimeout(()=>{
document.getElementById('vs-rimg-a').src=pa.photo; document.getElementById('vs-rname-a').textContent=pa.name; document.getElementById('vs-rimg-b').src=pb.photo; document.getElementById('vs-rname-b').textContent=pb.name;
const rfA=document.getElementById('vs-rf-a'); const rfB=document.getElementById('vs-rf-b');
rfA.className='vs-result-fighter '+(isAWin?'winner':'loser'); rfB.className='vs-result-fighter '+(!isAWin?'winner':'loser');
document.getElementById('vs-crown-a').textContent=isAWin?'🏆':''; document.getElementById('vs-crown-b').textContent=!isAWin?'🏆':'';
document.getElementById('vs-rpct-a').innerHTML=isAWin?`<div class="vs-winner-pct">${pctA}%</div>`:`<div class="vs-loser-pct">${pctA}%</div>`;
document.getElementById('vs-rpct-b').innerHTML=!isAWin?`<div class="vs-winner-pct">${pctB}%</div>`:`<div class="vs-loser-pct">${pctB}%</div>`;
document.getElementById('vs-bar-track').innerHTML=`<div class="vs-bar-a" style="--pct-a:${pctA}%;"></div><div class="vs-bar-b" style="--pct-b:${pctB}%;"></div>`;
document.getElementById('vs-detail').innerHTML=keys.map((k,i)=>{const aVal=pa[k],bVal=pb[k];const aStyle=aVal>bVal?'style="font-size:1.05rem;color:#ffd700;"':'';const bStyle=bVal>aVal?'style="font-size:1.05rem;color:#e0a0ff;"':'';return `<div class="vs-detail-a" ${aStyle}>${aVal}</div><div class="vs-detail-label">${labels[i]}</div><div class="vs-detail-b" ${bStyle}>${bVal}</div>`;}).join('');
result.classList.add('show');
},700);
},1100);
}
function closeVersus() {
document.getElementById('vs-overlay').classList.remove('active'); document.getElementById('vs-result').classList.remove('show');
vsMode=false; vsSelected=[]; document.querySelectorAll('.player-card').forEach(c=>c.classList.remove('vs-selected'));
const btn=document.getElementById('vs-btn'); btn.textContent='⚔ VERSUS'; btn.style.background=''; document.getElementById('vs-hint').classList.remove('active');
}
// ── 抽卡包 ──
let packBusy = false;
function showPackOverlay() { resetPack(); document.getElementById('pack-overlay').classList.add('active'); }
function openPack() {
if(packBusy) return;
const icon=document.getElementById('pack-icon'); if(icon.classList.contains('shaking')) return;
packBusy = true;
icon.classList.add('shaking');
setTimeout(()=>{ icon.classList.remove('shaking'); icon.classList.add('exploding'); setTimeout(()=>{ document.getElementById('pack-icon-wrap').style.display='none'; revealRandomPlayer(); packBusy = false; },380); },820);
}
function revealRandomPlayer() {
if(!players.length) return;
const weighted=[]; players.forEach(p=>{ const w=p.tier==='BLACK_GOLD'?1:p.tier==='PLATINUM'?2:p.tier==='GOLD_CARD'?3:4; for(let i=0;i<w;i++) weighted.push(p); });
const p=weighted[Math.floor(Math.random()*weighted.length)];
const img=document.getElementById('pack-card-img'); const flip=document.getElementById('pack-card-flip'); const wrap=document.getElementById('pack-card-wrap');
flip.classList.remove('flipped'); img.className='pack-card-img'; flip.querySelectorAll('.pack-holo-overlay').forEach(el=>el.remove());
img.src=p.photo||''; img.style.borderColor=p.tier==='BLACK_GOLD'?'#d4af37':p.tier==='PLATINUM'?'#e0e0e0':'#ffd700';
img.style.boxShadow=p.tier==='BLACK_GOLD'?'0 0 50px rgba(212,175,55,0.9)':p.tier==='PLATINUM'?'0 0 50px rgba(200,200,220,0.8)':'0 0 40px rgba(255,215,0,0.6)';
document.getElementById('pack-card-name').textContent=p.name; document.getElementById('pack-card-info').textContent=`${p.rank} RANK · ${p.position} · ${p.team}`;
if(p.tier==='BLACK_GOLD'||p.tier==='PLATINUM'){const holoColor=p.tier==='BLACK_GOLD'?'rgba(255,215,0,0.4) 44%, rgba(0,243,255,0.4) 50%, rgba(255,150,0,0.35) 56%':'rgba(220,220,220,0.4) 44%, rgba(180,180,255,0.45) 50%, rgba(188,19,254,0.35) 56%';const overlay=document.createElement('div');overlay.className='pack-holo-overlay';overlay.style.cssText=`position:absolute;inset:0;border-radius:16px;pointer-events:none;background:linear-gradient(105deg,transparent 35%,${holoColor},transparent 65%);background-size:200% 100%;animation:holo-sweep 2s ease-in-out infinite;`;img.insertAdjacentElement('afterend',overlay);}
wrap.classList.add('show'); document.getElementById('pack-card-name').style.opacity='0'; document.getElementById('pack-card-info').style.opacity='0';
flip.onclick=(e)=>{ e.stopPropagation(); if(flip.classList.contains('flipped')) return; flip.classList.add('flipped'); setTimeout(()=>{ document.getElementById('pack-card-name').style.opacity='1'; document.getElementById('pack-card-info').style.opacity='1'; },750); };
}
function resetPack() {
document.getElementById('pack-icon-wrap').style.display=''; const icon=document.getElementById('pack-icon'); icon.classList.remove('shaking','exploding');
const flip=document.getElementById('pack-card-flip'); flip.classList.remove('flipped'); flip.style.transform=''; flip.style.animation=''; flip.onclick=null; flip.querySelectorAll('.pack-holo-overlay').forEach(el=>el.remove());
const wrap=document.getElementById('pack-card-wrap'); wrap.classList.remove('show'); document.getElementById('pack-card-name').style.opacity='1'; document.getElementById('pack-card-info').style.opacity='1';
}
// ════════════════════════════════════════
// 戰術板邏輯
// ════════════════════════════════════════
let tacCanvas, tacCtx;
let tacTool = 'pen', tacColor = '#ff3333', tacSize = 4;
let tacDrawing = false, tacStartX, tacStartY;
let tacHistory = [], tacCurrentPath = [];
let tacSnapshot = null;
let tacFieldMode = 'full';
let tacInited = false;
function initTacticalBoard() {
tacCanvas = document.getElementById('tactical-canvas');
const wrap = tacCanvas.parentElement;
tacCanvas.width = wrap.clientWidth;
tacCanvas.height = wrap.clientHeight;
tacCtx = tacCanvas.getContext('2d');
tacHistory = [];
drawBaseballField('full');
if (!tacInited) {
tacInited = true;
tacCanvas.addEventListener('mousedown', tacPointerDown);
tacCanvas.addEventListener('mousemove', tacPointerMove);
tacCanvas.addEventListener('mouseup', tacPointerUp);
tacCanvas.addEventListener('mouseleave', tacPointerUp);
tacCanvas.addEventListener('touchstart', e=>{e.preventDefault();tacPointerDown(e.touches[0]);},{passive:false});
tacCanvas.addEventListener('touchmove', e=>{e.preventDefault();tacPointerMove(e.touches[0]);},{passive:false});
tacCanvas.addEventListener('touchend', e=>{e.preventDefault();tacPointerUp();},{passive:false});
}
}
function drawBaseballField(mode) {
tacFieldMode = mode;
const w = tacCanvas.width, h = tacCanvas.height;
const c = tacCtx;
c.clearRect(0,0,w,h);
if (mode === 'infield') { drawInfieldClose(w, h, c); } else { drawFullField(w, h, c); }
tacSnapshot = c.getImageData(0,0,w,h);
tacHistory = [];
}
function drawStickFigure(c, x, y, sz, color) {
color = color || '#fff';
c.strokeStyle = color; c.fillStyle = color; c.lineWidth = Math.max(1.5, sz * 0.18);
c.beginPath(); c.arc(x, y, sz * 0.22, 0, Math.PI*2); c.fill();
c.beginPath(); c.moveTo(x, y + sz*0.22); c.lineTo(x, y + sz*0.7); c.stroke();
c.beginPath(); c.moveTo(x - sz*0.3, y + sz*0.38); c.lineTo(x + sz*0.3, y + sz*0.38); c.stroke();
c.beginPath(); c.moveTo(x, y + sz*0.7); c.lineTo(x - sz*0.25, y + sz); c.stroke();
c.beginPath(); c.moveTo(x, y + sz*0.7); c.lineTo(x + sz*0.25, y + sz); c.stroke();
}
function drawFielderLabel(c, x, y, label, sz) {
drawStickFigure(c, x, y, sz, '#fff');
const labelY = y + sz * 1.22;
c.font = `bold ${Math.max(10, sz*0.85)}px Noto Sans TC,sans-serif`;
c.textAlign = 'center'; c.textBaseline = 'top';
const tw = c.measureText(label).width;
c.fillStyle = 'rgba(0,0,0,0.65)'; c.fillRect(x - tw/2 - 4, labelY - 1, tw + 8, Math.max(12, sz*0.9) + 2);
c.fillStyle = '#fff'; c.fillText(label, x, labelY);
c.textAlign = 'left'; c.textBaseline = 'alphabetic';
}
function drawFullField(w, h, c) {
const cx = w / 2;
const homeY = h * 0.88;
const bd = Math.min(w, h) * 0.30;
const baseH = { x: cx, y: homeY };
const base1 = { x: cx + bd, y: homeY - bd };
const base2 = { x: cx, y: homeY - bd*2 };
const base3 = { x: cx - bd, y: homeY - bd };
c.fillStyle = '#6abf40';
c.fillRect(0, 0, w, h);
const foulLineLen = Math.max(w, h) * 1.2;
const arcR = bd * 2.15;
const leftAngle = Math.atan2(base3.y - baseH.y, base3.x - baseH.x) + Math.PI;
const rightAngle = Math.atan2(base1.y - baseH.y, base1.x - baseH.x);
c.save();
c.beginPath();
c.moveTo(baseH.x, baseH.y);
c.lineTo(baseH.x + Math.cos(leftAngle + Math.PI) * foulLineLen, baseH.y + Math.sin(leftAngle + Math.PI) * foulLineLen);
c.arc(baseH.x, baseH.y, foulLineLen, leftAngle + Math.PI, rightAngle, false);
c.closePath();
c.fillStyle = '#5aaf30';
c.fill();
c.restore();
c.save();
c.beginPath();
c.moveTo(baseH.x, baseH.y);
c.lineTo(baseH.x - foulLineLen, baseH.y - foulLineLen);
c.arc(baseH.x, baseH.y, arcR, Math.PI * 1.25, Math.PI * 1.75, false);
c.lineTo(baseH.x, baseH.y);
c.closePath();
c.fillStyle = '#6abf40';
c.fill();
c.restore();
c.beginPath(); c.moveTo(baseH.x, baseH.y); c.lineTo(base1.x, base1.y); c.lineTo(base2.x, base2.y); c.lineTo(base3.x, base3.y); c.closePath();
c.fillStyle = '#d4622a'; c.fill(); c.strokeStyle = '#000'; c.lineWidth = 2; c.stroke();
c.beginPath(); c.arc(baseH.x, baseH.y, arcR, Math.PI * 1.22, Math.PI * 1.78, false); c.strokeStyle = '#000'; c.lineWidth = 3; c.stroke();
c.strokeStyle='#000'; c.lineWidth=2;
const lx = baseH.x + Math.cos(Math.PI * 1.25) * foulLineLen;
const ly = baseH.y + Math.sin(Math.PI * 1.25) * foulLineLen;
c.beginPath(); c.moveTo(baseH.x, baseH.y); c.lineTo(lx, ly); c.stroke();
const rx = baseH.x + Math.cos(Math.PI * 1.75) * foulLineLen;
const ry = baseH.y + Math.sin(Math.PI * 1.75) * foulLineLen;
c.beginPath(); c.moveTo(baseH.x, baseH.y); c.lineTo(rx, ry); c.stroke();
const pitchX = cx, pitchY = homeY - bd;
c.beginPath(); c.ellipse(pitchX, pitchY, bd*0.13, bd*0.11, 0, 0, Math.PI*2); c.fillStyle = '#c45820'; c.fill(); c.strokeStyle='#000'; c.lineWidth=1; c.stroke();
c.fillStyle='#fff'; c.fillRect(pitchX-bd*0.06, pitchY-bd*0.025, bd*0.12, bd*0.05);
c.strokeStyle='#fff'; c.lineWidth=2;
[[baseH,base1],[base1,base2],[base2,base3],[base3,baseH]].forEach(([a,b])=>{ c.beginPath(); c.moveTo(a.x,a.y); c.lineTo(b.x,b.y); c.stroke(); });
c.beginPath(); c.arc(baseH.x, baseH.y, bd*0.22, Math.PI, 0, false); c.strokeStyle='rgba(0,0,0,0.5)'; c.lineWidth=1.5; c.setLineDash([4,4]); c.stroke(); c.setLineDash([]);
const bsz_full = Math.max(9, bd * 0.075);
drawFieldBase(c, base2.x, base2.y, bsz_full); drawFieldBase(c, base1.x, base1.y, bsz_full); drawFieldBase(c, base3.x, base3.y, bsz_full);
drawHomeBase(c, baseH.x, baseH.y, bsz_full * 1.1);
// ── 守備位置人形標記 ──
const fsz = Math.max(12, bd * 0.11);
drawFielderLabel(c, cx, homeY - bd*2.75, '中外野手', fsz);
drawFielderLabel(c, cx + bd*1.45, homeY - bd*2.15, '右外野手', fsz);
drawFielderLabel(c, cx - bd*1.45, homeY - bd*2.15, '左外野手', fsz);
drawFielderLabel(c, cx + bd*0.32, homeY - bd*1.85, '二壘手', fsz);
drawFielderLabel(c, cx - bd*0.32, homeY - bd*1.85, '游擊手', fsz);
drawFielderLabel(c, cx + bd*0.88, homeY - bd*1.48, '一壘手', fsz);
drawFielderLabel(c, cx - bd*0.88, homeY - bd*1.48, '三壘手', fsz);
drawFielderLabel(c, pitchX, pitchY, '投手', fsz);
drawFielderLabel(c, cx, homeY + bd*0.08, '捕手', fsz);
}
function drawInfieldClose(w, h, c) {
const cx = w / 2; const homeY = h * 0.88; const bd = Math.min(w, h) * 0.38;
const baseH = { x: cx, y: homeY }; const base1 = { x: cx + bd, y: homeY - bd };
const base2 = { x: cx, y: homeY - bd*2 }; const base3 = { x: cx - bd, y: homeY - bd };
c.fillStyle = '#6abf40'; c.fillRect(0,0,w,h);
const foulLineLen = Math.max(w,h)*1.2;
c.strokeStyle='#000'; c.lineWidth=2;
c.beginPath(); c.moveTo(baseH.x,baseH.y); c.lineTo(baseH.x+Math.cos(Math.PI*1.25)*foulLineLen, baseH.y+Math.sin(Math.PI*1.25)*foulLineLen); c.stroke();
c.beginPath(); c.moveTo(baseH.x,baseH.y); c.lineTo(baseH.x+Math.cos(Math.PI*1.75)*foulLineLen, baseH.y+Math.sin(Math.PI*1.75)*foulLineLen); c.stroke();
c.beginPath(); c.moveTo(baseH.x, baseH.y); c.lineTo(base1.x, base1.y); c.lineTo(base2.x, base2.y); c.lineTo(base3.x, base3.y); c.closePath();
c.fillStyle='#d4622a'; c.fill(); c.strokeStyle='#000'; c.lineWidth=2; c.stroke();
const pitchX=cx, pitchY=homeY-bd;
c.beginPath(); c.ellipse(pitchX, pitchY, bd*0.13, bd*0.1, 0, 0, Math.PI*2); c.fillStyle='#c45820'; c.fill(); c.strokeStyle='#000'; c.lineWidth=1; c.stroke();
c.fillStyle='#fff'; c.fillRect(pitchX-bd*0.07, pitchY-bd*0.03, bd*0.14, bd*0.06);
c.strokeStyle='#fff'; c.lineWidth=2;
[[baseH,base1],[base1,base2],[base2,base3],[base3,baseH]].forEach(([a,b])=>{ c.beginPath(); c.moveTo(a.x,a.y); c.lineTo(b.x,b.y); c.stroke(); });
c.beginPath(); c.arc(baseH.x, baseH.y, bd*0.22, Math.PI, 0, false); c.strokeStyle='rgba(0,0,0,0.5)'; c.lineWidth=1.5; c.setLineDash([4,4]); c.stroke(); c.setLineDash([]);
const bsz = Math.max(11, bd * 0.09);
drawFieldBase(c, base2.x, base2.y, bsz); drawFieldBase(c, base1.x, base1.y, bsz); drawFieldBase(c, base3.x, base3.y, bsz);
drawHomeBase(c, baseH.x, baseH.y, bsz*1.1);
const ifsz = Math.max(11, bd * 0.09);
drawFielderLabel(c, cx + bd*0.32, homeY - bd*1.85, '二壘手', ifsz);
drawFielderLabel(c, cx - bd*0.32, homeY - bd*1.85, '游擊手', ifsz);
drawFielderLabel(c, cx + bd*0.88, homeY - bd*1.48, '一壘手', ifsz);
drawFielderLabel(c, cx - bd*0.88, homeY - bd*1.48, '三壘手', ifsz);
drawFielderLabel(c, pitchX, pitchY, '投手', ifsz);
drawFielderLabel(c, cx, homeY + bd*0.08, '捕手', ifsz);
}
function drawFieldBase(c, x, y, sz) {
c.save(); c.translate(x, y); c.rotate(Math.PI / 4);
c.fillStyle = '#e07820'; c.fillRect(-sz*1.15, -sz*1.15, sz*2.3, sz*2.3); c.strokeStyle = '#000'; c.lineWidth = 1.5; c.strokeRect(-sz*1.15, -sz*1.15, sz*2.3, sz*2.3);
c.fillStyle = '#fff'; c.fillRect(-sz*0.55, -sz*0.55, sz*1.1, sz*1.1); c.strokeStyle = 'rgba(0,0,0,0.4)'; c.lineWidth = 1; c.strokeRect(-sz*0.55, -sz*0.55, sz*1.1, sz*1.1);
c.restore();
}
function drawHomeBase(c, x, y, sz) {
c.save(); c.translate(x, y); c.rotate(Math.PI / 4);
c.fillStyle = '#cc2200'; c.fillRect(-sz*1.15, -sz*1.15, sz*2.3, sz*2.3); c.strokeStyle = '#000'; c.lineWidth = 1.5; c.strokeRect(-sz*1.15, -sz*1.15, sz*2.3, sz*2.3);
c.fillStyle = '#fff'; c.fillRect(-sz*0.55, -sz*0.55, sz*1.1, sz*1.1); c.strokeStyle = 'rgba(0,0,0,0.4)'; c.lineWidth = 1; c.strokeRect(-sz*0.55, -sz*0.55, sz*1.1, sz*1.1);
c.restore();
}
function getTacPos(e) {
const rect = tacCanvas.getBoundingClientRect();
return { x:(e.clientX-rect.left)*(tacCanvas.width/rect.width), y:(e.clientY-rect.top)*(tacCanvas.height/rect.height) };
}
// ── 文字工具:浮動預覽 ──
let tacFloatingText = null; // { text, x, y }
function startFloatingText() {
// 先跳出輸入框取得文字
const wrap = document.getElementById('tac-text-input-wrap');
wrap.style.display = 'flex';
document.getElementById('tac-text-input').value = '';
document.getElementById('tac-text-input').focus();
}
function confirmFloatingText() {
const val = document.getElementById('tac-text-input').value.trim();
document.getElementById('tac-text-input-wrap').style.display = 'none';
if(!val) { tacFloatingText = null; return; }
tacFloatingText = { text: val, x: tacCanvas.width/2, y: tacCanvas.height/2 };
tacCanvas.style.cursor = 'move';
renderFloatingText();
}
function renderFloatingText() {
if(!tacFloatingText) return;
// 重繪底圖再疊文字預覽
if(tacSnapshot) tacCtx.putImageData(tacSnapshot, 0, 0);
const fontSize = tacSize*4+12;
tacCtx.font = `bold ${fontSize}px Noto Sans TC,sans-serif`;
tacCtx.shadowBlur = 0;
tacCtx.globalAlpha = 0.7;
tacCtx.strokeStyle = '#fff';
tacCtx.lineWidth = 4;
tacCtx.strokeText(tacFloatingText.text, tacFloatingText.x, tacFloatingText.y);
tacCtx.fillStyle = '#000';
tacCtx.fillText(tacFloatingText.text, tacFloatingText.x, tacFloatingText.y);
tacCtx.globalAlpha = 1;
}
function placeFloatingText(x, y) {
if(!tacFloatingText) return;
tacFloatingText.x = x; tacFloatingText.y = y;
// 正式繪製
if(tacSnapshot) tacCtx.putImageData(tacSnapshot, 0, 0);
const fontSize = tacSize*4+12;
tacCtx.font = `bold ${fontSize}px Noto Sans TC,sans-serif`;
tacCtx.shadowBlur = 0;
tacCtx.strokeStyle = '#fff';
tacCtx.lineWidth = 4;
tacCtx.strokeText(tacFloatingText.text, x, y);
tacCtx.fillStyle = '#000';
tacCtx.fillText(tacFloatingText.text, x, y);
saveHistory();
tacFloatingText = null;
tacCanvas.style.cursor = 'crosshair';
}
function tacPointerDown(e) {
const {x,y} = getTacPos(e);
// 若有浮動文字,點擊確認放置
if(tacFloatingText) { placeFloatingText(x, y); return; }
tacDrawing=true; tacStartX=x; tacStartY=y; tacCurrentPath=[{x,y}];
if(tacTool==='text'){
tacDrawing=false;
startFloatingText();
return;
}
if(['batter','runner','fielder','ball','cone'].includes(tacTool)){ tacDrawing=false; drawBBShape(x,y); saveHistory(); return; }
if(tacTool==='pen'){ tacCtx.beginPath(); tacCtx.moveTo(x,y); tacCtx.strokeStyle=tacColor; tacCtx.lineWidth=tacSize; tacCtx.lineCap='round'; tacCtx.lineJoin='round'; tacCtx.shadowColor=tacColor; tacCtx.shadowBlur=tacSize*1.5; }
}
function tacPointerMove(e) {
// 浮動文字跟隨滑鼠
if(tacFloatingText) {
const {x,y} = getTacPos(e);
tacFloatingText.x = x; tacFloatingText.y = y;
renderFloatingText();
return;
}
if(!tacDrawing) return;
const {x,y}=getTacPos(e); tacCurrentPath.push({x,y});
if(tacTool==='pen'){ tacCtx.lineTo(x,y); tacCtx.stroke(); tacCtx.shadowBlur=0; }
else if(tacTool==='erase'){ tacCtx.clearRect(x-tacSize*3,y-tacSize*3,tacSize*6,tacSize*6); }
else if(tacTool==='arrow'){ tacCtx.putImageData(tacSnapshot,0,0); drawArrow(tacStartX,tacStartY,x,y,tacColor,tacSize); }
}
function tacPointerUp() {
if(!tacDrawing) return; tacDrawing=false;
if(tacTool==='arrow'&&tacCurrentPath.length>1){ const last=tacCurrentPath[tacCurrentPath.length-1]; drawArrow(tacStartX,tacStartY,last.x,last.y,tacColor,tacSize); }
tacCtx.shadowBlur=0; saveHistory();
}
function drawArrow(x1,y1,x2,y2,color,size) {
const c=tacCtx, headLen=size*5+10, angle=Math.atan2(y2-y1,x2-x1);
c.beginPath(); c.moveTo(x1,y1); c.lineTo(x2,y2); c.strokeStyle=color; c.lineWidth=size; c.lineCap='round'; c.shadowColor=color; c.shadowBlur=size*1.5; c.stroke();
c.beginPath(); c.moveTo(x2,y2); c.lineTo(x2-headLen*Math.cos(angle-Math.PI/6),y2-headLen*Math.sin(angle-Math.PI/6)); c.lineTo(x2-headLen*Math.cos(angle+Math.PI/6),y2-headLen*Math.sin(angle+Math.PI/6)); c.closePath(); c.fillStyle=color; c.fill(); c.shadowBlur=0;
}
function drawBBShape(x,y) {
const c=tacCtx; const r=tacSize*3+13; c.shadowBlur=14;
if(tacTool==='batter'){ c.shadowColor='#ffd700'; c.beginPath(); c.arc(x,y,r,0,Math.PI*2); c.fillStyle='rgba(220,160,0,0.9)'; c.fill(); c.strokeStyle='#fff'; c.lineWidth=2; c.stroke(); c.fillStyle='#000'; c.font=`bold ${r}px sans-serif`; c.textAlign='center'; c.textBaseline='middle'; c.fillText('打',x,y); }
else if(tacTool==='runner'){ c.shadowColor='#00aaff'; c.beginPath(); c.arc(x,y,r,0,Math.PI*2); c.fillStyle='rgba(0,100,220,0.9)'; c.fill(); c.strokeStyle='#00f3ff'; c.lineWidth=2; c.stroke(); c.fillStyle='#fff'; c.font=`bold ${r}px sans-serif`; c.textAlign='center'; c.textBaseline='middle'; c.fillText('跑',x,y); }
else if(tacTool==='fielder'){ c.shadowColor='#ff4444'; c.beginPath(); c.arc(x,y,r,0,Math.PI*2); c.fillStyle='rgba(200,20,20,0.9)'; c.fill(); c.strokeStyle='#ff8888'; c.lineWidth=2; c.stroke(); c.fillStyle='#fff'; c.font=`bold ${r}px sans-serif`; c.textAlign='center'; c.textBaseline='middle'; c.fillText('守',x,y); }
else if(tacTool==='ball'){
// 橘色樂樂棒球
const br = r * 0.75;
// 球體漸層
const grad = c.createRadialGradient(x - br*0.3, y - br*0.3, br*0.05, x, y, br);
grad.addColorStop(0, '#ffb060');
grad.addColorStop(0.5, '#ff7a20');
grad.addColorStop(1, '#cc4a00');
c.beginPath(); c.arc(x, y, br, 0, Math.PI*2);
c.fillStyle = grad; c.fill();
c.strokeStyle = 'rgba(0,0,0,0.25)'; c.lineWidth = 1.5; c.stroke();
// 縫線(橘色球上的深色壓紋線條)
c.strokeStyle = 'rgba(150,40,0,0.5)'; c.lineWidth = 1.2;
// 上弧縫線
c.beginPath(); c.arc(x, y - br*0.15, br*0.7, Math.PI*1.15, Math.PI*1.85, false); c.stroke();
// 下弧縫線
c.beginPath(); c.arc(x, y + br*0.15, br*0.7, Math.PI*0.15, Math.PI*0.85, false); c.stroke();
}
else if(tacTool==='cone'){ c.shadowColor='#ff8800'; c.beginPath(); c.moveTo(x,y-r); c.lineTo(x+r*0.8,y+r*0.6); c.lineTo(x-r*0.8,y+r*0.6); c.closePath(); c.fillStyle='#ff8800'; c.fill(); c.strokeStyle='#ffcc00'; c.lineWidth=2; c.stroke(); }
c.shadowBlur=0; c.textAlign='left'; c.textBaseline='alphabetic';
}
function saveHistory() { tacHistory.push(tacCtx.getImageData(0,0,tacCanvas.width,tacCanvas.height)); if(tacHistory.length>30) tacHistory.shift(); tacSnapshot=tacCtx.getImageData(0,0,tacCanvas.width,tacCanvas.height); }
function undoTactical() { if(tacHistory.length===0){drawBaseballField(tacFieldMode);return;} tacHistory.pop(); if(tacHistory.length>0){ tacCtx.putImageData(tacHistory[tacHistory.length-1],0,0); tacSnapshot=tacCtx.getImageData(0,0,tacCanvas.width,tacCanvas.height); } else drawBaseballField(tacFieldMode); }
function clearTactical() { tacHistory=[]; drawBaseballField(tacFieldMode); }
function setTacTool(tool) { tacTool=tool; document.querySelectorAll('.tac-tool-btn,.tac-shape-btn').forEach(b=>b.classList.remove('active')); const btn=document.getElementById('tool-'+tool); if(btn) btn.classList.add('active'); if(tacCanvas) tacCanvas.style.cursor=tool==='erase'?'cell':tool==='text'?'text':'crosshair'; }
function setTacColor(color, el) { tacColor=color; document.querySelectorAll('.tac-color-btn').forEach(b=>b.classList.remove('active')); if(el) el.classList.add('active'); }
function setTacSize(size, el) { tacSize=size; document.querySelectorAll('.tac-size-btn').forEach(b=>b.classList.remove('active')); if(el) el.classList.add('active'); }
// ════════════════════════════════════════
// 計分板邏輯 - 全自動版
// ════════════════════════════════════════
const BB_INN = 7;
let bbHomeScore=0, bbAwayScore=0;
let bbCurInn=1;
let bbInnScores={ home:new Array(BB_INN).fill(null), away:new Array(BB_INN).fill(null) };
let bbStrikes=0, bbOuts=0;
let bbBases={ '1st':null, '2nd':null, '3rd':null };
let bbLog=[];
let bbLineupSide='home';
let bbCurSide='home';
let bbLineup={
home: Array.from({length:9},(_,i)=>({name:`第${i+1}棒`})),
away: Array.from({length:9},(_,i)=>({name:`第${i+1}棒`}))
};
let bbBatterIdx={ home:0, away:0 };
let bbLineupTeam={ home:'', away:'' };
let _bbBT={};
function bbInitScoreboard() {
bbPopulateTeamSel();
bbRenderBSO();
bbRenderBases();
bbRenderLineup();
bbRenderInningTable();
bbUpdateBatterDisplay();
bbUpdateSideLabel();
}
function bbPopulateTeamSel() {
const sel=document.getElementById('bb-team-sel'); if(!sel) return;
sel.innerHTML='<option value="">-- 選隊伍 --</option>';
[...new Set(players.map(p=>p.team))].sort().forEach(t=>{
const o=document.createElement('option'); o.value=t; o.textContent=t;
if(t===bbLineupTeam[bbLineupSide]) o.selected=true;
sel.appendChild(o);
});
}
function bbOnTeamChange() {
const team=document.getElementById('bb-team-sel').value;
bbLineupTeam[bbLineupSide]=team;
if(team){
const tp=players.filter(p=>p.team===team);
bbLineup[bbLineupSide]=Array.from({length:9},(_,i)=>({name:tp[i]?.name||`第${i+1}棒`}));
bbBatterIdx[bbLineupSide]=0;
const nameInput=document.getElementById(bbLineupSide==='home'?'bb-home-name':'bb-away-name');
if(nameInput) nameInput.value=team;
}
bbRenderLineup(); bbUpdateBatterDisplay(); bbRenderInningTable(); bbUpdateSideLabel();
}
function bbDoPlay(type) {
const batter=bbGetBatterName();
if(type==='hr') bbHandleHR(batter);
else if(type==='1b') bbHandleHit(1,'一壘打',batter);
else if(type==='2b') bbHandleHit(2,'二壘打',batter);
else if(type==='3b') bbHandleHit(3,'三壘打',batter);
else if(type==='bb') bbHandleBB(batter);
else if(type==='e') bbHandleHit(1,'失誤 E',batter);
else if(type==='k') bbHandleOut(batter,'三振 K');
else if(type==='go') bbHandleOut(batter,'滾地 Out');
else if(type==='fo') bbHandleOut(batter,'高飛 Out');
else if(type==='dp') bbHandleDP(batter);
bbRenderBSO(); bbRenderBases(); bbRenderInningTable(); bbRenderLineup(); bbUpdateBatterDisplay();
}
function bbPushBases(n, batter) {
const scored=[];
for(const b of ['3rd','2nd','1st']){
if(!bbBases[b]) continue;
const dest=bbCalcDest(b,n);
if(dest==='home'){ scored.push(bbBases[b]); bbBases[b]=null; }
else { bbBases[dest]=bbBases[b]; bbBases[b]=null; }
}
const batterDest=bbCalcDest('home',n);
if(batterDest!=='home') bbBases[batterDest]=batter;
return scored;
}
function bbHandleBB(batter) {
const scored=[];
if(bbBases['1st'] && bbBases['2nd'] && bbBases['3rd']){
scored.push(bbBases['3rd']);
bbBases['3rd']=bbBases['2nd'];
bbBases['2nd']=bbBases['1st'];
bbBases['1st']=batter;
} else if(bbBases['1st'] && bbBases['2nd']){
bbBases['3rd']=bbBases['2nd'];
bbBases['2nd']=bbBases['1st'];
bbBases['1st']=batter;
} else if(bbBases['1st']){
bbBases['2nd']=bbBases['1st'];
bbBases['1st']=batter;
} else {
bbBases['1st']=batter;
}
scored.forEach(r=>{ bbDoScore(); bbAddLog('score',r,'四壞保送後得分 🏠'); bbShowBanner('score',r+' 得分!'); });
bbAddLog('hit',batter,'四壞保送 BB');
bbNextBatter();
}
function bbCalcDest(from, n) {
const map={'home':0,'1st':1,'2nd':2,'3rd':3};
const back=[null,'1st','2nd','3rd','home'];
const pos=map[from]; const np=pos+n;
return np>=4?'home':(back[np]||'home');
}
function bbHandleHit(n, label, batter) {
const scored=bbPushBases(n,batter);
scored.forEach(r=>{ bbDoScore(); bbAddLog('score',r,'得分回本壘 🏠'); bbShowBanner('score',r+' 得分!'); });
bbAddLog('hit',batter,label);
bbNextBatter();
}
function bbHandleHR(batter) {
const runners=[];
for(const b of ['3rd','2nd','1st']){ if(bbBases[b]){runners.push(bbBases[b]); bbBases[b]=null;} }
runners.push(batter);
runners.forEach(()=>bbDoScore());
bbAddLog('hr',batter,`全壘打 🏠 +${runners.length}分`);
bbShowBanner('hr');
bbNextBatter();
}
function bbHandleOut(batter, label) {
bbOuts++;
bbAddLog('out',batter,label);
bbShowBanner('out',label);
if(bbOuts>=3) bbSideRetired();
else bbNextBatter();
}
function bbHandleDP(batter) {
let removed=false;
for(const b of ['3rd','2nd','1st']){
if(bbBases[b]&&!removed){ bbAddLog('out',bbBases[b],'雙殺前位出局'); bbBases[b]=null; removed=true; }
}
bbOuts=Math.min(3,bbOuts+2);
bbAddLog('out',batter,'雙殺 DP');
bbShowBanner('out','雙殺!');
if(bbOuts>=3) bbSideRetired();
else bbNextBatter();
}
function bbSideRetired() {
bbAddLog('sys','','三出局!攻守交換');
bbShowBanner('side');
setTimeout(()=>{
bbOuts=0; bbStrikes=0;
bbBases={'1st':null,'2nd':null,'3rd':null};
const prevSide=bbCurSide;
bbCurSide=bbCurSide==='home'?'away':'home';
// 客隊打完換回主隊 → 自動進入下一局
if(prevSide==='away' && bbCurInn<BB_INN){
bbCurInn++;
for(let i=1;i<=BB_INN;i++) document.getElementById('inning-btn-'+i)?.classList.toggle('active',i===bbCurInn);
bbAddLog('sys','',`第 ${bbCurInn===BB_INN?'延長':bbCurInn} 局開始`);
}
bbLineupSide=bbCurSide;
document.getElementById('lineup-tab-home').classList.toggle('active',bbLineupSide==='home');
document.getElementById('lineup-tab-away').classList.toggle('active',bbLineupSide==='away');
const sel=document.getElementById('bb-team-sel');
if(sel) sel.value=bbLineupTeam[bbLineupSide]||'';
bbRenderBSO(); bbRenderBases(); bbRenderLineup(); bbRenderInningTable(); bbUpdateBatterDisplay(); bbUpdateSideLabel();
},1800);
}
function bbDoScore() {
if(bbCurSide==='home'){
bbHomeScore++;
bbInnScores.home[bbCurInn-1]=(bbInnScores.home[bbCurInn-1]||0)+1;
document.getElementById('bb-home-score').textContent=bbHomeScore;
bbPopScore('bb-home-score');
} else {
bbAwayScore++;
bbInnScores.away[bbCurInn-1]=(bbInnScores.away[bbCurInn-1]||0)+1;
document.getElementById('bb-away-score').textContent=bbAwayScore;
bbPopScore('bb-away-score');
}
}
function bbManualScore(side,delta) {
if(side==='home'){ bbHomeScore=Math.max(0,bbHomeScore+delta); document.getElementById('bb-home-score').textContent=bbHomeScore; if(delta>0)bbPopScore('bb-home-score'); }
else { bbAwayScore=Math.max(0,bbAwayScore+delta); document.getElementById('bb-away-score').textContent=bbAwayScore; if(delta>0)bbPopScore('bb-away-score'); }
bbRenderInningTable();
}
function bbPopScore(id){ const el=document.getElementById(id); el.classList.remove('popping'); void el.offsetWidth; el.classList.add('popping'); }
function bbGetBatterName(){ return bbLineup[bbCurSide][bbBatterIdx[bbCurSide]]?.name||`第${bbBatterIdx[bbCurSide]+1}棒`; }
function bbNextBatter(){ bbStrikes=0; bbBatterIdx[bbCurSide]=(bbBatterIdx[bbCurSide]+1)%9; bbUpdateBatterDisplay(); }
function bbManualAdvance(){ bbBatterIdx[bbLineupSide]=(bbBatterIdx[bbLineupSide]+1)%9; bbRenderLineup(); bbUpdateBatterDisplay(); }
function bbUpdateBatterDisplay(){ const el=document.getElementById('bb-batter-display'); if(el) el.textContent=bbGetBatterName(); }
function bbUpdateSideLabel(){
const el=document.getElementById('bb-side-label'); if(!el) return;
const name=bbCurSide==='home'?(document.getElementById('bb-home-name').value||'主隊'):(document.getElementById('bb-away-name').value||'客隊');
el.textContent=name+' 攻擊中';
el.style.color=bbCurSide==='home'?'rgba(0,243,255,0.5)':'rgba(255,180,0,0.5)';
}
function bbSwitchLineup(side){
bbLineupSide=side;
document.getElementById('lineup-tab-home').classList.toggle('active',side==='home');
document.getElementById('lineup-tab-away').classList.toggle('active',side==='away');
const sel=document.getElementById('bb-team-sel'); if(sel) sel.value=bbLineupTeam[side]||'';
bbRenderLineup(); bbUpdateBatterDisplay();
}
function bbRenderBSO(){
const sl=document.getElementById('bb-strike-lights');
const ol=document.getElementById('bb-out-lights');
if(!sl||!ol) return;
sl.innerHTML='';
for(let i=1;i<=3;i++){
const d=document.createElement('div'); d.className='bb-light'+(i<=bbStrikes?' on-strike':'');
d.title=`好球 ${i}`; d.onclick=(()=>{const n=i; return ()=>{ bbStrikes=(bbStrikes>=n?n-1:n); if(bbStrikes>=3){bbHandleOut(bbGetBatterName(),'三振');} bbRenderBSO(); };})(i);
sl.appendChild(d);
}
ol.innerHTML='';
for(let i=1;i<=3;i++){
const d=document.createElement('div'); d.className='bb-light'+(i<=bbOuts?' on-out':'');
d.title=`出局 ${i}`; d.onclick=(()=>{const n=i; return ()=>{ bbOuts=(bbOuts>=n?n-1:n); bbRenderBSO(); if(bbOuts>=3)bbSideRetired(); };})(i);
ol.appendChild(d);
}
}
function bbRenderBases(){
['1st','2nd','3rd'].forEach(b=>{
const el=document.getElementById('base-'+b);
if(el){ el.classList.toggle('on',!!bbBases[b]); el.title=bbBases[b]||b; }
});
}
function bbSetInning(n){
bbCurInn=n;
for(let i=1;i<=7;i++) document.getElementById('inning-btn-'+i).classList.toggle('active',i===n);
bbAddLog('sys','',`第 ${n===7?'延長':n} 局開始`);
bbRenderInningTable();
}
function bbRenderInningTable(){
const tbody=document.getElementById('bb-inning-tbody'); if(!tbody) return;
tbody.innerHTML='';
['home','away'].forEach(side=>{
const tr=document.createElement('tr');
const name=side==='home'?(document.getElementById('bb-home-name')?.value||'主隊'):(document.getElementById('bb-away-name')?.value||'客隊');
const total=side==='home'?bbHomeScore:bbAwayScore;
let html=`<td class="team-cell">${escapeHtml(name)}</td>`;
for(let i=0;i<BB_INN;i++){
const v=bbInnScores[side][i];
const isCur=i+1===bbCurInn;
html+=`<td class="bb-inn-cell${isCur?' cur':''}" onclick="bbEditCell('${side}',${i})">${v===null?'-':v}</td>`;
}
html+=`<td class="rhe-cell ${side==='home'?'home-rhe':'away-rhe'}" style="border-left:2px solid rgba(255,255,255,0.1);">${total}</td>`;
tr.innerHTML=html; tbody.appendChild(tr);
});
}
function bbEditCell(side,i){
const cur=bbInnScores[side][i];
const v=prompt(`修改第${i+1===7?'延長':i+1}局(${side==='home'?'主隊':'客隊'})得分:`,cur===null?0:cur);
if(v===null) return;
const n=parseInt(v); if(isNaN(n)||n<0) return;
bbInnScores[side][i]=n;
bbHomeScore=bbInnScores.home.reduce((a,v)=>a+(v||0),0);
bbAwayScore=bbInnScores.away.reduce((a,v)=>a+(v||0),0);
document.getElementById('bb-home-score').textContent=bbHomeScore;
document.getElementById('bb-away-score').textContent=bbAwayScore;
bbRenderInningTable();
}
function bbRenderLineup(){
const list=document.getElementById('bb-lineup-list'); if(!list) return;
list.innerHTML='';
const cur=bbBatterIdx[bbLineupSide];
bbLineup[bbLineupSide].forEach((p,i)=>{
const row=document.createElement('div');
row.className='bb-lineup-row'+(i===cur?' batting-now':'');
row.onclick=()=>{ bbBatterIdx[bbLineupSide]=i; bbRenderLineup(); bbUpdateBatterDisplay(); };
row.innerHTML=`<span class="bb-lineup-num">${i+1}</span><span class="bb-lineup-name">${escapeHtml(p.name)}</span><span class="bb-lineup-arrow">🏏</span>`;
list.appendChild(row);
});
}
function bbAddLog(type, player, result){
const inn=bbCurInn===7?'EX':bbCurInn;
const ord=bbBatterIdx[bbCurSide]+1;
bbLog.unshift({type,player,result,outs:bbOuts,inn,ord});
bbRenderLog();
}
function bbRenderLog(){
const list=document.getElementById('bb-log-list'); if(!list) return;
if(!bbLog.length){ list.innerHTML='<div style="text-align:center;font-family:\'Orbitron\';font-size:0.62rem;color:rgba(255,255,255,0.15);padding:16px;">尚無紀錄</div>'; return; }
list.innerHTML=bbLog.slice(0,60).map(item=>{
if(item.type==='sys') return `<div class="bb-log-item log-sys"><span class="bb-log-inn">${item.inn}</span><div class="bb-log-body"><span class="bb-log-tag tag-sys">${escapeHtml(item.result)}</span></div></div>`;
const tagCls={'hit':'tag-hit','score':'tag-score','hr':'tag-hr','out':'tag-out'}[item.type]||'tag-sys';
const lcls={'hit':'log-hit','score':'log-score','hr':'log-hr','out':'log-out'}[item.type]||'log-sys';
const dots=[1,2,3].map(i=>`<div class="bb-out-dot${i<=item.outs?' on':''}"></div>`).join('');
return `<div class="bb-log-item ${lcls}"><span class="bb-log-inn">${item.inn}</span><div class="bb-log-body"><div class="bb-log-player"><span style="font-size:0.58rem;color:rgba(255,255,255,0.28);margin-right:3px;">${item.ord}棒</span>${escapeHtml(item.player)}</div><span class="bb-log-tag ${tagCls}">${escapeHtml(item.result)}</span><div class="bb-out-dots">${dots}</div></div></div>`;
}).join('');
}
function bbShowBanner(type, msg){
const ids={out:'bb-banner-out',hr:'bb-banner-hr',side:'bb-banner-side',score:'bb-banner-score'};
const el=document.getElementById(ids[type]); if(!el) return;
if(type==='out'||type==='score') el.textContent=msg||'';
el.classList.remove('show'); void el.offsetWidth;
clearTimeout(_bbBT[type]);
el.classList.add('show');
_bbBT[type]=setTimeout(()=>el.classList.remove('show'),1600);
}
function resetScoreboard(){
if(!confirm('確定要重置計分板嗎?')) return;
bbHomeScore=0; bbAwayScore=0;
document.getElementById('bb-home-score').textContent='0';
document.getElementById('bb-away-score').textContent='0';
document.getElementById('bb-home-name').value='主隊';
document.getElementById('bb-away-name').value='客隊';
bbInnScores={home:new Array(BB_INN).fill(null),away:new Array(BB_INN).fill(null)};
bbStrikes=0; bbOuts=0;
bbBases={'1st':null,'2nd':null,'3rd':null};
bbLog=[]; bbBatterIdx={home:0,away:0};
bbCurSide='home'; bbLineupSide='home';
bbCurInn=1;
bbLineupTeam={home:'',away:''};
bbLineup={home:Array.from({length:9},(_,i)=>({name:`第${i+1}棒`})),away:Array.from({length:9},(_,i)=>({name:`第${i+1}棒`}))};
document.getElementById('lineup-tab-home').classList.add('active');
document.getElementById('lineup-tab-away').classList.remove('active');
const sel=document.getElementById('bb-team-sel'); if(sel) sel.value='';
for(let i=1;i<=7;i++) document.getElementById('inning-btn-'+i)?.classList.toggle('active',i===1);
bbRenderBSO(); bbRenderBases(); bbRenderLineup(); bbRenderInningTable(); bbRenderLog();
bbUpdateBatterDisplay(); bbUpdateSideLabel();
}
// ════════════════════════════════════════
// 賽程表邏輯
// ════════════════════════════════════════
let scheduleData = [];
let editingMatchIdx = null;
function openAddMatch() {
editingMatchIdx = null;
document.getElementById('match-modal-title').textContent = '新增賽事';
document.getElementById('m-home').value = '';
document.getElementById('m-away').value = '';
document.getElementById('m-date').value = '';
document.getElementById('m-time').value = '';
document.getElementById('m-venue').value = '';
document.getElementById('m-hscore').value = '';
document.getElementById('m-ascore').value = '';
document.getElementById('match-modal').style.display = 'flex';
}
function openEditMatch(idx) {
editingMatchIdx = idx;
const m = scheduleData[idx];
document.getElementById('match-modal-title').textContent = '編輯賽事';
document.getElementById('m-home').value = m.home || '';
document.getElementById('m-away').value = m.away || '';
document.getElementById('m-date').value = m.date || '';
document.getElementById('m-time').value = m.time || '';
document.getElementById('m-venue').value = m.venue || '';
document.getElementById('m-hscore').value = m.hscore !== null && m.hscore !== undefined ? m.hscore : '';
document.getElementById('m-ascore').value = m.ascore !== null && m.ascore !== undefined ? m.ascore : '';
document.getElementById('match-modal').style.display = 'flex';
}
function closeMatchModal() {
document.getElementById('match-modal').style.display = 'none';
}
function saveMatch() {
const home = document.getElementById('m-home').value.trim();
const away = document.getElementById('m-away').value.trim();
if(!home || !away) { alert('請填寫主隊和客隊名稱'); return; }
const hscore = document.getElementById('m-hscore').value;
const ascore = document.getElementById('m-ascore').value;
const match = {
home, away,
date: document.getElementById('m-date').value,
time: document.getElementById('m-time').value,
venue: document.getElementById('m-venue').value.trim(),
hscore: hscore !== '' ? parseInt(hscore) : null,
ascore: ascore !== '' ? parseInt(ascore) : null,
};
if(editingMatchIdx !== null) {
scheduleData[editingMatchIdx] = match;
} else {
scheduleData.push(match);
}
closeMatchModal();
renderSchedule();
}
function deleteMatch(idx) {
if(!confirm('確定刪除這場賽事?')) return;
scheduleData.splice(idx, 1);
renderSchedule();
}
function renderSchedule() {
const filter = document.getElementById('schedule-filter').value;
const list = document.getElementById('schedule-list');
if(!list) return;
let data = scheduleData.map((m,i) => ({...m, idx:i}));
if(filter === 'done') data = data.filter(m => m.hscore !== null && m.ascore !== null);
if(filter === 'pending') data = data.filter(m => m.hscore === null || m.ascore === null);
if(data.length === 0) {
list.innerHTML = '<div style="text-align:center; font-family:\'Orbitron\'; font-size:0.8rem; color:rgba(255,255,255,0.2); padding:40px;">尚無賽程</div>';
return;
}
// 依日期分組
const groups = {};
data.forEach(m => {
const key = m.date || '日期未定';
if(!groups[key]) groups[key] = [];
groups[key].push(m);
});
list.innerHTML = Object.keys(groups).sort().map(date => {
const label = date === '日期未定' ? '日期未定' : formatDate(date);
const matches = groups[date].map(m => {
const done = m.hscore !== null && m.ascore !== null;
const hWin = done && m.hscore > m.ascore;
const aWin = done && m.ascore > m.hscore;
return `
<div style="display:grid; grid-template-columns:1fr auto 1fr; gap:12px; align-items:center; padding:14px 18px; background:rgba(255,255,255,0.03); border-radius:12px; border:1px solid rgba(255,255,255,0.07); margin-bottom:8px; transition:0.2s;" onmouseover="this.style.borderColor='rgba(0,220,255,0.25)'" onmouseout="this.style.borderColor='rgba(255,255,255,0.07)'">
<div style="display:flex; flex-direction:column; align-items:flex-end; gap:4px;">
<div style="font-family:'Noto Sans TC'; font-weight:900; font-size:1rem; color:${hWin?'#ffd700':'#fff'};">${escapeHtml(m.home)}</div>
${done ? `<div style="font-family:'Orbitron'; font-size:1.8rem; font-weight:900; color:${hWin?'#00f3ff':'rgba(255,255,255,0.4)'};">${m.hscore}</div>` : ''}
</div>
<div style="display:flex; flex-direction:column; align-items:center; gap:6px;">
${done ? '<div style="font-family:\'Orbitron\'; font-size:0.6rem; letter-spacing:2px; color:rgba(255,255,255,0.3);">FINAL</div>' : `<div style="font-family:'Orbitron'; font-size:0.75rem; color:rgba(255,255,255,0.5);">${m.time||'--:--'}</div>`}
<div style="font-family:'Orbitron'; font-size:1rem; color:rgba(255,255,255,0.2);">VS</div>
${m.venue ? `<div style="font-size:0.65rem; color:rgba(255,255,255,0.3); font-family:'Noto Sans TC';">📍${escapeHtml(m.venue)}</div>` : ''}
</div>
<div style="display:flex; flex-direction:column; align-items:flex-start; gap:4px;">
<div style="font-family:'Noto Sans TC'; font-weight:900; font-size:1rem; color:${aWin?'#ffd700':'#fff'};">${escapeHtml(m.away)}</div>
${done ? `<div style="font-family:'Orbitron'; font-size:1.8rem; font-weight:900; color:${aWin?'#ffb300':'rgba(255,255,255,0.4)'};">${m.ascore}</div>` : ''}
</div>
<div style="grid-column:1/4; display:flex; justify-content:flex-end; gap:6px; margin-top:4px;">
<button onclick="openEditMatch(${m.idx})" style="padding:4px 14px; border-radius:20px; border:1px solid rgba(255,255,255,0.15); background:transparent; color:rgba(255,255,255,0.4); font-size:0.65rem; cursor:pointer; font-family:'Orbitron';">✏ 編輯</button>
<button onclick="deleteMatch(${m.idx})" style="padding:4px 14px; border-radius:20px; border:1px solid rgba(255,68,68,0.3); background:transparent; color:rgba(255,100,100,0.6); font-size:0.65rem; cursor:pointer; font-family:'Orbitron';">✕ 刪除</button>
</div>
</div>`;
}).join('');
return `<div style="margin-bottom:20px;">
<div style="font-family:'Orbitron'; font-size:0.7rem; letter-spacing:3px; color:rgba(0,220,255,0.6); margin-bottom:10px; padding-bottom:6px; border-bottom:1px solid rgba(0,220,255,0.15);">📅 ${label}</div>
${matches}
</div>`;
}).join('');
}
function formatDate(dateStr) {
const d = new Date(dateStr);
if(isNaN(d)) return dateStr;
const days = ['日','一','二','三','四','五','六'];
return `${d.getFullYear()}/${String(d.getMonth()+1).padStart(2,'0')}/${String(d.getDate()).padStart(2,'0')} (週${days[d.getDay()]})`;
}
</script>
</body>
</html>
貼到 Apps Script 的 index.html 檔案
6. 部署
部署成 Web App 後,就會拿到可分享網址。
- Apps Script 右上:部署 → 新部署
- 類型選:Web app
- 執行身分:你
- 存取權限:任何人
- 完成後複製網址
💬 評論區
老師們的交流與提問
登入 後即可發表評論