Spaces:
Running
Running
| <html lang="zh-Hant"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Google AI 教育探險</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@400;700&display=swap" rel="stylesheet"> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.7.77/Tone.min.js"></script> | |
| <!-- NEW: Firebase SDKs --> | |
| <script type="module"> | |
| // 載入 Firebase SDK | |
| import { initializeApp } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-app.js"; | |
| import { getAuth, signInAnonymously, signInWithCustomToken } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-auth.js"; | |
| import { getFirestore, setDoc, doc, collection, getDocs, query, where, serverTimestamp, setLogLevel, Timestamp, addDoc } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-firestore.js"; | |
| // 將 SDK 功能掛載到 window 物件,讓下方的非模組 <script> 可以存取 | |
| window.firebaseSDK = { | |
| initializeApp, | |
| getAuth, signInAnonymously, signInWithCustomToken, | |
| getFirestore, setDoc, doc, collection, getDocs, query, where, serverTimestamp, setLogLevel, Timestamp, addDoc | |
| }; | |
| </script> | |
| <style> | |
| body { | |
| font-family: 'Noto Sans TC', sans-serif; | |
| background-color: #f8f9fa; | |
| } | |
| .game-container { | |
| max-width: 800px; | |
| margin: 2rem auto; | |
| background: white; | |
| border-radius: 1.5rem; | |
| box-shadow: 0 10px 30px rgba(0,0,0,0.1); | |
| overflow: hidden; | |
| } | |
| .game-screen { | |
| display: none; | |
| padding: 2.5rem; | |
| animation: fadeIn 0.5s ease-in-out; | |
| position: relative; /* 新增:為了讓愛心定位 */ | |
| } | |
| .game-screen.active { | |
| display: block; | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; transform: translateY(20px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| .pixel-art-explorer { | |
| width: 80px; | |
| height: 80px; | |
| background-image: url('https://placehold.co/80x80/60a5fa/ffffff?text=Explorer'); | |
| image-rendering: pixelated; | |
| border-radius: 50%; | |
| border: 4px solid #fff; | |
| box-shadow: 0 0 10px rgba(0,0,0,0.2); | |
| } | |
| .btn { | |
| padding: 0.75rem 1.5rem; | |
| border-radius: 9999px; | |
| font-weight: bold; | |
| transition: all 0.3s ease; | |
| cursor: pointer; | |
| border: none; | |
| } | |
| .btn-primary { | |
| background-color: #4285F4; | |
| color: white; | |
| } | |
| .btn-primary:hover { | |
| background-color: #357ae8; | |
| transform: translateY(-2px); | |
| box-shadow: 0 4px 10px rgba(66, 133, 244, 0.4); | |
| } | |
| /* Drag and Drop Styles */ | |
| .drop-zone { | |
| border: 2px dashed #d1d5db; | |
| border-radius: 0.75rem; | |
| padding: 1rem; | |
| min-height: 150px; | |
| transition: background-color 0.3s; | |
| } | |
| .drop-zone.over { | |
| background-color: #e0f2fe; | |
| border-color: #3b82f6; | |
| } | |
| .draggable { | |
| cursor: grab; | |
| user-select: none; | |
| transition: transform 0.2s, box-shadow 0.2s; | |
| } | |
| .draggable.dragging { | |
| opacity: 0.5; | |
| } | |
| .draggable:active { | |
| cursor: grabbing; | |
| transform: scale(1.05); | |
| box-shadow: 0 8px 16px rgba(0,0,0,0.2); | |
| } | |
| /* Level 4 Bubble Styles */ | |
| .bubble { | |
| position: absolute; | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| color: white; | |
| font-weight: bold; | |
| cursor: pointer; | |
| transition: transform 0.3s ease, width 0.3s ease, height 0.3s ease; | |
| box-shadow: 0 4px 15px rgba(0,0,0,0.1); | |
| } | |
| .bubble:hover { | |
| transform: scale(1.1); | |
| } | |
| .plus-text-container { | |
| font-size: 5rem; | |
| font-weight: bold; | |
| color: #e0e0e0; | |
| position: relative; | |
| } | |
| .plus-fill { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| height: 100%; | |
| width: 0%; | |
| background: linear-gradient(90deg, #4285F4, #34A853, #FBBC05, #EA4335); | |
| color: transparent; | |
| -webkit-background-clip: text; | |
| background-clip: text; | |
| transition: width 0.5s ease; | |
| white-space: nowrap; | |
| overflow: hidden; | |
| } | |
| /* NEW: Level 5 Right Click Zone */ | |
| #right-click-zone { | |
| border: 2px dashed #d1d5db; | |
| border-radius: 0.75rem; | |
| padding: 2rem; | |
| min-height: 200px; | |
| display: flex; | |
| flex-direction: column; /* NEW */ | |
| align-items: center; | |
| justify-content: center; | |
| color: #9ca3af; | |
| font-size: 1.125rem; | |
| transition: all 0.3s; | |
| position: relative; /* For counter */ | |
| } | |
| #right-click-zone.success { | |
| border-color: #22c55e; | |
| border-style: solid; | |
| color: #16a34a; | |
| } | |
| #right-click-counter { | |
| font-size: 2rem; | |
| font-weight: bold; | |
| margin-top: 1rem; | |
| color: #3b82f6; /* blue-500 */ | |
| } | |
| #right-click-zone.success #right-click-counter { | |
| color: #16a34a; /* green-700 */ | |
| } | |
| /* NEW: Level 5 Paste Zone */ | |
| #paste-zone { | |
| border: 2px dashed #d1d5db; | |
| border-radius: 0.75rem; | |
| padding: 2rem; | |
| min-height: 200px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| color: #9ca3af; | |
| font-size: 1.125rem; | |
| transition: all 0.3s; | |
| } | |
| #paste-zone:focus { | |
| outline: none; | |
| border-color: #3b82f6; | |
| box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3); | |
| } | |
| #paste-zone.success { | |
| border-color: #22c55e; | |
| border-style: solid; | |
| color: #16a34a; | |
| } | |
| #paste-zone img { | |
| max-height: 200px; | |
| max-width: 100%; | |
| object-fit: contain; | |
| border-radius: 0.5rem; | |
| } | |
| /* Level 7 Connect Dots Styles */ | |
| #connect-game-svg { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| pointer-events: none; | |
| z-index: 0; | |
| } | |
| .connect-item { | |
| z-index: 1; | |
| position: relative; | |
| cursor: pointer; | |
| transition: background-color 0.3s; | |
| } | |
| .connect-item.selected { | |
| background-color: #fef08a; /* yellow-200 */ | |
| } | |
| /* New Styles for Level 7 Path Game */ | |
| .path-grid-container { | |
| display: grid; | |
| gap: 2px; | |
| background-color: #9ca3af; /* gray-400 */ | |
| border: 2px solid #9ca3af; | |
| } | |
| .path-cell { | |
| background-color: #f1f5f9; /* slate-100 */ | |
| cursor: pointer; | |
| aspect-ratio: 1 / 1; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .path-cell svg { | |
| width: 100%; | |
| height: 100%; | |
| transition: transform 0.3s cubic-bezier(0.68, -0.55, 0.27, 1.55); | |
| stroke-linecap: round; | |
| stroke-linejoin: round; | |
| } | |
| .connect-col-item { | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| text-align: center; | |
| padding: 0.75rem; | |
| border: 2px solid #d1d5db; | |
| border-radius: 0.5rem; | |
| background-color: white; | |
| flex-grow: 1; | |
| } | |
| .connect-col-item.path-correct { | |
| border-color: #22c55e; /* green-500 */ | |
| background-color: #dcfce7; /* green-100 */ | |
| } | |
| /* Level 8: Hidden Object Styles */ | |
| #icon-grid-8-container { /* Added container */ | |
| position: relative; /* Needed for absolute positioning of icons */ | |
| /* Removed min-height, will be set by JS */ | |
| background-color: #f1f5f9; /* slate-100 */ | |
| overflow: hidden; /* Keep icons inside */ | |
| border-radius: 0.5rem; | |
| } | |
| .icon-item { | |
| position: absolute; | |
| width: calc(100% / 12 - 4px); | |
| aspect-ratio: 1 / 1; | |
| cursor: pointer; | |
| transition: transform 0.3s ease, box-shadow 0.3s ease, border-color 0.3s ease, background-color 0.3s ease; /* Added transform */ | |
| border-radius: 0.375rem; | |
| border: 1px solid transparent; | |
| background-color: transparent; | |
| padding: 1px; | |
| object-fit: contain; | |
| will-change: transform; | |
| } | |
| .icon-item:hover { | |
| transform: scale(1.15); | |
| background-color: rgba(255, 255, 255, 0.6); | |
| z-index: 10; | |
| } | |
| .icon-item.hint-flash { | |
| animation: flash 0.5s 6; /* Flash for 3 seconds */ | |
| } | |
| /* NEW: Shrunk state for bonus round */ | |
| .icon-item.shrunk { | |
| transform: scale(0.5); | |
| border: 1px solid #cbd5e1; /* slate-300 */ | |
| box-shadow: 0 1px 2px rgba(0,0,0,0.05); | |
| } | |
| .icon-item.shrunk:hover { | |
| transform: scale(0.9); /* Slightly larger hover for small icons */ | |
| z-index: 10; | |
| background-color: rgba(255, 255, 255, 0.8); | |
| } | |
| @keyframes flash { | |
| 0%, 100% { box-shadow: 0 0 10px 3px #FBBC05; border-color: #FBBC05; background-color: rgba(251, 188, 5, 0.2); } | |
| 50% { box-shadow: none; border-color: transparent; background-color: transparent; } | |
| } | |
| #hint-btn-8:disabled { | |
| background-color: #d1d5db; /* gray-300 */ | |
| color: #6b7280; /* gray-500 */ | |
| cursor: not-allowed; | |
| } | |
| /* Bonus phase counters */ | |
| .bonus-counter { | |
| color: #db2777; /* Pink color for emphasis */ | |
| } | |
| /* Emergency Modal Flash */ | |
| .emergency-flash { | |
| animation: emergencyFlash 0.6s infinite alternate; | |
| } | |
| /* NEW: Success Flash for Level 4 */ | |
| .success-flash { | |
| animation: successFlash 0.8s infinite alternate; | |
| } | |
| @keyframes emergencyFlash { | |
| from { border-color: #ef4444; box-shadow: 0 0 15px 5px rgba(239, 68, 68, 0.7); } | |
| to { border-color: transparent; box-shadow: none; } | |
| } | |
| /* NEW */ | |
| @keyframes successFlash { | |
| from { border-color: #22c55e; box-shadow: 0 0 15px 5px rgba(34, 197, 94, 0.7); } | |
| to { border-color: transparent; box-shadow: none; } | |
| } | |
| /* NEW: Level 9 Poll Styles */ | |
| .poll-card { | |
| border: 2px solid #e5e7eb; /* gray-200 */ | |
| border-radius: 1rem; | |
| padding: 1.5rem; | |
| text-align: center; | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: space-between; | |
| height: 100%; | |
| } | |
| .poll-card img { | |
| width: 100%; | |
| height: 450px; /* Fixed height */ | |
| object-fit: contain; /* 改成 contain 來完整顯示圖片 */ | |
| background-color: #f9fafb; /* gray-50,為圖片加上底色 */ | |
| border-radius: 0.75rem; | |
| cursor: pointer; | |
| transition: transform 0.2s ease, box-shadow 0.2s ease; | |
| } | |
| .poll-card img:hover { | |
| transform: scale(1.03); | |
| box-shadow: 0 8px 20px rgba(0,0,0,0.1); | |
| } | |
| .floating-heart { | |
| position: absolute; | |
| font-size: 2rem; | |
| color: #ef4444; /* red-500 */ | |
| opacity: 0.8; | |
| transition: transform 1.5s ease-out, opacity 1.5s ease-out; | |
| pointer-events: none; | |
| z-index: 100; | |
| user-select: none; | |
| } | |
| /* NEW: Level 10 Drag & Drop */ | |
| .l10-card-container { | |
| min-height: 180px; /* Height for the card */ | |
| padding: 1rem; | |
| } | |
| .l10-card { | |
| background-color: white; | |
| border: 2px solid #cbd5e1; /* slate-300 */ | |
| border-radius: 0.75rem; | |
| padding: 1.5rem; | |
| text-align: center; | |
| box-shadow: 0 4px 10px rgba(0,0,0,0.05); | |
| cursor: grab; | |
| user-select: none; | |
| transition: transform 0.2s, box-shadow 0.2s; | |
| max-width: 500px; | |
| width: 100%; | |
| } | |
| .l10-card.dragging { | |
| opacity: 0.5; | |
| background-color: #f8fafc; /* slate-50 */ | |
| } | |
| .l10-card:active { | |
| cursor: grabbing; | |
| transform: scale(1.03); | |
| box-shadow: 0 8px 20px rgba(0,0,0,0.15); | |
| } | |
| .l10-drop-zone { | |
| border: 3px dashed #d1d5db; /* gray-300 */ | |
| border-radius: 1rem; | |
| padding: 2rem 1rem; | |
| transition: all 0.3s ease; | |
| text-align: center; | |
| font-size: 1.25rem; | |
| font-weight: bold; | |
| min-height: 120px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .l10-drop-zone.over { | |
| transform: scale(1.03); | |
| } | |
| #l10-red-zone.over { border-color: #ef4444; background-color: #fee2e2; } | |
| #l10-yellow-zone.over { border-color: #f59e0b; background-color: #fef9c3; } | |
| #l10-green-zone.over { border-color: #22c55e; background-color: #dcfce7; } | |
| .l10-drop-zone.flash-red { | |
| animation: flash-red-border 0.7s 2; | |
| } | |
| @keyframes flash-red-border { | |
| 0%, 100% { border-color: #ef4444; background-color: #fee2e2; } | |
| 50% { border-color: #d1d5db; background-color: white; } | |
| } | |
| /* NEW: Footer Credit */ | |
| .footer-credit { | |
| position: fixed; | |
| bottom: 8px; | |
| right: 12px; | |
| font-size: 0.75rem; /* 12px */ | |
| color: #9ca3af; /* gray-400 */ | |
| z-index: 1000; | |
| } | |
| /* NEW: Progress Bar Styles */ | |
| .progress-bar-container { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| padding: 0 1rem; | |
| margin: 1rem 2rem 0; | |
| position: relative; | |
| } | |
| .progress-bar-line { | |
| position: absolute; | |
| left: 1.5rem; /* Start after first node */ | |
| right: 1.5rem; /* End before last node */ | |
| top: 50%; | |
| height: 4px; | |
| background-color: #e5e7eb; /* gray-200 */ | |
| z-index: 0; | |
| transform: translateY(-50%); | |
| } | |
| .progress-node { | |
| width: 32px; | |
| height: 32px; | |
| border-radius: 50%; | |
| background-color: #d1d5db; /* gray-300 */ | |
| color: white; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-weight: bold; | |
| z-index: 1; | |
| border: 2px solid white; | |
| box-shadow: 0 0 5px rgba(0,0,0,0.1); | |
| transition: background-color 0.4s ease; | |
| } | |
| .progress-node.completed { | |
| background-color: #4285F4; | |
| } | |
| /* NEW: Replay Button Styles */ | |
| #replay-levels .btn { | |
| padding: 0.5rem; /* Tighter padding for longer text */ | |
| font-size: 0.75rem; /* 12px, Smaller text */ | |
| line-height: 1.2; /* Better than 1 */ | |
| min-height: 40px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| text-align: center; /* Add for good measure */ | |
| } | |
| #replay-levels .bg-yellow-400 { | |
| color: #78350f; /* yellow-900 */ | |
| } | |
| #replay-levels .bg-yellow-400:hover { | |
| background-color: #f59e0b; /* yellow-500 */ | |
| } | |
| /* NEW: Score Display */ | |
| .score-display { | |
| text-align: center; | |
| font-size: 1.25rem; /* 20px */ | |
| /* ... existing code ... --> | |
| </style> | |
| </head> | |
| <body class="bg-gray-100 flex items-center justify-center min-h-screen"> | |
| <div class="game-container w-full"> | |
| <!-- NEW: Progress Bar & Score --> | |
| <div class="score-display"> | |
| 總分 (Total Score): <span id="score-value">0</span> | |
| </div> | |
| <div class="progress-bar-container"> | |
| <div class="progress-bar-line"></div> | |
| <div id="progress-node-1" class="progress-node">1</div> | |
| <div id="progress-node-2" class="progress-node">2</div> | |
| <div id="progress-node-3" class="progress-node">3</div> | |
| <div id="progress-node-4" class="progress-node">4</div> | |
| <div id="progress-node-5" class="progress-node">5</div> | |
| <div id="progress-node-6" class="progress-node">6</div> | |
| <div id="progress-node-7" class="progress-node">7</div> | |
| <div id="progress-node-8" class="progress-node">8</div> | |
| <div id="progress-node-9" class="progress-node">9</div> | |
| <div id="progress-node-10" class="progress-node">10</div> | |
| </div> | |
| <!-- END: Progress Bar & Score --> | |
| <div id="screen-0" class="game-screen active text-center"> | |
| <h1 class="text-4xl font-bold text-gray-800 mb-4 pt-10">Google AI 教育探險</h1> | |
| <p class="text-lg text-gray-600 mb-8">歡迎來到Google AI教育宇宙!您,一位充滿熱情的教育探險家,即將踏上一段未知的旅程。準備好解開AI的秘密,並為您的課堂帶回最強大的寶藏了嗎?</p> | |
| <button onclick="startGame()" class="btn btn-primary text-xl">讓我們開始探險吧!</button> | |
| </div> | |
| <div id="screen-1" class="game-screen"> | |
| <h2 class="text-3xl font-bold text-center mb-2">第一關:建立你的探險隊</h2> | |
| <p class="text-center text-gray-600 mb-8">在Google的教育宇宙中,每位探險家都能找到自己的定位。請將下方的徽章拖到正確的分類中!</p> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-6"> | |
| <div> | |
| <h3 class="font-bold text-xl mb-3 text-center text-blue-600">個人成長</h3> | |
| <div id="personal-growth" class="drop-zone bg-blue-50 h-full flex flex-col items-center justify-center space-y-2"></div> | |
| </div> | |
| <div> | |
| <h3 class="font-bold text-xl mb-3 text-center text-green-600">團隊榮譽</h3> | |
| <div id="team-honor" class="drop-zone bg-green-50 h-full flex flex-col items-center justify-center space-y-2"></div> | |
| </div> | |
| </div> | |
| <div id="draggable-items" class="flex flex-wrap justify-center gap-4 mt-12"> | |
| <div id="gct" draggable="true" class="draggable p-3 bg-white border-2 border-blue-400 text-blue-800 font-semibold rounded-lg shadow-sm" data-type="personal">講師 (GCT)</div> | |
| <div id="gci" draggable="true" class="draggable p-3 bg-white border-2 border-blue-400 text-blue-800 font-semibold rounded-lg shadow-sm" data-type="personal">創意家 (GCI)</div> | |
| <div id="gcc" draggable="true" class="draggable p-3 bg-white border-2 border-blue-400 text-blue-800 font-semibold rounded-lg shadow-sm" data-type="personal">教練 (GCC)</div> | |
| <div id="grc" draggable="true" class="draggable p-3 bg-white border-2 border-green-400 text-green-800 font-semibold rounded-lg shadow-sm" data-type="team">認證學校 (GRC)</div> | |
| </div> | |
| <div id="feedback-1" class="text-center font-bold mt-6 h-6"></div> | |
| </div> | |
| <div id="screen-2" class="game-screen text-center"> | |
| <h2 class="text-3xl font-bold mb-2">第二關:接收秘密通訊</h2> | |
| <p class="text-gray-600 mb-6">探險家,你截獲了一段神秘的訊息,似乎是一個秘密組織的通關密語...</p> | |
| <div class="bg-yellow-100 border-l-4 border-yellow-400 text-yellow-800 p-6 rounded-lg text-left max-w-2xl mx-auto"> | |
| <p class="mb-4">你知道有個叫「<strong>GEG Taiwan</strong>」の酷東西嗎?聽起來像個秘密特務組織,其實是 Google 為老師們成立的教育家社群啦!</p> | |
| <p class="mb-4">這群老師的通關密語超親切,不是什麼複雜的口號,而是最台的問候:「<strong>呷飽沒?</strong>」</p> | |
| <p>沒錯,他們相信,無論是要用多厲害的 Google 工具翻轉教育,都得先填飽肚子!畢竟,老師和學生,「呷飽」才有力氣改變世界嘛!</p> | |
| </div> | |
| <p class="mt-8 mb-4 font-semibold text-lg">立刻掃描下方的通訊信標(QR Code),加入這群熱血的夥伴吧!</p> | |
| <div class="flex justify-center mb-8"> | |
| <img src="https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=https://www.facebook.com/groups/2294169410829101" alt="GEG Taiwan QR Code"> | |
| </div> | |
| <button onclick="completeLevel2()" class="btn btn-primary">我已加入!前往下一關</button> | |
| </div> | |
| <div id="screen-3" class="game-screen"> | |
| <h2 class="text-3xl font-bold text-center mb-2">第三關:鑑定探險裝備</h2> | |
| <p class="text-center text-gray-600 mb-8">身為一位專業的探險家,你必須辨別出哪一項才是 Chromebook 真正擁有的「神功能」組合?</p> | |
| <div id="quiz-3-options" class="space-y-4 max-w-xl mx-auto"> | |
| <label class="block p-4 border-2 border-gray-200 rounded-lg hover:bg-gray-100 cursor-pointer has-[:checked]:bg-blue-50 has-[:checked]:border-blue-400"> | |
| <input type="radio" name="chromebook-features" value="A" class="mr-3"> (A) 會自動寫作業、能當暖暖包、還內建隱形功能,上課偷打電動老師都看不到。 | |
| </label> | |
| <label class="block p-4 border-2 border-gray-200 rounded-lg hover:bg-gray-100 cursor-pointer has-[:checked]:bg-blue-50 has-[:checked]:border-blue-400"> | |
| <input type="radio" name="chromebook-features" value="B" class="mr-3"> (B) 尊爵不凡的信仰標誌、價格跟傳家寶一樣貴、機身輕到可以當飛盤玩。 | |
| </label> | |
| <label class="block p-4 border-2 border-gray-200 rounded-lg hover:bg-gray-100 cursor-pointer has-[:checked]:bg-blue-50 has-[:checked]:border-blue-400"> | |
| <input type="radio" name="chromebook-features" value="C" class="mr-3"> (C) 超快開機、續電力強、不會中毒、不怕你摔,1.2公尺摔不壞。 | |
| </label> | |
| <label class="block p-4 border-2 border-gray-200 rounded-lg hover:bg-gray-100 cursor-pointer has-[:checked]:bg-blue-50 has-[:checked]:border-blue-400"> | |
| <input type="radio" name="chromebook-features" value="D" class="mr-3"> (D) 內建AI管家會幫你泡咖啡、螢幕可以當鏡子、作業系統每天都要拜拜才能順暢運行。 | |
| </label> | |
| </div> | |
| <div class="text-center mt-8"> | |
| <button onclick="checkAnswer(3, 'C')" class="btn btn-primary">確認鑑定</button> | |
| </div> | |
| <div id="feedback-3" class="text-center font-bold mt-6 h-6"></div> | |
| </div> | |
| <div id="screen-4" class="game-screen"> | |
| <h2 class="text-3xl font-bold text-center mb-2">第四關:注入能量</h2> | |
| <p class="text-center text-gray-600 mb-6">前方便是能量閘門,注入正確的能量才能通過。通關密語是Google最新推出的機型!</p> | |
| <div class="flex justify-center items-center my-8"> | |
| <div class="plus-text-container"> | |
| <span>Chromebook </span> | |
| <span class="relative inline-block"> | |
| <span class="text-gray-300">Plus</span> | |
| <span id="plus-fill" class="plus-fill absolute top-0 left-0">Plus</span> | |
| </span> | |
| </div> | |
| </div> | |
| <div id="bubble-container" class="relative h-64 md:h-80 w-full bg-slate-100 rounded-lg overflow-hidden"> | |
| </div> | |
| <div class="text-center mt-4"> | |
| <button onclick="resetBubbles()" class="bg-gray-500 text-white py-2 px-4 rounded-full hover:bg-gray-600 transition">重置泡泡大小</button> | |
| </div> | |
| <div id="feedback-4" class="text-center font-bold mt-6 h-6"></div> | |
| </div> | |
| <div id="screen-5" class="game-screen"> | |
| <h2 class="text-3xl font-bold text-center mb-2">第五關 (1/2):整備雙重技能</h2> <!-- UPDATED --> | |
| <p class="text-center text-gray-600 mb-6">探險開始前,我們先來實戰演練!請完成下列兩項探險家基本技能。</p> <!-- UPDATED --> | |
| <div class="bg-gray-100 p-4 rounded-lg text-center max-w-xl mx-auto mb-6"> | |
| <p class="font-semibold mb-2">快捷鍵提示:</p> | |
| <p class="mb-2"><strong>右鍵 (或兩指輕觸)</strong>:開啟情境選單</p> | |
| <p><code class="bg-gray-300 px-2 py-1 rounded">Ctrl</code> + <code class="bg-gray-300 px-2 py-1 rounded">☐||</code> (全螢幕) / <code class="bg-gray-300 px-2 py-1 rounded">Ctrl</code> + <code class="bg-gray-300 px-2 py-1 rounded">Shift</code> + <code class="bg-gray-300 px-2 py-1 rounded">☐||</code> (自訂範圍)</p> | |
| </div> | |
| <!-- NEW: 2-column layout --> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-6 max-w-4xl mx-auto"> | |
| <!-- NEW: Task 1 --> | |
| <div class="flex flex-col"> | |
| <p class="text-center text-lg text-gray-700 mb-4"><strong>技能 1:</strong> 在下方方框內點擊 <strong>右鍵 (或兩指輕觸)</strong> 3 次。</p> | |
| <div id="right-click-zone" class="flex-1"> | |
| 點擊右鍵 | |
| <div id="right-click-counter">0 / 3</div> | |
| </div> | |
| </div> | |
| <!-- Task 2 (Existing) --> | |
| <div class="flex flex-col"> | |
| <p class="text-center text-lg text-gray-700 mb-4"><strong>技能 2:</strong> 成功截圖後,點擊下方方框並按下 <strong>Ctrl + V</strong> 貼上圖片。</p> | |
| <div id="paste-zone" contenteditable="true" class="mx-auto w-full flex-1"> <!-- UPDATED: w-full, mx-auto, flex-1 --> | |
| 點擊此處,然後按下 Ctrl + V | |
| </div> | |
| </div> | |
| </div> | |
| <div id="feedback-5" class="text-center font-bold mt-6 h-6"></div> | |
| </div> | |
| <div id="screen-5-2" class="game-screen"> | |
| <h2 class="text-3xl font-bold text-center mb-2">第五關 (2/2):整備技能 - 探險家報到</h2> | |
| <p class="text-center text-gray-600 mb-8">太棒了!接下來,請練習打字並完成你的探險家報到程序。這也是為了等等的「AI Prompt 互動」做暖身喔!</p> | |
| <div class="max-w-lg mx-auto space-y-4"> | |
| <div class="flex flex-col sm:flex-row items-center gap-2 text-lg"> | |
| <span>我是來自</span> | |
| <input id="l5-school" type="text" class="flex-1 p-2 border-2 border-gray-300 rounded-md focus:border-blue-500 focus:ring-blue-500 w-full sm:w-auto" placeholder="學校"> | |
| <span>(學校) 的</span> | |
| </div> | |
| <div class="flex flex-col sm:flex-row items-center gap-2 text-lg"> | |
| <input id="l5-name" type="text" class="w-full sm:w-32 p-2 border-2 border-gray-300 rounded-md focus:border-blue-500 focus:ring-blue-500" placeholder="姓名"> | |
| <span>(姓名) 老師,</span> | |
| </div> | |
| <div class="flex flex-col sm:flex-row items-center gap-2 text-lg"> | |
| <span>在學校擔任</span> | |
| <input id="l5-job" type="text" class="flex-1 p-2 border-2 border-gray-300 rounded-md focus:border-blue-500 focus:ring-blue-500 w-full sm:w-auto" placeholder="職務"> | |
| <span>(職務) 工作。</span> | |
| </div> | |
| </div> | |
| <div class="text-center mt-8"> | |
| <button onclick="checkLevel5_2()" class="btn btn-primary">完成報到</button> | |
| </div> | |
| <div id="feedback-5-2" class="text-center font-bold mt-6 h-6"></div> | |
| </div> | |
| <!-- --- (FIXED BLOCK: Level 6) --- --> | |
| <div id="screen-6" class="game-screen"> | |
| <h2 class="text-3xl font-bold text-center mb-2">第六關:洞察AI的蹤跡</h2> | |
| <p class="text-center text-gray-600 mb-8">AI早已融入我們的生活,下面哪一個常見的生活情境,其實「沒有」用到 AI 技術呢?</p> | |
| <div id="quiz-6-options" class="space-y-4 max-w-2xl mx-auto"> | |
| <!-- NEW Option A (Plausible AI) --> | |
| <label class="block p-4 border-2 border-gray-200 rounded-lg hover:bg-gray-100 cursor-pointer has-[:checked]:bg-blue-50 has-[:checked]:border-blue-400"> | |
| <input type="radio" name="ai-myth" value="A" class="mr-3"> A. 網頁翻譯工具,能即時將日文新聞翻譯成中文。 | |
| </label> | |
| <!-- NEW Option B (Plausible AI) --> | |
| <label class="block p-4 border-2 border-gray-200 rounded-lg hover:bg-gray-100 cursor-pointer has-[:checked]:bg-blue-50 has-[:checked]:border-blue-400"> | |
| <input type="radio" name="ai-myth" value="B" class="mr-3"> B. 手機相簿自動將照片分類為「寵物」、「食物」、「風景」。 | |
| </label> | |
| <!-- FIXED Option C (Was outside) --> | |
| <label class="block p-4 border-2 border-gray-200 rounded-lg hover:bg-gray-100 cursor-pointer has-[:checked]:bg-blue-50 has-[:checked]:border-blue-400"> | |
| <input type="radio" name="ai-myth" value="C" class="mr-3"> C. YouTube 自動推薦下一部你可能會喜歡的搞笑影片。 | |
| </label> | |
| <!-- FIXED Option D (Was outside) --> | |
| <label class="block p-4 border-2 border-gray-200 rounded-lg hover:bg-gray-100 cursor-pointer has-[:checked]:bg-blue-50 has-[:checked]:border-blue-400"> | |
| <input type="radio" name="ai-myth" value="D" class="mr-3"> D. 用微波爐加熱隔夜的便當,設定「中火、3分鐘」。 | |
| </label> | |
| </div> | |
| <!-- FIXED This div was split into two and contained wrong buttons --> | |
| <div class="text-center mt-8"> | |
| <button onclick="showHint(6)" class="bg-yellow-400 text-yellow-900 font-bold py-2 px-4 rounded-full hover:bg-yellow-500 transition mr-4">需要一點線索嗎?</button> | |
| <button onclick="checkAnswer(6, 'D')" class="btn btn-primary">確認答案</button> | |
| </div> | |
| <div id="feedback-6" class="text-center font-bold mt-6 h-6"></div> | |
| <div id="hint-6" class="text-center text-blue-600 bg-blue-100 p-3 rounded-lg mt-4 max-w-xl mx-auto hidden"></div> | |
| </div> | |
| <!-- --- (END OF FIXED BLOCK) --- --> | |
| <div id="screen-7" class="game-screen"> | |
| <h2 class="text-3xl font-bold text-center mb-2">第七關:解碼AI神諭</h2> | |
| <p id="level-7-instructions" class="text-center text-gray-600 mb-4 h-12">請將指定的詞彙與解釋正確配對。</p> | |
| <div id="matched-pairs-display" class="mb-4 p-3 bg-gray-100 rounded-lg min-h-[50px] text-center space-x-2 space-y-2"> | |
| </div> | |
| <div id="connect-game-wrapper" class="relative flex justify-center items-stretch gap-1 md:gap-2 text-xs md:text-sm"> | |
| <div id="keywords-col" class="flex flex-col gap-2 w-1/4"> | |
| <div id="term-ai" class="connect-col-item">人工智慧 (AI)</div> | |
| <div id="term-ml" class="connect-col-item">機器學習 (ML)</div> | |
| <div id="term-genai" class="connect-col-item">生成式AI</div> | |
| <div id="term-llm" class="connect-col-item">大型語言模型 (LLM)</div> | |
| <div id="term-chatbot" class="connect-col-item">聊天機器人</div> | |
| </div> | |
| <div id="path-grid-container" class="path-grid-container"> | |
| </div> | |
| <div id="definitions-col" class="flex flex-col gap-2 w-1/4"> | |
| <div id="def-genai" class="connect-col-item" data-def-id="def-genai">能創造新東西的AI</div> | |
| <div id="def-chatbot" class="connect-col-item" data-def-id="def-chatbot">能與你對話的機器人</div> | |
| <div id="def-ai" class="connect-col-item" data-def-id="def-ai">機器能像人一樣聰明</div> | |
| <div id="def-llm" class="connect-col-item" data-def-id="def-llm">語言大師 | |
| </div> | |
| <div id="def-ml" class="connect-col-item" data-def-id="def-ml">會自己學習的機器</div> | |
| </div> | |
| <svg id="path-svg-overlay" class="absolute top-0 left-0 w-full h-full pointer-events-none" style="display: none;"></svg> | |
| </div> | |
| <div class="text-center mt-8"> | |
| <button id="check-path-btn" onclick="checkRotatedPathConnection()" class="btn btn-primary" style="display: none;">確認路徑</button> | |
| </div> | |
| <div id="feedback-7" class="text-center font-bold mt-6 h-6"></div> | |
| </div> | |
| <div id="screen-8" class="game-screen"> | |
| <h2 id="level-8-title" class="text-3xl font-bold text-center mb-2">第八關:發掘 Workspace 新星</h2> | |
| <p id="level-8-subtitle" class="text-center text-gray-600 mb-2">探險家,Google 的工具宇宙中誕生了一顆新星——Google Vids!</p> | |
| <p id="level-8-warning" class="text-center text-lg font-bold text-red-600 mb-4">(很重要,考試會考!😉)</p> | |
| <p id="level-8-main-instruction" class="text-center text-gray-600 mb-6">它就隱藏在我們熟悉的工具夥伴中。快睜大眼睛,在下方的工具海中找出 <strong>3</strong> 個 [Google Vids] 的 icon 吧!</p> | |
| <div class="flex flex-col sm:flex-row justify-between items-center mb-4 p-3 bg-gray-100 rounded-lg gap-2"> | |
| <div class="text-lg font-bold">計時器: <span id="timer-8">00:00</span></div> | |
| <div id="vids-counter-container" class="text-lg font-bold">已找到 Vids: <span id="found-counter-8">0</span> / 3</div> | |
| <div id="bonus-counters-container" class="text-lg font-bold hidden flex space-x-4"> | |
| <span class="bonus-counter">Gemini: <span id="found-gemini-8">0</span> / 5</span> | |
| <span class="bonus-counter">NotebookLM: <span id="found-notebooklm-8">0</span> / 5</span> | |
| </div> | |
| <button id="hint-btn-8" onclick="useHintLevel8()" class="bg-yellow-400 text-yellow-900 font-bold py-2 px-4 rounded-full hover:bg-yellow-500 transition"> | |
| 提示按鈕 (剩下 1 次) | |
| </button> | |
| </div> | |
| <div id="icon-grid-8-container" class="p-2 rounded-lg relative"> | |
| <!-- Icons will be generated by JS --> | |
| </div> | |
| </div> | |
| <!-- Screen 9: NEW AI Poll --> | |
| <div id="screen-9" class="game-screen"> | |
| <h2 class="text-3xl font-bold text-center mb-2">第九關:AI男神人氣投票!</h2> | |
| <p class="text-center text-gray-600 mb-8">探險家,在了解了兩位AI男神的魅力之後,是時候表達你的心意了!為你喜歡的AI男神點讚,讓他獲得更多人氣吧!</p> | |
| <!-- NEW: Poll Stats --> | |
| <div class="flex justify-around items-center text-center mb-6 p-4 bg-gray-100 rounded-lg"> | |
| <div class="text-2xl font-bold text-red-600"> | |
| 倒數計時: <span id="poll-timer">20</span>s | |
| </div> | |
| <div class="text-2xl font-bold text-blue-600"> | |
| 總點擊數: <span id="poll-clicks">0</span> | |
| </div> | |
| </div> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-6 items-stretch"> | |
| <!-- Gemini Card --> | |
| <div class="poll-card bg-blue-50 border-blue-200"> | |
| <div> | |
| <img id="gemini-img" src="https://imgtolinkx.com/i/UjO8oPQV" alt="Gemini 圖片" class="mb-4"> | |
| <h3 class="text-2xl font-bold text-blue-800 mb-2">Gemini</h3> | |
| <p class="text-gray-700 mb-4">知識淵博、創意無限的智慧夥伴,擅長多模態溝通與複雜推理。</p> | |
| </div> | |
| <div id="gemini-likes-display" class="text-2xl font-bold text-red-500">❤️ Gemini 人氣: 0</div> | |
| </div> | |
| <!-- NotebookLM Card --> | |
| <div class="poll-card bg-purple-50 border-purple-200"> | |
| <div> | |
| <img id="notebooklm-img" src="https://imgtolinkx.com/i/aNzFNvv4" alt="NotebookLM 圖片" class="mb-4"> | |
| <h3 class="text-2xl font-bold text-purple-800 mb-2">NotebookLM</h3> | |
| <p class="text-gray-700 mb-4">你的專屬研究助理,精通文件閱讀與重點摘要,是學術與工作的好幫手。</p> | |
| </div> | |
| <div id="notebooklm-likes-display" class="text-2xl font-bold text-red-500">❤️ NotebookLM 人氣: 0</div> | |
| </div> | |
| </div> | |
| <p class="text-center text-gray-500 mt-6 text-lg">點擊圖片為你的男神應援!</p> | |
| </div> | |
| <!-- Screen 10: NEW AI Ethics --> | |
| <div id="screen-10" class="game-screen"> | |
| <h2 class="text-3xl font-bold text-center mb-2">最終挑戰:AI 倫理守門人</h2> | |
| <p class="text-center text-gray-600 mb-6">恭喜你來到最後一關!身為一位專業的教育探險家,你現在的任務是審核下列教學情境。請依據AI倫理「紅綠燈」原則,將這些「教案申請卡」拖曳到正確的區域!</p> | |
| <div class="text-center text-xl font-bold text-gray-700 mb-6"> | |
| 已審核:<span id="l10-progress-counter">0</span> / <span id="l10-progress-total">6</span> | |
| </div> | |
| <!-- Card Container --> | |
| <div id="l10-card-container" class="l10-card-container flex items-center justify-center"> | |
| <!-- Card will be injected by JS --> | |
| </div> | |
| <!-- Drop Zones --> | |
| <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mt-8"> | |
| <div id="l10-red-zone" class="l10-drop-zone text-red-600 bg-red-50 border-red-300"> | |
| 🟥 紅燈區 (停止使用) | |
| </div> | |
| <div id="l10-yellow-zone" class="l10-drop-zone text-yellow-600 bg-yellow-50 border-yellow-300"> | |
| 🟨 黃燈區 (謹慎前行) | |
| </div> | |
| <div id="l10-green-zone" class="l10-drop-zone text-green-600 bg-green-50 border-green-300"> | |
| 🟩 綠燈區 (放心探索) | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Screen 11: End (Old Screen 10) --> | |
| <div id="screen-11" class="game-screen text-center"> | |
| <h1 class="text-4xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-blue-500 to-green-500 mb-4">恭喜!</h1> | |
| <p class="text-lg text-gray-600 mb-8">您已成功完成 Google AI 教育探險!您現在已經掌握了關鍵的 AI 知識,準備好在您的課堂上發光發熱了!</p> | |
| <!-- NEW: Final Score --> | |
| <p id="final-score-display" class="text-2xl font-bold text-gray-800 mb-8">你的總分是:0 分!</p> | |
| <!-- NEW: Replay Section --> | |
| <hr class="my-6"> | |
| <h3 class="text-xl font-bold text-gray-700 mb-2">關卡挑戰室</h3> | |
| <p class="text-gray-600 mb-4">點擊下方按鈕可重玩關卡。<strong class="text-yellow-700">黃色按鈕</strong>代表該關卡未達滿分,回去挑戰並「一次通關」可獲得 5 分獎勵 (僅限一次)!</p> | |
| <div id="replay-levels" class="grid grid-cols-5 gap-2 max-w-lg mx-auto mb-8"> | |
| <!-- JS will generate buttons here --> | |
| </div> | |
| <!-- END: Replay Section --> | |
| <button onclick="restartGame()" class="btn btn-primary">再次探險</button> | |
| </div> | |
| </div> | |
| <!-- Level 8 Modal --> | |
| <div id="level-8-modal-container" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 hidden z-50"> | |
| <div id="level-8-modal-content" class="bg-white rounded-lg shadow-xl p-8 text-center max-w-md w-full border-4 border-transparent transition-all duration-300"> | |
| <h3 id="level-8-modal-title" class="text-2xl font-bold mb-4">太棒了!</h3> | |
| <p id="level-8-feedback" class="text-lg text-gray-700 mb-6"></p> | |
| <button id="level-8-modal-button" class="btn btn-primary w-full"></button> | |
| <button id="level-8-modal-button-skip" class="btn bg-gray-300 text-gray-700 hover:bg-gray-400 mt-2 w-full" style="display: none;">以後再說</button> | |
| </div> | |
| </div> | |
| <!-- New Level 10 Modal --> | |
| <div id="level-10-modal-container" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 hidden z-50"> | |
| <div id="level-10-modal-content" class="bg-white rounded-lg shadow-xl p-8 text-center max-w-md w-full border-4 border-red-500 transition-all duration-300"> | |
| <h3 id="level-10-modal-title" class="text-2xl font-bold mb-4 text-red-600">哎呀,不對喔!</h3> | |
| <p id="level-10-feedback" class="text-lg text-gray-700 mb-6"></p> | |
| <button id="level-10-modal-button" class="btn btn-primary" onclick="hideLevel10Modal(false)">再試一次</button> | |
| </div> | |
| </div> | |
| <!-- NEW: Footer Credit --> | |
| <div class="footer-credit"> | |
| 程式設計者:新竹縣精華國中 藍星宇 與 Gemini Canvas | |
| </div> | |
| <script> | |
| let currentScreen = 0; | |
| const totalScreens = 11; // UPDATED from 10 to 11 | |
| let correctSound, wrongSound, successSound; | |
| let cheatCodeBuffer = ''; | |
| // --- NEW: Firebase Globals --- | |
| let db, auth; | |
| let userId = null; // Firebase Auth User ID | |
| let playerIdentifier = ""; // L5-2 儲存的玩家名稱 | |
| let isFirebaseReady = false; | |
| let fbSDK = null; // 用來存放 window.firebaseSDK | |
| // --- END NEW --- | |
| // --- NEW: Scoring and State --- | |
| let totalScore = 0; | |
| let levelScores = {}; // Stores the *best* score for each level (1-10) | |
| let replayBonusAwarded = {}; // Stores if replay bonus was given (e.g., { 3: true }) | |
| let isReplaying = false; // Flag for replay mode | |
| const MAX_SCORES = { | |
| 1: 100, 2: 100, 3: 100, 4: 100, 5: 100, | |
| 6: 100, 7: 100, 8: 100, 9: 100, 10: 120 | |
| }; // L7 = 5*20, L10 = 12*10 | |
| let level1WrongDrop = false; // L1 | |
| let level3Tries = 0; // L3 | |
| let level4ResetUsed = false; // L4 | |
| let level5Part1Score = 0; // L5 | |
| let level5RightClicks = 0; // L5 - NEW | |
| let level5RightClickDone = false; // L5 - NEW | |
| let level5PasteDone = false; // L5 - NEW | |
| let level6Tries = 0; // L6 | |
| let level7Score = 0; // L7 | |
| let level7FirstTry = true; // L7 | |
| let level7CurrentStageTries = 0; // L7 | |
| let level8BonusTime = 0; // L8 | |
| let pollGameTimer = null; // L9 | |
| let pollCountdownInterval = null; // L9 | |
| let pollClickCount = 0; // L9 | |
| let level10Score = 0; // L10 | |
| let level10FirstTry = true; // L10 | |
| let level10CurrentCardTries = 0; // L10 | |
| // --- END: Scoring and State --- | |
| // --- NEW: Image Preloader --- | |
| function preloadImages() { | |
| const imageUrls = [ | |
| 'https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=https://www.facebook.com/groups/2294169410829101', | |
| // Level 8 Icons | |
| 'https://imgtolinkx.com/i/XLPlZVZw', // Vids | |
| 'https://imgtolinkx.com/i/ByK3AKuQ', // Gemini | |
| 'https://imgtolinkx.com/i/kZvAtWwv', // NotebookLM | |
| 'https://www.gstatic.com/images/branding/product/1x/gmail_2020q4_48dp.png', | |
| 'https://ssl.gstatic.com/images/branding/product/2x/drive_48dp.png', | |
| 'https://ssl.gstatic.com/calendar/images/dynamiclogo_2020q4/calendar_15_2x.png', | |
| 'https://ssl.gstatic.com/images/branding/product/2x/docs_48dp.png', | |
| 'https://ssl.gstatic.com/images/branding/product/2x/sheets_48dp.png', | |
| 'https://ssl.gstatic.com/images/branding/product/2x/keep_48dp.png', | |
| 'https://fonts.gstatic.com/s/i/productlogos/meet_2020q4/v1/web-96dp/logo_meet_2020q4_color_1x_web_96dp.png', | |
| // Level 9 Icons | |
| 'https://imgtolinkx.com/i/UjO8oPQV', // Gemini Poll | |
| 'https://imgtolinkx.com/i/aNzFNvv4' // NotebookLM Poll | |
| ]; | |
| imageUrls.forEach(url => { | |
| const img = new Image(); | |
| img.src = url; | |
| img.onload = () => console.log(`Preloaded: ${url}`); | |
| img.onerror = () => console.warn(`Failed to preload: ${url}`); | |
| }); | |
| } | |
| preloadImages(); // <--- NEW: 立即呼叫預載 | |
| // --- Cheat Codes --- | |
| document.addEventListener('keydown', e => { | |
| // NEW FIX: 僅將單一、可列印的字元添加到緩衝區 | |
| // 這能防止 "Shift", "Control", "Backspace" 等鍵污染密碼 | |
| if (e.key.length > 1) { | |
| return; // 忽略 "Shift", "Control" 等非字元鍵 | |
| } | |
| cheatCodeBuffer += e.key; | |
| // 增加緩衝區長度以容納 scoreboard | |
| if (cheatCodeBuffer.length > 10) { | |
| cheatCodeBuffer = cheatCodeBuffer.substring(cheatCodeBuffer.length - 10); | |
| } | |
| // NEW: Scoreboard Trigger | |
| // FIX: 轉換為小寫以進行不分大小寫的比對 | |
| if (cheatCodeBuffer.toLowerCase().endsWith('scoreboard')) { | |
| console.log("Scoreboard activated!"); | |
| showScoreboard(); // 呼叫新函數 | |
| cheatCodeBuffer = ''; // Reset | |
| } | |
| // END NEW | |
| if (cheatCodeBuffer.endsWith('101010')) { // Special check for 10 | |
| console.log(`Cheat code activated: Go to screen 10`); | |
| goToScreen(10); | |
| cheatCodeBuffer = ''; // Reset buffer | |
| } | |
| for (let i = 1; i <= totalScreens; i++) { | |
| if (i === 10) continue; // Skip 10, has custom code | |
| let repeatNum = (i === 11) ? 6 : 5; // 6 repeats for 11, 5 for others | |
| // Use (i % 10) to get '1' from 11. | |
| if (cheatCodeBuffer.endsWith(String(i % 10).repeat(repeatNum))) { | |
| console.log(`Cheat code activated: Go to screen ${i}`); | |
| goToScreen(i); | |
| cheatCodeBuffer = ''; // Reset buffer | |
| } | |
| } | |
| }); | |
| // --- Sound Engine --- | |
| function setupSounds() { | |
| if (typeof Tone !== 'undefined') { | |
| if (Tone.context.state !== 'running') { | |
| Tone.start(); | |
| } | |
| correctSound = new Tone.Synth({ | |
| oscillator: { type: 'sine' }, | |
| envelope: { attack: 0.005, decay: 0.1, sustain: 0.3, release: 1 } | |
| }).toDestination(); | |
| wrongSound = new Tone.Synth({ | |
| oscillator: { type: 'square' }, | |
| envelope: { attack: 0.01, decay: 0.2, sustain: 0.2, release: 0.2 } | |
| }).toDestination(); | |
| successSound = new Tone.Synth({ | |
| envelope: { attack: 0.02, decay: 0.2, sustain: 0.5, release: 0.8 } | |
| }).toDestination(); | |
| } else { | |
| console.warn("Tone.js not loaded, sounds will be disabled."); | |
| } | |
| } | |
| function playSound(type) { | |
| if (!correctSound) return; | |
| try { | |
| if (type === 'correct') { | |
| correctSound.triggerAttackRelease('C5', '8n'); | |
| } else if (type === 'wrong') { | |
| wrongSound.triggerAttackRelease('A2', '8n'); | |
| } else if (type === 'success') { | |
| successSound.triggerAttackRelease('C4', '8n', Tone.now()); | |
| successSound.triggerAttackRelease('E4', '8n', Tone.now() + 0.2); | |
| successSound.triggerAttackRelease('G4', '8n', Tone.now() + 0.4); | |
| } | |
| } catch (e) { | |
| console.error("Audio playback error:", e); | |
| } | |
| } | |
| // --- NEW: Score & Progress Functions --- | |
| /** | |
| * Records the score for a level. | |
| * Handles replay bonuses and stores only the best score. | |
| */ | |
| function recordLevelScore(levelNum, score, isFirstTrySuccess = false) { | |
| levelNum = Math.floor(levelNum); | |
| if (!levelNum || levelNum < 1 || levelNum > 10) { | |
| console.error("Invalid levelNum:", levelNum); | |
| return; | |
| } | |
| let oldScore = levelScores[levelNum] || 0; | |
| let maxScore = MAX_SCORES[levelNum]; | |
| // 1. Award replay bonus? | |
| // Only if: replaying, old score was bad, new score is "first try", bonus not already given | |
| if (isReplaying && oldScore < maxScore && isFirstTrySuccess && !replayBonusAwarded[levelNum]) { | |
| // Award 5 bonus points | |
| replayBonusAwarded[levelNum] = true; | |
| // We don't add to totalScore directly, updateTotalScore will handle it | |
| } | |
| // 2. Update the level's best score | |
| if (score > oldScore) { | |
| levelScores[levelNum] = score; | |
| } | |
| // 3. Update progress bar (always, even if score isn't better) | |
| updateProgressBar(levelNum); | |
| // 4. Recalculate and update total score display | |
| updateTotalScore(); | |
| } | |
| /** | |
| * Calculates the total score from levelScores and bonuses, then updates the display. | |
| */ | |
| function updateTotalScore() { | |
| let newTotal = 0; | |
| // Sum all best level scores | |
| for (let i = 1; i <= 10; i++) { | |
| newTotal += (levelScores[i] || 0); | |
| } | |
| // Add any replay bonuses that were awarded | |
| for (const level in replayBonusAwarded) { | |
| if (replayBonusAwarded[level]) newTotal += 5; | |
| } | |
| totalScore = newTotal; | |
| updateScoreDisplay(); | |
| } | |
| function updateScoreDisplay() { | |
| const scoreEl = document.getElementById('score-value'); | |
| if (scoreEl) scoreEl.textContent = totalScore; | |
| } | |
| function updateProgressBar(levelNum) { | |
| if (levelNum < 1 || levelNum > 10) return; | |
| const node = document.getElementById(`progress-node-${Math.floor(levelNum)}`); // Use floor for 5.2 | |
| if (node) { | |
| node.classList.add('completed'); | |
| } | |
| } | |
| // --- END: Score & Progress Functions --- | |
| // --- Game Flow --- | |
| function goToScreen(screenNumber) { | |
| // Stop activities if leaving specific screens | |
| if (currentScreen === 8) { | |
| clearInterval(level8Timer); | |
| } | |
| if (currentScreen === 4) { | |
| clearInterval(bubbleGrowInterval); | |
| } | |
| if (currentScreen === 9) { // NEW: Cleanup for poll screen | |
| stopPollGame(); | |
| } | |
| // NEW: No cleanup needed for L10 yet | |
| // Handle non-integer screen numbers for sub-screens | |
| const screenId = `screen-${String(screenNumber).replace('.', '-')}`; | |
| const currentScreenEl = document.getElementById(`screen-${String(currentScreen).replace('.', '-')}`); | |
| if (currentScreenEl) { | |
| currentScreenEl.classList.remove('active'); | |
| } | |
| currentScreen = screenNumber; | |
| const nextScreenEl = document.getElementById(screenId); | |
| if (nextScreenEl) { | |
| nextScreenEl.classList.add('active'); | |
| } else { | |
| console.error(`Screen with id ${screenId} not found!`); | |
| // Fallback to screen 0 | |
| document.getElementById('screen-0').classList.add('active'); | |
| currentScreen = 0; | |
| } | |
| if(currentScreen === 4) { | |
| initBubbleGame(); | |
| } | |
| if(currentScreen === 5) { // NEW | |
| initLevel5(); | |
| } | |
| if(currentScreen === 7) { | |
| currentPathStage = 0; | |
| initConnectGame(); | |
| } | |
| if(currentScreen === 8) { | |
| initLevel8Game(); | |
| } | |
| if(currentScreen === 9) { // NEW: Init for poll screen | |
| initPollGame(); | |
| } | |
| if(currentScreen === 10) { // NEW: Init for L10 | |
| initLevel10(); | |
| } | |
| if(currentScreen === 11) { // NEW: Show final score | |
| isReplaying = false; // We have arrived at the end | |
| updateTotalScore(); // Recalculate score | |
| document.getElementById('final-score-display').textContent = `你的總分是:${totalScore} 分!`; | |
| generateReplayButtons(); // NEW: Generate replay buttons | |
| // --- NEW: Save score to Firebase --- | |
| // 每當玩家到達結束畫面(無論是第一次或重玩),都儲存/更新分數 | |
| saveScoreToFirebase(); | |
| // --- END NEW --- | |
| } | |
| } | |
| async function startGame() { // NEW: 設為 async 函數 | |
| setupSounds(); | |
| // NEW: Initialize Firebase | |
| await initializeFirebase(); // NEW: 加入 await,強制等待 Firebase 連線完成 | |
| // END NEW | |
| goToScreen(1); | |
| } | |
| <!-- --- NEW: Firebase Init Function --- | |
| async function initializeFirebase() { | |
| if (isFirebaseReady) return; | |
| // 等待 SDK 模組載入完成 | |
| while (!window.firebaseSDK) { | |
| await new Promise(resolve => setTimeout(resolve, 50)); | |
| } | |
| fbSDK = window.firebaseSDK; | |
| try { | |
| // --- NEW: Correct Firebase Config Logic --- | |
| // 檢查 __firebase_config 變數是否存在 | |
| if (typeof __firebase_config === 'undefined') { | |
| console.warn("Firebase config (__firebase_config) not found. Scoreboard will not work."); | |
| return; | |
| } | |
| const firebaseConfig = JSON.parse(__firebase_config); | |
| // --- END NEW --- | |
| if (!firebaseConfig.apiKey) { | |
| console.warn("Firebase config is invalid. Scoreboard will not work."); | |
| return; | |
| } | |
| const app = fbSDK.initializeApp(firebaseConfig); | |
| db = fbSDK.getFirestore(app); | |
| auth = fbSDK.getAuth(app); | |
| fbSDK.setLogLevel('Debug'); // 方便除錯 | |
| // --- NEW: Correct Authentication Logic --- | |
| // 檢查 __initial_auth_token 並登入 | |
| if (typeof __initial_auth_token !== 'undefined') { | |
| console.log("Signing in with custom token..."); | |
| await fbSDK.signInWithCustomToken(auth, __initial_auth_token); | |
| } else { | |
| console.log("Signing in anonymously..."); | |
| await fbSDK.signInAnonymously(auth); | |
| } | |
| // --- END NEW --- | |
| // 取得並儲存這個(匿名)使用者的唯一 ID | |
| userId = auth.currentUser ? auth.currentUser.uid : crypto.randomUUID(); | |
| isFirebaseReady = true; | |
| console.log("Firebase initialized, User ID:", userId); | |
| } catch (error) { | |
| console.error("Firebase initialization failed:", error); | |
| } | |
| } | |
| // --- END NEW --- | |
| // --- NEW: Firebase Save Score Function --- | |
| async function saveScoreToFirebase() { | |
| if (!isFirebaseReady || !userId || !fbSDK) { | |
| console.warn("Firebase not ready or user not logged in. Cannot save score."); | |
| return; | |
| } | |
| // 如果玩家跳過了 L5-2,提供一個預設名稱 | |
| const displayName = playerIdentifier || `探險家-${userId.substring(0, 4)}`; | |
| try { | |
| const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id'; | |
| // --- FIX: 改為 addDoc 來儲存每一次的紀錄 --- | |
| // 1. 取得 collection 的參照 | |
| const scoresCollectionRef = fbSDK.collection(db, `artifacts/${appId}/public/data/scores`); | |
| // 2. 使用 addDoc 來新增文件,Firebase 會自動產生ID | |
| // 這樣就能儲存每一次的遊玩紀錄,而不是覆蓋舊紀錄 | |
| await fbSDK.addDoc(scoresCollectionRef, { | |
| displayName: displayName, | |
| score: totalScore, // 全域變數中的最新分數 | |
| lastUpdated: fbSDK.serverTimestamp(), // 儲存伺服器時間 | |
| userId: userId // 儲存玩家ID以便追蹤 | |
| }); | |
| // --- END FIX --- | |
| console.log("Score saved successfully to Firebase:", totalScore); | |
| } catch (error) { | |
| console.error("Error saving score to Firebase:", error); | |
| } | |
| } | |
| // --- END NEW --- | |
| function restartGame() { | |
| location.reload(); | |
| } | |
| // --- NEW: Replay Functions --- | |
| function generateReplayButtons() { | |
| const container = document.getElementById('replay-levels'); | |
| if (!container) return; | |
| container.innerHTML = ''; | |
| // NEW: Level Names | |
| const levelNames = [ | |
| "1. 探險隊", "2. 秘密通訊", "3. 鑑定裝備", "4. 注入能量", "5. 整備技能", | |
| "6. 洞察AI", "7. 解碼神諭", "8. 發掘新星", "9. AI男神", "10. 倫理守門人" | |
| ]; | |
| for (let i = 1; i <= 10; i++) { | |
| const btn = document.createElement('button'); | |
| btn.textContent = levelNames[i-1]; // NEW | |
| btn.onclick = () => replayLevel(i); | |
| let score = levelScores[i] || 0; | |
| let max = MAX_SCORES[i]; | |
| // For level 10, max is 120, but 60 is also "complete" | |
| let isFullScore = score >= max; | |
| if (i === 10 && score >= 60 && score < 120) { | |
| // Special case: Completed L10 but not bonus, still show yellow | |
| isFullScore = false; | |
| } | |
| if (!isFullScore) { | |
| btn.className = 'btn bg-yellow-400 text-yellow-900 hover:bg-yellow-500'; // Needs improvement | |
| } else { | |
| btn.className = 'btn btn-primary'; // Full score | |
| } | |
| container.appendChild(btn); | |
| } | |
| } | |
| function replayLevel(levelNum) { | |
| isReplaying = true; | |
| // Reset level-specific "tries" or "state" variables | |
| level1WrongDrop = false; | |
| level3Tries = 0; | |
| level4ResetUsed = false; | |
| level5Part1Score = 0; // Reset for L5 | |
| level6Tries = 0; | |
| level7Score = 0; // Reset for L7 | |
| level7FirstTry = true; | |
| level7CurrentStageTries = 0; | |
| // L8 and L9 state are reset by their init functions | |
| level10Score = 0; // Reset for L10 | |
| level10FirstTry = true; | |
| level10CurrentCardTries = 0; | |
| goToScreen(levelNum); | |
| } | |
| // --- END Replay --- | |
| function showFeedback(screenNum, message, isCorrect) { | |
| const feedbackEl = document.getElementById(`feedback-${screenNum}`); | |
| if (!feedbackEl) { // Check if feedback element exists | |
| console.warn(`Feedback element for screen ${screenNum} not found.`); | |
| return; | |
| } | |
| feedbackEl.textContent = message; | |
| feedbackEl.className = `text-center font-bold mt-6 h-6 ${isCorrect ? 'text-green-600' : 'text-red-600'}`; | |
| if (isCorrect) { | |
| playSound('success'); | |
| // Updated: Go to screen 8 from 7, otherwise go to screen+1 | |
| const nextScreen = (screenNum === 7) ? 8 : screenNum + 1; | |
| // DO NOT ADVANCE for level 5.2, it's handled separately | |
| if (screenNum !== 5.2 && screenNum !== 3 && screenNum !== 6 && screenNum !== 7) { | |
| // OLD: setTimeout(() => goToScreen(nextScreen), 1500); | |
| // NEW: Handle replay nav | |
| setTimeout(() => { | |
| if (isReplaying) goToScreen(11); | |
| else goToScreen(nextScreen); | |
| }, 1500); | |
| } | |
| // NEW: Handle score-advancing screens | |
| if (screenNum === 3 || screenNum === 6 || screenNum === 7) { | |
| // OLD: setTimeout(() => goToScreen(nextScreen), 1500); | |
| // NEW: Handle replay nav | |
| setTimeout(() => { | |
| if (isReplaying) goToScreen(11); | |
| else goToScreen(nextScreen); | |
| }, 1500); | |
| } | |
| } else { | |
| playSound('wrong'); | |
| } | |
| } | |
| function checkAnswer(screenNum, correctAnswer) { | |
| let userAnswer; | |
| if (screenNum === 3 || screenNum === 6) { | |
| // NEW: Track tries | |
| if (screenNum === 3) level3Tries++; | |
| if (screenNum === 6) level6Tries++; | |
| const selectedOption = document.querySelector(`input[name="${screenNum === 3 ? 'chromebook-features' : 'ai-myth'}"]:checked`); | |
| userAnswer = selectedOption ? selectedOption.value : null; | |
| } else if (screenNum === 5) { | |
| // OLD LOGIC REMOVED | |
| return; // Level 5 logic is now custom | |
| } | |
| if (userAnswer === correctAnswer) { | |
| // NEW: Scoring logic | |
| if (screenNum === 3) { | |
| // OLD: addScore(level3Tries === 1 ? 100 : 75); | |
| recordLevelScore(3, level3Tries === 1 ? 100 : 75, level3Tries === 1); | |
| // OLD: updateProgressBar(3); | |
| } | |
| if (screenNum === 6) { | |
| // OLD: addScore(level6Tries === 1 ? 100 : 75); | |
| recordLevelScore(6, level6Tries === 1 ? 100 : 75, level6Tries === 1); | |
| // OLD: updateProgressBar(6); | |
| } | |
| const correctMessages = { | |
| 3: '鑑定成功!這才是探險家的標準配備!', | |
| // 5: '正確!你找到了寶藏地圖!', // REMOVED | |
| 6: '觀察入微!你已經掌握了分辨AI的基本能力!' | |
| }; | |
| showFeedback(screenNum, correctMessages[screenNum], true); | |
| } else { | |
| const wrongMessages = { | |
| 3: '哎呀!看來你被小華唬住了,再試一次吧!', | |
| // 5: '網址不對喔,再想想看!', // REMOVED | |
| 6: '這個情境有用AI喔,再試一次吧!' | |
| }; | |
| showFeedback(screenNum, wrongMessages[screenNum], false); | |
| } | |
| } | |
| function showHint(screenNum) { | |
| const hintEl = document.getElementById(`hint-${screenNum}`); | |
| if (screenNum === 6) { | |
| hintEl.textContent = '試著想想看,哪個選項的運作方式只是在執行一個「固定不變」的指令呢?'; | |
| } | |
| hintEl.style.display = 'block'; | |
| } | |
| // --- NEW: Level 2 Completion --- | |
| function completeLevel2() { | |
| // OLD: addScore(100); // Level 2 can't fail | |
| // OLD: updateProgressBar(2); | |
| recordLevelScore(2, 100, true); | |
| goToScreen(3); | |
| } | |
| // --- Level 1: Drag & Drop Logic --- | |
| document.addEventListener('DOMContentLoaded', () => { | |
| // preloadImages(); // NEW: Call preloader <-- REMOVED: 已移到更早的位置 | |
| const draggables = document.querySelectorAll('.draggable'); | |
| const dropZones = document.querySelectorAll('.drop-zone'); | |
| let draggedItem = null; | |
| // Mouse Events | |
| draggables.forEach(draggable => { | |
| draggable.addEventListener('dragstart', e => { | |
| draggedItem = e.target; | |
| e.dataTransfer.setData('text/plain', e.target.id); | |
| setTimeout(() => e.target.classList.add('dragging'), 0); | |
| }); | |
| draggable.addEventListener('dragend', e => { | |
| e.target.classList.remove('dragging'); | |
| }); | |
| }); | |
| dropZones.forEach(zone => { | |
| zone.addEventListener('dragover', e => { | |
| e.preventDefault(); | |
| zone.classList.add('over'); | |
| }); | |
| zone.addEventListener('dragleave', () => zone.classList.remove('over')); | |
| zone.addEventListener('drop', e => { | |
| e.preventDefault(); | |
| zone.classList.remove('over'); | |
| // Find the dragged item by ID if it's not set by touch | |
| if (!draggedItem) { | |
| const id = e.dataTransfer.getData('text/plain'); | |
| draggedItem = document.getElementById(id); | |
| } | |
| handleDrop(draggedItem, zone); | |
| draggedItem = null; // Reset after drop | |
| }); | |
| }); | |
| // Touch Events | |
| let touchDragClone = null; | |
| draggables.forEach(draggable => { | |
| draggable.addEventListener('touchstart', e => { | |
| draggedItem = e.target; | |
| // Create clone | |
| touchDragClone = draggedItem.cloneNode(true); | |
| touchDragClone.style.position = 'absolute'; | |
| touchDragClone.style.zIndex = '1000'; | |
| touchDragClone.style.opacity = '0.8'; | |
| touchDragClone.style.pointerEvents = 'none'; | |
| document.body.appendChild(touchDragClone); | |
| const touch = e.touches[0]; | |
| touchDragClone.style.left = `${touch.clientX - touchDragClone.offsetWidth / 2}px`; | |
| touchDragClone.style.top = `${touch.clientY - touchDragClone.offsetHeight / 2}px`; | |
| draggedItem.classList.add('dragging'); | |
| }, { passive: false }); | |
| draggable.addEventListener('touchmove', e => { | |
| e.preventDefault(); | |
| if (!touchDragClone) return; | |
| const touch = e.touches[0]; | |
| touchDragClone.style.left = `${touch.clientX - touchDragClone.offsetWidth / 2}px`; | |
| touchDragClone.style.top = `${touch.clientY - touchDragClone.offsetHeight / 2}px`; | |
| dropZones.forEach(zone => { | |
| const rect = zone.getBoundingClientRect(); | |
| if (touch.clientX > rect.left && touch.clientX < rect.right && | |
| touch.clientY > rect.top && touch.clientY < rect.bottom) { | |
| zone.classList.add('over'); | |
| } else { | |
| zone.classList.remove('over'); | |
| } | |
| }); | |
| }, { passive: false }); | |
| draggable.addEventListener('touchend', e => { | |
| if (!touchDragClone) return; | |
| const touch = e.changedTouches[0]; | |
| // Find the element *under* the touch point | |
| touchDragClone.style.display = 'none'; // Hide clone temporarily | |
| const dropTarget = document.elementFromPoint(touch.clientX, touch.clientY); | |
| touchDragClone.style.display = 'block'; // Show again | |
| // Check if dropTarget is a drop-zone or inside a drop-zone | |
| let actualZone = dropTarget ? dropTarget.closest('.drop-zone') : null; | |
| if (actualZone) { | |
| handleDrop(draggedItem, actualZone); | |
| } | |
| document.body.removeChild(touchDragClone); | |
| touchDragClone = null; | |
| draggedItem.classList.remove('dragging'); | |
| dropZones.forEach(zone => zone.classList.remove('over')); | |
| draggedItem = null; // Reset after drop | |
| }); | |
| }); | |
| function handleDrop(item, zone) { | |
| if (!item) return; | |
| const isPersonal = item.dataset.type === 'personal'; | |
| const isTeam = item.dataset.type === 'team'; | |
| if ((zone.id === 'personal-growth' && isPersonal) || (zone.id === 'team-honor' && isTeam)) { | |
| zone.appendChild(item); | |
| item.setAttribute('draggable', 'false'); | |
| item.style.cursor = 'default'; | |
| playSound('correct'); | |
| } else { | |
| playSound('wrong'); | |
| level1WrongDrop = true; // NEW: Track wrong drop | |
| } | |
| checkLevel1Completion(); // NEW: Check completion | |
| } | |
| // NEW: Level 1 Completion Check | |
| function checkLevel1Completion() { | |
| const personalZone = document.getElementById('personal-growth'); | |
| const teamZone = document.getElementById('team-honor'); | |
| if (personalZone.children.length === 3 && teamZone.children.length === 1) { | |
| // Disable all draggables to prevent further moves | |
| document.querySelectorAll('.draggable').forEach(d => { | |
| d.setAttribute('draggable', 'false'); | |
| d.style.cursor = 'default'; | |
| }); | |
| // NEW: Add score | |
| // OLD: addScore(level1WrongDrop ? 75 : 100); | |
| // OLD: updateProgressBar(1); | |
| recordLevelScore(1, level1WrongDrop ? 75 : 100, !level1WrongDrop); | |
| showFeedback(1, '探險隊成立!正在前往下一關...', true); | |
| } | |
| } | |
| }); | |
| // --- NEW: Level 5 (Screenshot & Typing) Logic --- | |
| function initLevel5() { | |
| const pasteZone = document.getElementById('paste-zone'); | |
| // NEW: Right click zone | |
| const rightClickZone = document.getElementById('right-click-zone'); | |
| const rightClickCounter = document.getElementById('right-click-counter'); | |
| if (pasteZone) { | |
| // Clear old content and state | |
| pasteZone.innerHTML = '點擊此處,然後按下 Ctrl + V'; | |
| pasteZone.classList.remove('success', 'border-green-500', 'border-solid'); | |
| pasteZone.onpaste = handlePasteLevel5; | |
| } | |
| // NEW: Reset right click zone | |
| if (rightClickZone) { | |
| rightClickZone.classList.remove('success'); | |
| rightClickZone.oncontextmenu = handleRightClickLevel5; // Attach event | |
| } | |
| if (rightClickCounter) { | |
| rightClickCounter.textContent = '0 / 3'; | |
| } | |
| const feedbackEl = document.getElementById('feedback-5'); | |
| if (feedbackEl) { | |
| feedbackEl.innerHTML = ''; | |
| } | |
| // Reset Part 2 fields | |
| const school = document.getElementById('l5-school'); | |
| const name = document.getElementById('l5-name'); | |
| const job = document.getElementById('l5-job'); | |
| if(school) school.value = ''; | |
| if(name) name.value = ''; | |
| if(job) job.value = ''; | |
| const feedbackEl2 = document.getElementById('feedback-5-2'); | |
| if(feedbackEl2) feedbackEl2.innerHTML = ''; | |
| // NEW: Reset flags | |
| level5RightClicks = 0; | |
| level5RightClickDone = false; | |
| level5PasteDone = false; | |
| level5Part1Score = 0; // Already global, but good to reset | |
| } | |
| // NEW: Handle Right Click | |
| function handleRightClickLevel5(e) { | |
| e.preventDefault(); // Stop context menu | |
| if (level5RightClickDone) return; // Already completed | |
| level5RightClicks++; | |
| playSound('correct'); // Play soft click sound | |
| const rightClickCounter = document.getElementById('right-click-counter'); | |
| if (level5RightClicks >= 3) { | |
| level5RightClicks = 3; // Cap at 3 | |
| level5RightClickDone = true; | |
| if (rightClickCounter) rightClickCounter.textContent = '✅ 完成!'; | |
| document.getElementById('right-click-zone').classList.add('success'); | |
| document.getElementById('right-click-zone').oncontextmenu = (e) => e.preventDefault(); // Keep blocking menu | |
| checkLevel5Part1Completion(); | |
| } else { | |
| if (rightClickCounter) rightClickCounter.textContent = `${level5RightClicks} / 3`; | |
| } | |
| } | |
| function handlePasteLevel5(e) { | |
| e.preventDefault(); | |
| const feedbackEl = document.getElementById('feedback-5'); | |
| const pasteZone = document.getElementById('paste-zone'); | |
| const files = e.clipboardData.files; | |
| if (files.length > 0 && files[0].type.startsWith('image/')) { | |
| const file = files[0]; | |
| const url = URL.createObjectURL(file); | |
| pasteZone.innerHTML = `<img src="${url}" alt="貼上的截圖">`; | |
| pasteZone.classList.add('success'); | |
| // feedbackEl.textContent = '✅ 截圖成功!正在前往下一階段...'; // MOVED | |
| // feedbackEl.className = 'text-center font-bold mt-6 h-6 text-green-600'; // MOVED | |
| // playSound('success'); // MOVED | |
| level5PasteDone = true; // NEW | |
| // level5Part1Score = 50; // MOVED to checkLevel5Part1Completion | |
| // Remove paste listener to prevent multiple triggers | |
| pasteZone.onpaste = null; | |
| checkLevel5Part1Completion(); // NEW | |
| // setTimeout(() => goToScreen(5.2), 2000); // MOVED to checkLevel5Part1Completion | |
| } else { | |
| feedbackEl.textContent = '哎呀,你需要貼上一張『圖片』喔!'; | |
| feedbackEl.className = 'text-center font-bold mt-6 h-6 text-red-600'; | |
| playSound('wrong'); | |
| } | |
| } | |
| // NEW: Check for completion of *both* tasks | |
| function checkLevel5Part1Completion() { | |
| if (level5RightClickDone && level5PasteDone) { | |
| const feedbackEl = document.getElementById('feedback-5'); | |
| feedbackEl.textContent = '✅ 技能整備完成!正在前往下一階段...'; | |
| feedbackEl.className = 'text-center font-bold mt-6 h-6 text-green-600'; | |
| playSound('success'); | |
| level5Part1Score = 50; // Grant score | |
| setTimeout(() => goToScreen(5.2), 2000); // Go to Part 2 | |
| } | |
| } | |
| function checkLevel5_2() { | |
| const school = document.getElementById('l5-school').value; | |
| const name = document.getElementById('l5-name').value; | |
| const job = document.getElementById('l5-job').value; | |
| const feedbackEl = document.getElementById('feedback-5-2'); | |
| if (school.trim() && name.trim() && job.trim()) { | |
| // --- NEW: Store Player Identifier --- | |
| // 儲存玩家在 L5-2 輸入的名稱 | |
| playerIdentifier = `${school.trim()} - ${name.trim()}`; | |
| console.log("Player identifier set:", playerIdentifier); | |
| // --- END NEW --- | |
| feedbackEl.textContent = '報到完成!準備進入下一關...'; | |
| feedbackEl.className = 'text-center font-bold mt-6 h-6 text-green-600'; | |
| playSound('success'); | |
| // OLD: addScore(50); // NEW: Add 50 points | |
| // OLD: updateProgressBar(5); // NEW: Mark level 5 complete | |
| // NEW: Record full score for L5 | |
| recordLevelScore(5, level5Part1Score + 50, true); // Assume part 1 was also first try | |
| // OLD: setTimeout(() => goToScreen(6), 1500); | |
| setTimeout(() => { | |
| if (isReplaying) goToScreen(11); | |
| else goToScreen(6); | |
| }, 1500); | |
| } else { | |
| feedbackEl.textContent = '請填寫所有欄位喔!'; | |
| feedbackEl.className = 'text-center font-bold mt-6 h-6 text-red-600'; | |
| playSound('wrong'); | |
| } | |
| } | |
| // --- Level 4: Bubble Game Logic --- | |
| let plusClicked = 0; | |
| const plusNeeded = 5; | |
| const googleColors = ['#EA4335', '#4285F4', '#FBBC05', '#34A853']; // Red, Blue, Yellow, Green | |
| let bubbleGrowInterval; | |
| // let level4ResetUsed = false; // Already global | |
| function initBubbleGame() { | |
| const container = document.getElementById('bubble-container'); | |
| container.innerHTML = ''; | |
| plusClicked = 0; | |
| level4ResetUsed = false; // NEW: Reset flag | |
| updatePlusFill(); | |
| clearInterval(bubbleGrowInterval); | |
| const words = ['Plus', 'Pause', 'Play', 'Power', 'Pigs', 'Plus', 'Plus', 'Pause', 'Play', 'Plus', 'Power', 'Plus', 'Pigs', 'Play', 'Plus', 'Power', 'Pause', 'Play', 'Pigs', 'Power', 'Plus', 'Plus', 'Play','Power', 'Pause', 'Play', 'Pigs', 'Power', 'Plus']; | |
| words.forEach(word => { | |
| const bubble = document.createElement('div'); | |
| const size = Math.random() * 40 + 30; | |
| bubble.classList.add('bubble'); | |
| bubble.dataset.word = word; | |
| bubble.style.width = `${size}px`; | |
| bubble.style.height = `${size}px`; | |
| bubble.style.left = `${Math.random() * (container.offsetWidth - size)}px`; | |
| bubble.style.top = `${Math.random() * (container.offsetHeight - size)}px`; | |
| bubble.style.backgroundColor = word === 'Plus' | |
| ? googleColors[1] | |
| : googleColors[Math.floor(Math.random() * 4)]; | |
| bubble.textContent = word; | |
| if (word === 'Plus') { | |
| bubble.addEventListener('click', handleCorrectBubble); | |
| } else { | |
| bubble.addEventListener('click', handleWrongBubbleManualClick); | |
| } | |
| container.appendChild(bubble); | |
| animateBubbleMovement(bubble); // Renamed function for clarity | |
| }); | |
| bubbleGrowInterval = setInterval(growWrongBubblesAutomatically, 3000); | |
| } | |
| function animateBubbleMovement(bubble) { // Renamed | |
| const container = document.getElementById('bubble-container'); | |
| let xSpeed = (Math.random() - 0.5) * 1.5; | |
| let ySpeed = (Math.random() - 0.5) * 1.5; | |
| let x = parseFloat(bubble.style.left); | |
| let y = parseFloat(bubble.style.top); | |
| function move() { | |
| if (!bubble.parentNode) return; | |
| x += xSpeed; | |
| y += ySpeed; | |
| const currentWidth = bubble.offsetWidth; | |
| const currentHeight = bubble.offsetHeight; | |
| const containerWidth = container.offsetWidth; | |
| const containerHeight = container.offsetHeight; | |
| if (x <= 0) { | |
| xSpeed = Math.abs(xSpeed); | |
| x = 0; | |
| } else if (x + currentWidth >= containerWidth) { | |
| xSpeed = -Math.abs(xSpeed); | |
| x = containerWidth - currentWidth; | |
| } | |
| if (y <= 0) { | |
| ySpeed = Math.abs(ySpeed); | |
| y = 0; | |
| } else if (y + currentHeight >= containerHeight) { | |
| ySpeed = -Math.abs(ySpeed); | |
| y = containerHeight - currentHeight; | |
| } | |
| bubble.style.left = `${x}px`; | |
| bubble.style.top = `${y}px`; | |
| requestAnimationFrame(move); | |
| } | |
| move(); | |
| } | |
| function handleCorrectBubble(e) { | |
| playSound('correct'); | |
| plusClicked++; | |
| e.target.remove(); | |
| updatePlusFill(); | |
| if (plusClicked >= plusNeeded) { | |
| clearInterval(bubbleGrowInterval); | |
| // UPDATED: | |
| // const feedbackMessage = '能量注入完成!沒錯,就是「Chromebook Plus」!'; | |
| // showFeedback(4, feedbackMessage, true); | |
| // NEW: Call modal | |
| showLevel4SuccessModal(); | |
| } | |
| } | |
| function handleWrongBubbleManualClick(e) { | |
| playSound('wrong'); | |
| growBubble(e.target); | |
| } | |
| function growBubble(bubble) { | |
| if (!bubble || !bubble.parentNode) return; | |
| const container = document.getElementById('bubble-container'); | |
| const maxSize = Math.min(container.offsetWidth, container.offsetHeight) * 0.8; | |
| const currentSize = bubble.offsetWidth; | |
| let newSize = currentSize * 1.15; | |
| if (newSize > maxSize) { | |
| newSize = maxSize; | |
| } | |
| bubble.style.width = `${newSize}px`; | |
| bubble.style.height = `${newSize}px`; | |
| bubble.style.zIndex = (parseInt(bubble.style.zIndex) || 10) + 1; | |
| } | |
| function growWrongBubblesAutomatically() { | |
| const wrongBubbles = document.querySelectorAll('#bubble-container .bubble'); | |
| wrongBubbles.forEach(bubble => { | |
| if (bubble.dataset.word !== 'Plus') { | |
| growBubble(bubble); | |
| } | |
| }); | |
| } | |
| function updatePlusFill() { | |
| const fillPercentage = (plusClicked / plusNeeded) * 100; | |
| document.getElementById('plus-fill').style.width = `${fillPercentage}%`; | |
| } | |
| function resetBubbles() { | |
| const wrongBubbles = document.querySelectorAll('#bubble-container .bubble'); | |
| wrongBubbles.forEach(bubble => { | |
| if (bubble.dataset.word !== 'Plus') { | |
| const originalSize = Math.random() * 40 + 30; | |
| bubble.style.width = `${originalSize}px`; | |
| bubble.style.height = `${originalSize}px`; | |
| bubble.style.zIndex = '10'; | |
| } | |
| }); | |
| playSound('correct'); | |
| level4ResetUsed = true; // NEW: Set flag | |
| } | |
| // --- Level 7: Connect Game Logic --- | |
| const GRID_ROWS = 5; | |
| const GRID_COLS = 4; | |
| const pathGridState = []; | |
| let currentPathStage = 0; | |
| let level7Phase = 'SELECTING'; | |
| // let level7Score = 0; // Already global | |
| // let level7FirstTry = true; // Already global | |
| // let level7CurrentStageTries = 0; // Already global | |
| const solutionOrder = [ 'term-ai', 'term-ml', 'term-genai', 'term-llm', 'term-chatbot' ]; | |
| const stageLayouts = [ | |
| // Stage 1 (index 0): Row 0 -> 2 | |
| [ | |
| ['L', 'I', 'I', 'I'], | |
| ['I', 'I', 'L', 'I'], | |
| ['L', 'I', 'I', 'I'], | |
| ['I', 'L', 'I', 'I'], | |
| ['L', 'I', 'L', 'I'] | |
| ], | |
| // Stage 2 (index 1): Row 1 -> 4 | |
| [ | |
| ['L', 'I', 'L', 'I'], | |
| ['I', 'L', 'I', 'I'], | |
| ['I', 'L', 'L', 'I'], | |
| ['I', 'L', 'I', 'L'], | |
| ['L', 'I', 'L', 'I'] | |
| ], | |
| // Stage 3 (index 2): Row 2 -> 0 | |
| [ | |
| ['I', 'I', 'I', 'L'], | |
| ['L', 'I', 'I', 'L'], | |
| ['L', 'I', 'I', 'I'], | |
| ['I', 'I', 'I', 'I'], | |
| ['I', 'I', 'I', 'I'] | |
| ], | |
| // Stage 4 (index 3): Row 3 -> 3 | |
| [ | |
| ['I', 'L', 'L', 'I'], | |
| ['I', 'I', 'I', 'I'], | |
| ['I', 'I', 'I', 'I'], | |
| ['I', 'L', 'L', 'I'], | |
| ['I', 'I', 'I', 'I'] | |
| ], | |
| // Stage 5 (index 4): Row 4 -> 1 | |
| [ | |
| ['I', 'I', 'I', 'I'], | |
| ['I', 'I', 'L', 'I'], | |
| ['L', 'I', 'I', 'I'], | |
| ['L', 'I', 'L', 'I'], | |
| ['L', 'I', 'I', 'I'] | |
| ] | |
| ]; | |
| const puzzleSolution = { | |
| 'term-ai': 'def-ai', 'term-ml': 'def-ml', 'term-genai': 'def-genai', | |
| 'term-llm': 'def-llm', 'term-chatbot': 'def-chatbot' | |
| }; | |
| function initConnectGame() { | |
| if (currentPathStage >= solutionOrder.length) return; | |
| level7Phase = 'SELECTING'; | |
| if (currentPathStage === 0) { | |
| document.getElementById('matched-pairs-display').innerHTML = ''; | |
| level7Score = 0; // NEW: Reset score on L7 init | |
| level7FirstTry = true; // NEW: Reset first try flag | |
| } | |
| level7CurrentStageTries = 0; // NEW: Reset tries for the stage | |
| const currentKeywordId = solutionOrder[currentPathStage]; | |
| const activeKeywordEl = document.getElementById(currentKeywordId); | |
| const instructionsEl = document.getElementById('level-7-instructions'); | |
| instructionsEl.innerHTML = `階段 ${currentPathStage + 1} / 5: 請為 <strong class="text-blue-600">${activeKeywordEl.textContent}</strong> 選擇正確的解釋。`; | |
| document.getElementById('feedback-7').textContent = ''; | |
| document.getElementById('path-grid-container').style.visibility = 'hidden'; | |
| document.getElementById('check-path-btn').style.display = 'none'; | |
| document.getElementById('definitions-col').style.visibility = 'visible'; | |
| document.querySelectorAll('#keywords-col .connect-col-item').forEach(el => el.classList.add('opacity-30', 'bg-gray-50')); | |
| activeKeywordEl.classList.remove('opacity-30', 'bg-gray-50'); | |
| document.querySelectorAll('#definitions-col .connect-col-item').forEach(el => { | |
| const isAlreadyMatched = Array.from(document.getElementById('matched-pairs-display').children).some(p => p.dataset.defId === el.dataset.defId); | |
| const newEl = el.cloneNode(true); | |
| el.parentNode.replaceChild(newEl, el); | |
| if (isAlreadyMatched) { | |
| newEl.classList.add('opacity-30', 'bg-gray-50', 'cursor-not-allowed'); | |
| } else { | |
| newEl.classList.remove('opacity-30', 'bg-gray-50', 'cursor-not-allowed'); | |
| newEl.classList.add('cursor-pointer', 'hover:bg-yellow-100'); | |
| newEl.addEventListener('click', handleDefinitionSelection); | |
| } | |
| }); | |
| } | |
| function handleDefinitionSelection(event) { | |
| if (level7Phase !== 'SELECTING') return; | |
| const selectedDefEl = event.currentTarget; | |
| const selectedDefId = selectedDefEl.dataset.defId; | |
| const currentKeywordId = solutionOrder[currentPathStage]; | |
| const correctDefId = puzzleSolution[currentKeywordId]; | |
| if (selectedDefId === correctDefId) { | |
| playSound('correct'); | |
| level7Phase = 'CONNECTING'; | |
| document.querySelectorAll('#definitions-col .connect-col-item').forEach(el => { | |
| const newEl = el.cloneNode(true); | |
| el.parentNode.replaceChild(newEl, el); | |
| if (newEl.dataset.defId !== correctDefId) { | |
| newEl.classList.add('opacity-30', 'bg-gray-50'); | |
| } | |
| newEl.classList.remove('cursor-pointer', 'hover:bg-yellow-100'); | |
| newEl.classList.add('cursor-not-allowed'); | |
| }); | |
| document.getElementById('level-7-instructions').innerHTML = `選擇正確!現在請旋轉方塊,<strong class="text-green-600">連接路徑</strong>。`; | |
| document.getElementById('path-grid-container').style.visibility = 'visible'; | |
| document.getElementById('check-path-btn').style.display = 'inline-block'; | |
| setupRotatablePathGrid(); | |
| } else { | |
| playSound('wrong'); | |
| level7CurrentStageTries++; // NEW: Increment wrong tries | |
| const feedbackEl = document.getElementById('feedback-7'); | |
| feedbackEl.textContent = `這個解釋不對喔,再試一次!`; | |
| feedbackEl.className = `text-center font-bold mt-6 h-6 text-red-600`; | |
| selectedDefEl.classList.add('border-red-500'); | |
| setTimeout(() => { | |
| selectedDefEl.classList.remove('border-red-500'); | |
| feedbackEl.textContent = ''; | |
| }, 1500); | |
| } | |
| } | |
| function setupRotatablePathGrid() { | |
| const gridContainer = document.getElementById('path-grid-container'); | |
| gridContainer.innerHTML = ''; | |
| pathGridState.length = 0; | |
| gridContainer.style.gridTemplateColumns = `repeat(${GRID_COLS}, 1fr)`; | |
| gridContainer.style.gridTemplateRows = `repeat(${GRID_ROWS}, 1fr)`; | |
| const currentLayout = stageLayouts[currentPathStage]; | |
| for (let r = 0; r < GRID_ROWS; r++) { | |
| pathGridState[r] = []; | |
| for (let c = 0; c < GRID_COLS; c++) { | |
| const cell = document.createElement('div'); | |
| cell.classList.add('path-cell'); | |
| const type = currentLayout[r][c]; | |
| const rotation = Math.floor(Math.random() * 4) * 90; | |
| cell.innerHTML = getPathSvg(type); | |
| cell.firstElementChild.style.transform = `rotate(${rotation}deg)`; | |
| const cellState = { type, rotation, element: cell }; | |
| pathGridState[r][c] = cellState; | |
| cell.addEventListener('click', () => { | |
| playSound('correct'); | |
| cellState.rotation = (cellState.rotation + 90) % 360; | |
| cell.firstElementChild.style.transform = `rotate(${cellState.rotation}deg)`; | |
| }); | |
| gridContainer.appendChild(cell); | |
| } | |
| } | |
| } | |
| function checkRotatedPathConnection() { | |
| if (level7Phase !== 'CONNECTING') return; | |
| const keywordElements = document.querySelectorAll('#keywords-col .connect-col-item'); | |
| const definitionElements = document.querySelectorAll('#definitions-col .connect-col-item'); | |
| const currentKeywordId = solutionOrder[currentPathStage]; | |
| const correctDefId = puzzleSolution[currentKeywordId]; | |
| const startRow = Array.from(keywordElements).findIndex(el => el.id === currentKeywordId); | |
| const expectedEndRow = Array.from(definitionElements).findIndex(el => el.dataset.defId === correctDefId); | |
| const tracedEndRow = tracePath(startRow); | |
| if (tracedEndRow === expectedEndRow) { | |
| playSound('success'); | |
| const activeKeywordEl = document.getElementById(currentKeywordId); | |
| const correctDefEl = document.querySelector(`[data-def-id="${correctDefId}"]`); | |
| const matchedDisplay = document.getElementById('matched-pairs-display'); | |
| const newPair = document.createElement('span'); | |
| newPair.className = 'bg-green-100 text-green-800 font-semibold p-2 rounded-md inline-block m-1 text-xs sm:text-sm'; | |
| newPair.textContent = `${activeKeywordEl.textContent} → ${correctDefEl.textContent}`; | |
| newPair.dataset.defId = correctDefId; | |
| matchedDisplay.appendChild(newPair); | |
| // NEW: Add score | |
| // OLD: addScore(level7CurrentStageTries === 0 ? 20 : 15); | |
| if (level7CurrentStageTries > 0) level7FirstTry = false; // Mark as not first try | |
| level7Score += (level7CurrentStageTries === 0 ? 20 : 15); // Add to partial score | |
| currentPathStage++; | |
| if (currentPathStage < solutionOrder.length) { | |
| const feedbackEl = document.getElementById(`feedback-7`); | |
| feedbackEl.textContent = '路徑連接成功!準備下一題...'; | |
| feedbackEl.className = `text-center font-bold mt-6 h-6 text-green-600`; | |
| setTimeout(() => initConnectGame(), 2000); | |
| } else { | |
| // Go to screen 8 | |
| // OLD: updateProgressBar(7); // NEW: Mark level 7 complete | |
| recordLevelScore(7, level7Score, level7FirstTry); // NEW: Record final L7 score | |
| showFeedback(7, '解碼完成!你已獲得AI的智慧!', true); | |
| } | |
| } else { | |
| playSound('wrong'); | |
| const feedbackEl = document.getElementById('feedback-7'); | |
| if (tracedEndRow === -1) { | |
| feedbackEl.textContent = `路徑不通或走出了邊界,請檢查!`; | |
| } else { | |
| feedbackEl.textContent = `路徑連接到錯誤的答案了,請再試一次!`; | |
| } | |
| feedbackEl.className = `text-center font-bold mt-6 h-6 text-red-600`; | |
| } | |
| } | |
| function getPathSvg(type) { | |
| const color = "#4b5563"; // gray-600 | |
| if (type === 'I') { // Straight | |
| return `<svg viewBox="0 0 10 10"><path d="M0 5 H 10" stroke="${color}" stroke-width="2" fill="none"/></svg>`; | |
| } else { // Corner | |
| return `<svg viewBox="0 0 10 10"><path d="M0 5 H 5 V 10" stroke="${color}" stroke-width="2" fill="none"/></svg>`; | |
| } | |
| } | |
| function getExits(cell) { | |
| const exits = []; | |
| const r = cell.rotation; | |
| if (cell.type === 'I') { | |
| if (r === 0 || r === 180) exits.push('left', 'right'); | |
| else exits.push('top', 'bottom'); | |
| } else { // type 'L' - Path is from left to bottom at 0deg | |
| if (r === 0) exits.push('left', 'bottom'); | |
| if (r === 90) exits.push('top', 'left'); | |
| if (r === 180) exits.push('right', 'top'); | |
| if (r === 270) exits.push('bottom', 'right'); | |
| } | |
| return exits; | |
| } | |
| function tracePath(startRow) { | |
| let pos = { r: startRow, c: 0 }; | |
| let fromDir = 'left'; | |
| for(let i = 0; i < (GRID_COLS * GRID_ROWS + 5); i++) { // Failsafe loop | |
| if (pos.c < 0 || pos.r < 0 || pos.r >= GRID_ROWS) { | |
| return -1; // Went off grid | |
| } | |
| if (pos.c >= GRID_COLS) { | |
| return pos.r; // Reached the end | |
| } | |
| const cell = pathGridState[pos.r][pos.c]; | |
| if (!cell) { | |
| console.error("Error: Cell is undefined at", pos); | |
| return -1; | |
| } | |
| const exits = getExits(cell); | |
| if (!exits.includes(fromDir)) return -1; // Dead end | |
| const exitDir = exits.find(dir => dir !== fromDir); | |
| if (!exitDir) return -1; // Should not happen | |
| // Move to next cell | |
| if (exitDir === 'right') { pos.c++; fromDir = 'left'; } | |
| else if (exitDir === 'left') { pos.c--; fromDir = 'right'; } | |
| else if (exitDir === 'bottom') { pos.r++; fromDir = 'top'; } | |
| else if (exitDir === 'top') { pos.r--; fromDir = 'bottom'; } | |
| } | |
| return -1; // Path is too long or looping | |
| } | |
| // --- Level 8: Hidden Object Game Logic --- | |
| let level8Timer; | |
| let time_taken_vids = 0; | |
| let foundVids = 0; | |
| let hint_used = false; | |
| const totalVids = 3; | |
| let level8Phase = 'FINDING_VIDS'; | |
| let foundGemini = 0; | |
| let foundNotebookLM = 0; | |
| const neededGemini = 5; | |
| const neededNotebookLM = 5; | |
| function initLevel8Game() { | |
| // Reset game state | |
| clearInterval(level8Timer); | |
| time_taken_vids = 0; | |
| level8BonusTime = 0; // NEW: Reset bonus time | |
| foundVids = 0; | |
| hint_used = false; | |
| level8Phase = 'FINDING_VIDS'; | |
| foundGemini = 0; | |
| foundNotebookLM = 0; | |
| document.getElementById('vids-counter-container').classList.remove('hidden'); | |
| document.getElementById('bonus-counters-container').classList.add('hidden'); | |
| document.getElementById('level-8-title').textContent = "第八關:發掘 Workspace 新星"; | |
| document.getElementById('level-8-subtitle').classList.remove('hidden'); | |
| document.getElementById('level-8-warning').classList.remove('hidden'); | |
| document.getElementById('level-8-main-instruction').innerHTML = `它就隱藏在我們熟悉的工具夥伴中。快睜大眼睛,在下方的工具海中找出 <strong>3</strong> 個 [Google Vids] 的 icon 吧!`; | |
| const hintBtn = document.getElementById('hint-btn-8'); | |
| hintBtn.disabled = false; | |
| hintBtn.textContent = '提示按鈕 (剩下 1 次)'; | |
| hintBtn.classList.remove('hidden'); | |
| document.getElementById('level-8-modal-container').classList.add('hidden'); | |
| const gridContainer = document.getElementById('icon-grid-8-container'); | |
| gridContainer.innerHTML = ''; | |
| // Define icons | |
| const vidsIcon = { type: 'vids', src: 'https://imgtolinkx.com/i/XLPlZVZw', alt: 'Google Vids Icon' }; | |
| const geminiIcon = { type: 'gemini', src: 'https://imgtolinkx.com/i/ByK3AKuQ', alt: 'Gemini Icon'}; | |
| const notebooklmIcon = { type: 'notebooklm', src: 'https://imgtolinkx.com/i/kZvAtWwv', alt: 'NotebookLM Icon'}; | |
| const iconPool = [ | |
| { type: 'gmail', src: 'https://www.gstatic.com/images/branding/product/1x/gmail_2020q4_48dp.png', alt: 'Gmail Icon' }, | |
| { type: 'drive', src: 'https://ssl.gstatic.com/images/branding/product/2x/drive_48dp.png', alt: 'Drive Icon' }, | |
| { type: 'cal', src: 'https://ssl.gstatic.com/calendar/images/dynamiclogo_2020q4/calendar_15_2x.png', alt: 'Calendar Icon' }, | |
| { type: 'docs', src: 'https://ssl.gstatic.com/images/branding/product/2x/docs_48dp.png', alt: 'Docs Icon' }, | |
| { type: 'sheets', src: 'https://ssl.gstatic.com/images/branding/product/2x/sheets_48dp.png', alt: 'Sheets Icon' }, | |
| { type: 'keep', src: 'https://ssl.gstatic.com/images/branding/product/2x/keep_48dp.png', alt: 'Keep Icon' }, | |
| { type: 'meet', src: 'https://fonts.gstatic.com/s/i/productlogos/meet_2020q4/v1/web-96dp/logo_meet_2020q4_color_1x_web_96dp.png', alt: 'Meet Icon'}, | |
| geminiIcon, | |
| notebooklmIcon | |
| ]; | |
| let finalIconList = [ vidsIcon, vidsIcon, vidsIcon ]; | |
| for(let i=0; i < neededGemini; i++) finalIconList.push(geminiIcon); | |
| for(let i=0; i < neededNotebookLM; i++) finalIconList.push(notebooklmIcon); | |
| const neededOthers = 120 - finalIconList.length; | |
| for(let i=0; i < neededOthers; i++) { | |
| const poolWithoutTargets = iconPool.filter(icon => icon.type !== 'gemini' && icon.type !== 'notebooklm'); | |
| if (poolWithoutTargets.length > 0) { | |
| finalIconList.push(poolWithoutTargets[i % poolWithoutTargets.length]); | |
| } else { | |
| finalIconList.push(iconPool[i % iconPool.length]); | |
| } | |
| } | |
| finalIconList.sort(() => Math.random() - 0.5); | |
| finalIconList.sort(() => Math.random() - 0.5); | |
| // --- NEW: Calculate container height --- | |
| const numCols = 12; | |
| const numRows = 10; // 120 icons / 12 cols = 10 rows | |
| const containerWidth = gridContainer.offsetWidth || 700; | |
| const iconWidth = containerWidth / numCols; | |
| const iconHeight = iconWidth; // Square icons | |
| const calculatedContainerHeight = numRows * iconHeight; | |
| gridContainer.style.height = `${calculatedContainerHeight}px`; // Explicitly set height | |
| // --- END NEW --- | |
| finalIconList.forEach((icon, index) => { | |
| const img = document.createElement('img'); | |
| img.src = icon.src; | |
| img.dataset.type = icon.type; | |
| img.classList.add('icon-item'); | |
| img.alt = icon.alt; | |
| const row = Math.floor(index / numCols); | |
| const col = index % numCols; | |
| let leftPos = col * iconWidth; | |
| let topPos = row * iconHeight; | |
| // Apply boundary checks using calculated dimensions | |
| leftPos = Math.max(0, Math.min(leftPos, containerWidth - iconWidth)); | |
| topPos = Math.max(0, Math.min(topPos, calculatedContainerHeight - iconHeight)); // Use calculated height | |
| img.style.left = `${leftPos}px`; | |
| img.style.top = `${topPos}px`; | |
| img.style.width = `${iconWidth}px`; | |
| img.onerror = () => { | |
| console.warn(`Failed to load icon: ${icon.src}`); | |
| let fallbackText = icon.alt.split(' ')[0] || 'Err'; | |
| img.src = `https://placehold.co/${Math.round(iconWidth)}x${Math.round(iconHeight)}/cccccc/FFFFFF?text=${fallbackText}`; | |
| img.alt = `${icon.alt} (failed to load)`; | |
| } | |
| img.addEventListener('click', handleIconClick); | |
| gridContainer.appendChild(img); | |
| }); | |
| level8Timer = setInterval(() => { | |
| if (level8Phase === 'FINDING_VIDS') { | |
| time_taken_vids++; | |
| } | |
| if (level8Phase === 'BONUS_PHASE') { // NEW: Time bonus round | |
| level8BonusTime++; | |
| } | |
| // Always update timer display regardless of phase (or just for active phases) | |
| const totalTime = time_taken_vids + level8BonusTime; | |
| const minutes = String(Math.floor(totalTime / 60)).padStart(2, '0'); | |
| const seconds = String(totalTime % 60).padStart(2, '0'); | |
| document.getElementById('timer-8').textContent = `${minutes}:${seconds}`; | |
| }, 1000); | |
| } | |
| function handleIconClick(e) { | |
| const icon = e.target; | |
| if (level8Phase === 'MODAL' || !icon.parentNode) return; | |
| if (level8Phase === 'FINDING_VIDS') { | |
| if (icon.dataset.type === 'vids') { | |
| playSound('correct'); | |
| icon.remove(); | |
| foundVids++; | |
| document.getElementById('found-counter-8').textContent = foundVids; | |
| if (foundVids === totalVids) { | |
| level8Phase = 'MODAL'; | |
| // DO NOT clear interval yet | |
| // clearInterval(level8Timer); | |
| showVidsFoundModal(); | |
| } | |
| } else { | |
| playSound('wrong'); | |
| } | |
| } else if (level8Phase === 'BONUS_PHASE') { | |
| if (icon.dataset.type === 'gemini') { | |
| if (foundGemini < neededGemini) { | |
| playSound('correct'); | |
| // Removed: cancelIconAnimation(icon); | |
| icon.remove(); | |
| foundGemini++; | |
| document.getElementById('found-gemini-8').textContent = foundGemini; | |
| } else { | |
| playSound('wrong'); | |
| } | |
| } else if (icon.dataset.type === 'notebooklm') { | |
| if (foundNotebookLM < neededNotebookLM) { | |
| playSound('correct'); | |
| // Removed: cancelIconAnimation(icon); | |
| icon.remove(); | |
| foundNotebookLM++; | |
| document.getElementById('found-notebooklm-8').textContent = foundNotebookLM; | |
| } else { | |
| playSound('wrong'); | |
| } | |
| } else { | |
| // Do nothing for wrong clicks in bonus | |
| } | |
| if (foundGemini === neededGemini && foundNotebookLM === neededNotebookLM) { | |
| level8Phase = 'MODAL'; | |
| clearInterval(level8Timer); // NEW: Stop timer *here* | |
| handleWinLevel8BonusPhase(); | |
| } | |
| } | |
| } | |
| function useHintLevel8() { | |
| if (hint_used) return; | |
| hint_used = true; | |
| const hintBtn = document.getElementById('hint-btn-8'); | |
| hintBtn.disabled = true; | |
| hintBtn.textContent = '提示已使用'; | |
| if (level8Phase === 'FINDING_VIDS') { | |
| const vidsIcon = document.querySelector('.icon-item[data-type="vids"]'); | |
| if (vidsIcon) { | |
| vidsIcon.classList.add('hint-flash'); | |
| setTimeout(() => vidsIcon.classList.remove('hint-flash'), 3000); | |
| } | |
| } else if (level8Phase === 'BONUS_PHASE') { | |
| const geminiIcon = document.querySelector('.icon-item[data-type="gemini"]'); | |
| const notebookIcon = document.querySelector('.icon-item[data-type="notebooklm"]'); | |
| if (geminiIcon) { | |
| geminiIcon.classList.add('hint-flash'); | |
| setTimeout(() => geminiIcon.classList.remove('hint-flash'), 3000); | |
| } | |
| if (notebookIcon) { | |
| notebookIcon.classList.add('hint-flash'); | |
| setTimeout(() => notebookIcon.classList.remove('hint-flash'), 3000); | |
| } | |
| } | |
| } | |
| function showVidsFoundModal() { | |
| playSound('success'); | |
| // --- BUG FIX: Remove Modal & Auto-start Bonus Phase --- | |
| // const modalContainer = document.getElementById('level-8-modal-container'); | |
| // const modalContent = document.getElementById('level-8-modal-content'); | |
| // const modalTitle = document.getElementById('level-8-modal-title'); | |
| // const modalFeedback = document.getElementById('level-8-feedback'); | |
| // const modalButton = document.getElementById('level-8-modal-button'); | |
| // const modalSkipButton = document.getElementById('level-8-modal-button-skip'); | |
| // modalTitle.textContent = '太棒了!'; | |
| // modalFeedback.innerHTML = `您在 <strong class="text-xl text-blue-600">${time_taken_vids} 秒</strong> 內找到了 Vids!<br><br>...等等,雷達偵測到另外兩股強大的AI能量!<br><br>是否要挑戰<strong class="bonus-counter">「找出 5 個 Gemini 和 5 個 NotebookLM」</strong>的獎勵關卡?`; | |
| // modalButton.textContent = '接受挑戰!'; | |
| // modalButton.onclick = startLevel8BonusPhase; | |
| // modalSkipButton.textContent = '跳過獎勵關卡'; | |
| // modalSkipButton.style.display = 'block'; | |
| // modalSkipButton.onclick = skipLevel8BonusPhase; | |
| // modalContent.classList.add('emergency-flash'); | |
| // modalContainer.classList.remove('hidden'); | |
| // NEW: Go directly to phase 2 | |
| startLevel8BonusPhase(); | |
| // --- END BUG FIX --- | |
| } | |
| function startLevel8BonusPhase() { | |
| level8Phase = 'BONUS_PHASE'; | |
| document.getElementById('level-8-modal-container').classList.add('hidden'); | |
| document.getElementById('level-8-modal-button-skip').style.display = 'none'; | |
| // Update UI for bonus phase | |
| document.getElementById('level-8-modal-content').classList.remove('emergency-flash'); | |
| document.getElementById('level-8-title').textContent = "第八關:獎勵挑戰!"; | |
| document.getElementById('level-8-subtitle').classList.add('hidden'); | |
| document.getElementById('level-8-warning').classList.add('hidden'); | |
| document.getElementById('level-8-main-instruction').innerHTML = `在 <strong class="bonus-counter">Gemini</strong> 和 <strong class="bonus-counter">NotebookLM</strong> 消失前,盡可能找出所有 icon!`; | |
| document.getElementById('vids-counter-container').classList.add('hidden'); | |
| document.getElementById('bonus-counters-container').classList.remove('hidden'); | |
| // Reset hint button for bonus round | |
| hint_used = false; | |
| const hintBtn = document.getElementById('hint-btn-8'); | |
| hintBtn.disabled = false; | |
| hintBtn.textContent = '提示按鈕 (剩下 1 次)'; | |
| // --- NEW: Shrink non-target icons --- | |
| const allIcons = document.querySelectorAll('.icon-item'); | |
| allIcons.forEach(icon => { | |
| const type = icon.dataset.type; | |
| // BUG FIX: Shrink ALL icons | |
| icon.classList.add('shrunk'); | |
| if (type !== 'gemini' && type !== 'notebooklm') { | |
| // icon.classList.add('shrunk'); // MOVED | |
| icon.style.pointerEvents = 'none'; // Make them unclickable | |
| } else { | |
| // NEW: Ensure targets are not shrunk | |
| // icon.classList.remove('shrunk'); // REMOVED (per user request to shrink all) | |
| icon.style.pointerEvents = 'auto'; | |
| } | |
| }); | |
| } | |
| function skipLevel8BonusPhase() { | |
| level8Phase = 'MODAL'; // Set phase to modal | |
| clearInterval(level8Timer); // Stop timer | |
| // --- NEW: Scoring Logic for Skipping --- | |
| const totalLevel8Time = time_taken_vids; // Only Vids time | |
| let level8Score = 0; | |
| let titleText = ''; | |
| if (totalLevel8Time <= 10) { | |
| titleText = `S級 (${totalLevel8Time}秒)`; | |
| level8Score = 100; | |
| } else if (totalLevel8Time <= 15) { | |
| titleText = `A級 (${totalLevel8Time}秒)`; | |
| level8Score = 95; | |
| } else if (totalLevel8Time <= 20) { | |
| titleText = `B級 (${totalLevel8Time}秒)`; | |
| level8Score = 90; | |
| } else { | |
| titleText = `C級 (${totalLevel8Time}秒)`; | |
| level8Score = 85; | |
| } | |
| // OLD: addScore(level8Score); | |
| recordLevelScore(8, level8Score, totalLevel8Time <= 10); // S-Class counts as "first try" | |
| // --- END: Scoring Logic --- | |
| const modalContainer = document.getElementById('level-8-modal-container'); | |
| const modalTitle = document.getElementById('level-8-modal-title'); | |
| const modalFeedback = document.getElementById('level-8-feedback'); | |
| const modalButton = document.getElementById('level-8-modal-button'); | |
| const modalSkipButton = document.getElementById('level-8-modal-button-skip'); | |
| modalTitle.textContent = `跳過獎勵關卡 (${titleText})`; // Show rank | |
| modalFeedback.textContent = "已記錄您的 Vids 成績。正在前往下一關..."; | |
| modalButton.textContent = '前往第九關'; | |
| modalButton.onclick = closeLevel8ModalAndAdvance; | |
| modalSkipButton.style.display = 'none'; | |
| } | |
| /* --- (FIXED: Added function wrapper) --- */ | |
| function handleWinLevel8BonusPhase() { | |
| playSound('success'); | |
| // --- NEW: Scoring Logic --- | |
| const totalLevel8Time = time_taken_vids + level8BonusTime; | |
| let level8Score = 0; | |
| let titleText = ''; | |
| // (Vids time + Bonus time) | |
| if (totalLevel8Time <= 10) { | |
| titleText = `S級 (${totalLevel8Time}秒)`; | |
| level8Score = 100; | |
| } else if (totalLevel8Time <= 15) { | |
| titleText = `A級 (${totalLevel8Time}秒)`; | |
| level8Score = 95; | |
| } else if (totalLevel8Time <= 20) { | |
| titleText = `B級 (${totalLevel8Time}秒)`; | |
| level8Score = 90; | |
| } else { | |
| titleText = `C級 (${totalLevel8Time}D`; | |
| level8Score = 85; | |
| } | |
| // OLD: addScore(level8Score); | |
| recordLevelScore(8, level8Score, totalLevel8Time <= 10); // S-Class counts as "first try" | |
| // --- END: Scoring Logic --- | |
| // This is the fixed feedback text from Request 4 | |
| const feedbackText = `太強了!您在 ${time_taken_vids} 秒內找到 Vids,並在 ${level8BonusTime} 秒內完成獎勵關卡!總計 ${totalLevel8Time} 秒!`; | |
| const modalContainer = document.getElementById('level-8-modal-container'); | |
| const modalContent = document.getElementById('level-8-modal-content'); | |
| const modalTitle = document.getElementById('level-8-modal-title'); | |
| const modalFeedback = document.getElementById('level-8-feedback'); | |
| const modalButton = document.getElementById('level-8-modal-button'); | |
| modalTitle.textContent = `獎勵挑戰完成! (${titleText})`; // Show rank | |
| modalFeedback.textContent = feedbackText; | |
| modalButton.textContent = '前往第九關'; | |
| modalButton.onclick = closeLevel8ModalAndAdvance; | |
| modalContent.classList.remove('emergency-flash'); | |
| modalContainer.classList.remove('hidden'); | |
| } | |
| /* --- (END OF FIX) --- */ | |
| function closeLevel8ModalAndAdvance() { | |
| document.getElementById('level-8-modal-container').classList.add('hidden'); | |
| // OLD: updateProgressBar(8); // NEW: Mark level 8 complete (Handled by recordLevelScore) | |
| // OLD: goToScreen(9); // UPDATED: Go to the new poll screen (9) | |
| if (isReplaying) goToScreen(11); // NEW: Handle replay nav | |
| else goToScreen(9); | |
| } | |
| // --- NEW: Level 9 Poll Game Logic --- | |
| function initPollGame() { | |
| pollClickCount = 0; // Already global | |
| let geminiLikes = 0; | |
| let notebookLMLikes = 0; | |
| let timeRemaining = 20; | |
| const timerEl = document.getElementById('poll-timer'); | |
| const clicksEl = document.getElementById('poll-clicks'); | |
| const geminiImg = document.getElementById('gemini-img'); | |
| const notebookImg = document.getElementById('notebooklm-img'); | |
| const geminiLikesEl = document.getElementById('gemini-likes-display'); | |
| const notebookLikesEl = document.getElementById('notebooklm-likes-display'); | |
| // Reset UI | |
| timerEl.textContent = timeRemaining; | |
| clicksEl.textContent = pollClickCount; | |
| geminiLikesEl.textContent = `❤️ Gemini 人氣: 0`; | |
| notebookLikesEl.textContent = `❤️ NotebookLM 人氣: 0`; | |
| // Clear old intervals if any | |
| if (pollCountdownInterval) clearInterval(pollCountdownInterval); | |
| if (pollGameTimer) clearTimeout(pollGameTimer); | |
| function handleClick(e, type) { | |
| if (timeRemaining <= 0) return; | |
| pollClickCount++; | |
| clicksEl.textContent = pollClickCount; | |
| createFloatingHeart(e); // Create visual effect | |
| playSound('correct'); // Play click sound | |
| if (type === 'gemini') { | |
| geminiLikes++; | |
| geminiLikesEl.textContent = `❤️ Gemini 人氣: ${geminiLikes}`; | |
| } else { | |
| notebookLMLikes++; | |
| notebookLikesEl.textContent = `❤️ NotebookLM 人氣: ${notebookLMLikes}`; | |
| } | |
| } | |
| // --- Re-add event listeners --- | |
| // Clone and replace to remove old listeners | |
| const newGeminiImg = geminiImg.cloneNode(true); | |
| geminiImg.parentNode.replaceChild(newGeminiImg, geminiImg); | |
| newGeminiImg.addEventListener('click', (e) => handleClick(e, 'gemini')); | |
| const newNotebookImg = notebookImg.cloneNode(true); | |
| notebookImg.parentNode.replaceChild(newNotebookImg, notebookImg); | |
| newNotebookImg.addEventListener('click', (e) => handleClick(e, 'notebooklm')); | |
| // --- End re-add --- | |
| // Start Countdown | |
| pollCountdownInterval = setInterval(() => { | |
| timeRemaining--; | |
| timerEl.textContent = timeRemaining; | |
| if (timeRemaining <= 0) { | |
| stopPollGame(); | |
| } | |
| }, 1000); | |
| // Set game end timer | |
| pollGameTimer = setTimeout(() => { | |
| stopPollGame(); | |
| }, 20000); | |
| } | |
| function stopPollGame() { | |
| // --- BUG FIX: Add guard to prevent double-trigger --- | |
| if (pollGameTimer === null) return; | |
| if (pollCountdownInterval) clearInterval(pollCountdownInterval); | |
| if (pollGameTimer) clearTimeout(pollGameTimer); | |
| pollGameTimer = null; // Prevent multiple triggers | |
| // Calculate score | |
| let pollScore = 0; | |
| if (pollClickCount > 80) { | |
| pollScore = 100; | |
| } else if (pollClickCount >= 65) { | |
| pollScore = 90; | |
| } else { | |
| pollScore = 80; | |
| } | |
| // OLD: addScore(pollScore); | |
| recordLevelScore(9, pollScore, pollScore === 100); // 100-score counts as "first try" | |
| // Show end modal after a short delay | |
| setTimeout(() => showPollEndModal(pollScore, pollClickCount), 500); // NEW: Pass score | |
| } | |
| // --- Level 8 Modal Logic (Shared Modals) --- | |
| <!-- (FIX) This function was missing. I am re-adding it here. --> | |
| <!-- It belongs with the other modal functions --> | |
| function showLevel4SuccessModal() { | |
| playSound('success'); | |
| // --- BUG FIX: Add score and progress bar update --- | |
| const score = level4ResetUsed ? 75 : 100; | |
| recordLevelScore(4, score, !level4ResetUsed); | |
| // --- END BUG FIX --- | |
| const modalContainer = document.getElementById('level-8-modal-container'); | |
| const modalContent = document.getElementById('level-8-modal-content'); | |
| const modalTitle = document.getElementById('level-8-modal-title'); | |
| const modalFeedback = document.getElementById('level-8-feedback'); | |
| const modalButton = document.getElementById('level-8-modal-button'); | |
| modalTitle.textContent = '能量注入完成!'; | |
| modalFeedback.textContent = '沒錯,就是「Chromebook Plus」!'; | |
| modalButton.textContent = '前往下一關 (第五關)'; | |
| modalButton.onclick = () => { | |
| hideLevel4Modal(); | |
| if (isReplaying) goToScreen(11); // NEW: Handle replay nav | |
| else goToScreen(5); | |
| }; | |
| modalContent.classList.remove('emergency-flash'); // Cleanup | |
| /* --- (FIXED: This was the part that caused the 'else' error) --- */ | |
| /* | |
| This block was incorrectly copy-pasted. | |
| */ | |
| /* --- (END OF FIX) --- */ | |
| modalContent.classList.add('success-flash'); // NEW | |
| modalContainer.classList.remove('hidden'); | |
| } | |
| <!-- (END OF FIX) --> | |
| function hideLevel4Modal() { // NEW: Specific for L4 | |
| document.getElementById('level-8-modal-container').classList.add('hidden'); | |
| document.getElementById('level-8-modal-content').classList.remove('success-flash'); // NEW | |
| } | |
| function showPollEndModal(pollScore, pollClickCount) { // NEW: Accept score | |
| const modalContainer = document.getElementById('level-8-modal-container'); | |
| const modalContent = document.getElementById('level-8-modal-content'); | |
| const modalTitle = document.getElementById('level-8-modal-title'); | |
| const modalFeedback = document.getElementById('level-8-feedback'); | |
| const modalButton = document.getElementById('level-8-modal-button'); | |
| modalTitle.textContent = '投票結束!'; | |
| modalFeedback.textContent = `你在20秒內點擊了 ${pollClickCount} 次!獲得 ${pollScore} 分!`; // NEW: Show score | |
| modalButton.textContent = '前往最終挑戰!'; // UPDATED | |
| modalButton.onclick = () => { | |
| modalContainer.classList.add('hidden'); | |
| // OLD: updateProgressBar(9); // NEW: Mark level 9 complete (Handled by recordLevelScore) | |
| // OLD: goToScreen(10); // Go to NEW ethics screen (10) | |
| if (isReplaying) goToScreen(11); // NEW: Handle replay nav | |
| else goToScreen(10); | |
| }; | |
| modalContent.classList.remove('emergency-flash'); | |
| modalContent.classList.remove('success-flash'); // NEW: Cleanup | |
| modalContainer.classList.remove('hidden'); | |
| } | |
| function createFloatingHeart(event) { | |
| const heart = document.createElement('div'); | |
| heart.innerHTML = '❤️'; | |
| heart.className = 'floating-heart'; | |
| const screenRect = document.getElementById('screen-9').getBoundingClientRect(); | |
| const x = event.clientX - screenRect.left; | |
| const y = event.clientY - screenRect.top; | |
| heart.style.left = `${x - 16}px`; // Adjust for heart size | |
| heart.style.top = `${y - 16}px`; | |
| document.getElementById('screen-9').appendChild(heart); | |
| // Trigger animation | |
| setTimeout(() => { | |
| heart.style.transform = 'translateY(-100px)'; | |
| heart.style.opacity = '0'; | |
| }, 10); | |
| // Remove from DOM | |
| setTimeout(() => { | |
| heart.remove(); | |
| }, 1600); | |
| } | |
| // --- END: Level 9 Poll Game Logic --- | |
| // --- NEW: Level 10 Ethics Game Logic --- | |
| const level10Cards = [ | |
| { | |
| id: 0, | |
| title: "激發靈感", | |
| content: "學生使用 Gemini 針對特定主題(例如:環保)發想點子,產生心智圖或不同觀點。", | |
| correctZone: "green", | |
| hints: { | |
| red: "哎呀,太嚴格了!激發創意是『綠燈區』鼓勵的用法喔。記住,AI 很適合作為創意的『起點』,而不是『終點』!", | |
| yellow: "哎呀,太嚴格了!激發創意是『綠燈區』鼓勵的用法喔。記住,AI 很適合作為創意的『起點』,而不是『終點』!" | |
| }, | |
| correctHint: "你抓到重點了!AI 是很棒的『個人化學習』工具,能幫助學生真正理解,是『綠燈區』鼓勵的用法。" // NEW | |
| }, | |
| { | |
| id: 1, | |
| title: "正式考試", | |
| content: "學生在期末考的申論題中,使用 AI 工具來撰寫答案。", | |
| correctZone: "red", | |
| hints: { | |
| yellow: "等等!這太危險了!為了確保學習的真實性與公平性,在『正式考試』中,原則上『一律禁止』使用 AI。", | |
| green: "等等!這太危險了!為了確保學習的真實性與公平性,在『正式考試』中,原則上『一律禁止』使用 AI。" | |
| }, | |
| correctHint: "完全正確!為了確保公平性與學術誠信,『正式考試』中原則上禁止使用 AI。" | |
| }, | |
| { | |
| id: 2, | |
| title: "輔助除錯 (Debug)", | |
| content: "老師在程式設計課中,指導學生如何使用 AI 尋求程式碼的錯誤,並理解其建議。", | |
| correctZone: "yellow", | |
| hints: { | |
| red: "也不用完全禁止啦!在『老師的指引』下,AI 是個很好的學習助手。這應該是『黃燈區』的範圍。", | |
| green: "很接近了!但因為需要『老師的指引』來確保學生真正理解,而不只是盲目複製,所以這屬於『黃燈區』喔!" | |
| }, | |
| correctHint: "答對了!關鍵在於學生必須能『解釋』AI的建議,而不只是複製貼上,因此需要老師指引。" | |
| }, | |
| { | |
| id: 3, | |
| title: "最終成果", | |
| content: "學生將 AI 生成的完整文章,當作自己的「最終專題報告」繳交。", | |
| correctZone: "red", | |
| hints: { | |
| yellow: "不行!『最終成果』或『專題報告』強調的是個人原創性。過度依賴 AI 會削弱核心能力的培養,這屬於『紅燈區』喔!", | |
| green: "不行!『最終成果』或『專題報告』強調的是個人原創性。過度依賴 AI 會削弱核心能力的培養,這屬於『紅燈區』喔!" | |
| }, | |
| correctHint: "沒錯!『最終成果』強調個人觀點與創造力,不能完全依賴 AI,這會削弱核心能力的培養。" | |
| }, | |
| { | |
| id: 4, | |
| title: "角色扮演", | |
| content: "老師設計情境,讓 AI 扮演特定角色(如:科學家、歷史人物),學生與其進行訪談。", | |
| correctZone: "yellow", | |
| hints: { | |
| red: "別擔心!在老師的『規範與引導』下,AI 角色扮演是很好的『黃燈區』應用,可以深化學習。", | |
| green: "差一點!这个情境需要『老師設計』與『引導』學生進行有深度的討論,所以是需要師長指引的『黃燈區』。" | |
| }, | |
| correctHint: "非常準確!『黃燈區』的重點在於『老師的設計與引導』,才能提出有深度的問題。" | |
| }, | |
| { | |
| id: 5, | |
| title: "個人學習", | |
| content: "學生使用可汗學院 (Khan Academy) 等具備 AI 功能的平台,進行客製化的學習路徑與練習。", | |
| correctZone: "green", | |
| hints: { | |
| red: "別怕!AI 在『個人化學習與練習』上是個好夥伴,這是『綠燈區』鼓勵的用法,目的是幫助學生真正理解學習內容。", | |
| yellow: "別怕!AI 在『個人化學習與練習』上是個好夥伴,這是『綠燈區』鼓勵的用法,目的是幫助學生真正理解學習內容。" | |
| }, | |
| correctHint: "你抓到重點了!AI 是很棒的『個人化學習』工具,能幫助學生真正理解,是『綠燈區』鼓勵的用法。" | |
| } | |
| ]; | |
| // --- NEW: Bonus Cards --- | |
| const level10BonusCards = [ | |
| { | |
| id: 6, | |
| title: "涉及個人隱私", | |
| content: "老師引導學生將自己的家庭住址、電話等個資輸入 AI,請 AI 幫忙規劃週末出遊。", | |
| correctZone: "red", | |
| hints: { | |
| yellow: "非常危險!『個人隱私』資料絕對不可輸入公開的 AI 模型,這是『紅燈區』的基本原則!", | |
| green: "非常危險!『個人隱私』資料絕對不可輸入公開的 AI 模型,這是『紅燈區』的基本原則!" | |
| }, | |
| correctHint: "完全正確!『個人隱私』資料絕對不可輸入公開的 AI 模型,這是『紅燈區』的基本原則!" | |
| }, | |
| { | |
| id: 7, | |
| title: "事實核查", | |
| content: "老師請學生直接詢問 AI 某個爭議性新聞的「標準答案」,並以此作為唯一的報告來源。", | |
| correctZone: "red", | |
| hints: { | |
| yellow: "不行喔!『事實核查』的核心是培養思辨能力,應先由學生獨立完成,而非直接尋求 AI 的『標準答案』。", | |
| green: "不行喔!『事實核查』的核心是培養思辨能力,應先由學生獨立完成,而非直接尋求 AI 的『標準答案』。" | |
| }, | |
| correctHint: "答對了!『事實核查』應由學生獨立完成,AI 只能當作對照的觀點之一,不能當作『標準答案』。" | |
| }, | |
| { | |
| id: 8, | |
| title: "草稿撰寫", | |
| content: "在老師指導下,學生使用 AI 生成文章大綱,並學習如何「下指令」(Prompting) 來修改與重寫內容。", | |
| correctZone: "yellow", | |
| hints: { | |
| red: "不用這麼嚴格!在老師指導下,學習如何『下指令』重寫草稿,是『黃燈區』很好的練習喔。", | |
| green: "很接近!但因為這需要『老師指導』學生如何下指令及修改,所以是『黃燈區』。" | |
| }, | |
| correctHint: "沒錯!在『老師指導』下學習 Prompting 和修改草稿,是『黃燈區』的絕佳應用。" | |
| }, | |
| { | |
| id: 9, | |
| title: "數據分析", | |
| content: "在專題研究中,老師引導學生使用 AI 工具協助處理數據、製作圖表,並『解釋圖表背後的意義』。", | |
| correctZone: "yellow", | |
| hints: { | |
| red: "太可惜了!在老師引導下,AI 是處理數據的好幫手,只要學生能『解釋意義』,就是很好的『黃燈區』應用。", | |
| green: "差一點!關鍵在於『老師的引導』與學生必須『解釋意義』,而不只是操作工具,所以是『黃燈區』。" | |
| }, | |
| correctHint: "答對了!重點是學生必須能『解釋圖表意義』,而不只是當個操作工具的『黑手』。" | |
| }, | |
| { | |
| id: 10, | |
| title: "資料整理", | |
| content: "學生利用 AI 快速彙整大量資料,作為研究的背景知識,並在老師指導下學習『驗證資訊真偽』。", | |
| correctZone: "green", | |
| hints: { | |
| red: "別擔心!AI 用在『資料整理』是『綠燈區』的好用法,只要記得去『驗證真偽』就沒問題了!", | |
| yellow: "別擔心!AI 用在『資料整理』是『綠燈區』的好用法,只要記得去『驗證真偽』就沒問題了!" | |
| }, | |
| correctHint: "完全正確!AI 是『資料整理』的好幫手,是『綠燈區』的應用,但別忘了要『驗證真偽』喔!" | |
| }, | |
| { | |
| id: 11, | |
| title: "語言學習發音", | |
| content: "學生利用 AI 進行外語口說練習,獲得即時的發音與文法反饋,並內化為自己的語言能力。", | |
| correctZone: "green", | |
| hints: { | |
| red: "別怕!AI 是提升語言溝通能力的絕佳夥伴,這是『綠燈區』鼓勵的用法!", | |
| yellow: "別怕!AI 是提升語言溝通能力的絕佳夥伴,這是『綠燈區』鼓勵的用法!" | |
| }, | |
| correctHint: "太棒了!AI 用在『語言學習』是『綠燈區』的絕佳應用,能有效提升溝通能力。" | |
| } | |
| ]; | |
| let currentCardIndex_L10 = 0; | |
| let correctCount_L10 = 0; | |
| let draggableItem_L10 = null; | |
| let touchDragClone_L10 = null; | |
| const l10DropZones = []; // Will populate in init | |
| // let level10Score = 0; // Already global | |
| // let level10FirstTry = true; // Already global | |
| // let level10CurrentCardTries = 0; // Already global | |
| function initLevel10() { | |
| currentCardIndex_L10 = 0; | |
| correctCount_L10 = 0; | |
| draggableItem_L10 = null; | |
| touchDragClone_L10 = null; | |
| level10CurrentCardTries = 0; // NEW: Reset card tries | |
| level10Score = 0; // NEW: Reset L10 score | |
| level10FirstTry = true; // NEW: Reset L10 first try flag | |
| level10Cards.splice(6); // IMPORTANT: Reset to only 6 cards on init | |
| document.getElementById('l10-progress-total').textContent = '6'; // Reset total | |
| updateLevel10Progress(); | |
| // Clear any old listeners by cloning nodes | |
| l10DropZones.forEach(zone => { | |
| const newZone = zone.cloneNode(true); | |
| zone.parentNode.replaceChild(newZone, zone); | |
| }); | |
| l10DropZones.length = 0; // Empty the array | |
| // Setup Drop Zones | |
| const zones = ['l10-red-zone', 'l10-yellow-zone', 'l10-green-zone']; | |
| zones.forEach(id => { | |
| const zone = document.getElementById(id); | |
| if (zone) { | |
| zone.addEventListener('dragover', e => { | |
| e.preventDefault(); | |
| zone.classList.add('over'); | |
| }); | |
| zone.addEventListener('dragleave', handleL10DragLeave); | |
| zone.addEventListener('drop', handleL10Drop); | |
| l10DropZones.push(zone); | |
| } | |
| }); | |
| showLevel10Card(); | |
| } | |
| function handleL10DragLeave(e) { | |
| e.currentTarget.classList.remove('over'); | |
| } | |
| function updateLevel10Progress() { | |
| document.getElementById('l10-progress-counter').textContent = correctCount_L10; | |
| } | |
| function showLevel10Card() { | |
| const container = document.getElementById('l10-card-container'); | |
| container.innerHTML = ''; // Clear previous card | |
| // --- NEW: Check for bonus round trigger --- | |
| if (currentCardIndex_L10 === 6 && level10Cards.length === 6) { | |
| // Just finished the first 6 cards | |
| setTimeout(showLevel10BonusModal, 300); | |
| return; // Stop here, wait for user input | |
| } | |
| if (currentCardIndex_L10 >= level10Cards.length) { | |
| // Game complete (either 6 or 12) | |
| // --- NEW: Check if this is final completion --- | |
| if (!(currentCardIndex_L10 === 6 && level10Cards.length === 6)) { | |
| // This means we are at 12/12, or we were at 6/6 and skipped bonus | |
| playSound('success'); | |
| // OLD: updateProgressBar(10); // NEW: Mark level 10 complete | |
| recordLevelScore(10, level10Score, level10FirstTry); // NEW: Record L10 score | |
| setTimeout(() => goToScreen(11), 1000); // Go to final screen (11) | |
| } | |
| return; | |
| } | |
| const cardData = level10Cards[currentCardIndex_L10]; | |
| const card = document.createElement('div'); | |
| card.id = `l10-card-${cardData.id}`; | |
| card.classList.add('l10-card'); | |
| card.setAttribute('draggable', 'true'); | |
| card.dataset.cardId = cardData.id; | |
| card.innerHTML = ` | |
| <h4 class="text-xl font-bold mb-2 text-gray-800">${cardData.title}</h4> | |
| <p class="text-gray-600">${cardData.content}</p> | |
| `; | |
| // Mouse Drag Events | |
| card.addEventListener('dragstart', handleL10DragStart); | |
| card.addEventListener('dragend', handleL10DragEnd); | |
| // Touch Drag Events | |
| card.addEventListener('touchstart', handleL10TouchStart, { passive: false }); | |
| card.addEventListener('touchmove', handleL10TouchMove, { passive: false }); | |
| card.addEventListener('touchend', handleL10TouchEnd); | |
| container.appendChild(card); | |
| } | |
| function handleL10DragStart(e) { | |
| draggableItem_L10 = e.target; | |
| e.dataTransfer.setData('text/plain', e.target.dataset.cardId); | |
| setTimeout(() => e.target.classList.add('dragging'), 0); | |
| } | |
| function handleL10DragEnd(e) { | |
| if(e.target) e.target.classList.remove('dragging'); | |
| draggableItem_L10 = null; | |
| l10DropZones.forEach(zone => zone.classList.remove('over')); | |
| } | |
| function handleL10TouchStart(e) { | |
| draggableItem_L10 = e.currentTarget; | |
| // Create clone | |
| touchDragClone_L10 = draggableItem_L10.cloneNode(true); | |
| touchDragClone_L10.style.position = 'absolute'; | |
| touchDragClone_L10.style.zIndex = '1000'; | |
| touchDragClone_L10.style.opacity = '0.8'; | |
| touchDragClone_L10.style.pointerEvents = 'none'; | |
| document.body.appendChild(touchDragClone_L10); | |
| const touch = e.touches[0]; | |
| touchDragClone_L10.style.width = `${draggableItem_L10.offsetWidth}px`; // Match width | |
| touchDragClone_L10.style.left = `${touch.clientX - touchDragClone_L10.offsetWidth / 2}px`; | |
| touchDragClone_L10.style.top = `${touch.clientY - touchDragClone_L10.offsetHeight / 2}px`; | |
| draggableItem_L10.classList.add('dragging'); | |
| } | |
| function handleL10TouchMove(e) { | |
| e.preventDefault(); | |
| if (!touchDragClone_L10) return; | |
| const touch = e.touches[0]; | |
| touchDragClone_L10.style.left = `${touch.clientX - touchDragClone_L10.offsetWidth / 2}px`; | |
| touchDragClone_L10.style.top = `${touch.clientY - touchDragClone_L10.offsetHeight / 2}px`; | |
| l10DropZones.forEach(zone => { | |
| const rect = zone.getBoundingClientRect(); | |
| if (touch.clientX > rect.left && touch.clientX < rect.right && | |
| touch.clientY > rect.top && touch.clientY < rect.bottom) { | |
| zone.classList.add('over'); | |
| } else { | |
| zone.classList.remove('over'); | |
| } | |
| }); | |
| } | |
| function handleL10TouchEnd(e) { | |
| if (!touchDragClone_L10) return; | |
| const touch = e.changedTouches[0]; | |
| touchDragClone_L10.style.display = 'none'; // Hide clone temporarily | |
| const dropTarget = document.elementFromPoint(touch.clientX, touch.clientY); | |
| touchDragClone_L10.style.display = 'block'; // Show again | |
| let actualZone = dropTarget ? dropTarget.closest('.l10-drop-zone') : null; | |
| if (actualZone) { | |
| processL10Drop(draggableItem_L10, actualZone); | |
| } | |
| document.body.removeChild(touchDragClone_L10); | |
| touchDragClone_L10 = null; | |
| if(draggableItem_L10) draggableItem_L10.classList.remove('dragging'); | |
| l10DropZones.forEach(zone => zone.classList.remove('over')); | |
| draggableItem_L10 = null; | |
| } | |
| function handleL10Drop(e) { | |
| e.preventDefault(); | |
| const zone = e.currentTarget; | |
| zone.classList.remove('over'); | |
| // Find the dragged item (either from mouse or touch) | |
| let droppedCard = draggableItem_L10; | |
| if (!droppedCard) { | |
| const cardId = e.dataTransfer.getData('text/plain'); | |
| droppedCard = document.querySelector(`[data-card-id="${cardId}"]`); | |
| } | |
| if (droppedCard) { | |
| processL10Drop(droppedCard, zone); | |
| } | |
| draggableItem_L10 = null; // Clear item after drop | |
| } | |
| function processL10Drop(card, zone) { | |
| if (!card) return; | |
| const cardId = card.dataset.cardId; | |
| const cardData = level10Cards.find(c => c.id == cardId); | |
| let droppedZoneType; | |
| if (zone.id === 'l10-red-zone') droppedZoneType = 'red'; | |
| else if (zone.id === 'l10-yellow-zone') droppedZoneType = 'yellow'; | |
| else if (zone.id === 'l10-green-zone') droppedZoneType = 'green'; | |
| if (cardData.correctZone === droppedZoneType) { | |
| // --- CORRECT --- | |
| playSound('success'); | |
| // NEW: Add score | |
| // OLD: addScore(level10CurrentCardTries === 0 ? 10 : 5); | |
| if (level10CurrentCardTries > 0) level10FirstTry = false; // Mark as not first try | |
| level10Score += (level10CurrentCardTries === 0 ? 10 : 5); // Add to partial score | |
| const hint = cardData.correctHint || "答對了!"; | |
| showLevel10Modal(hint, true); // Show "Correct" modal | |
| } else { | |
| // --- INCORRECT --- | |
| playSound('wrong'); | |
| level10CurrentCardTries++; // NEW: Increment wrong tries | |
| zone.classList.add('flash-red'); | |
| setTimeout(() => zone.classList.remove('flash-red'), 1400); | |
| // Show modal with the specific hint | |
| const hint = cardData.hints[droppedZoneType]; | |
| showLevel10Modal(hint, false); // Show "Incorrect" modal | |
| } | |
| } | |
| function showLevel10Modal(message, isCorrect) { | |
| const modal = document.getElementById('level-10-modal-container'); | |
| const title = document.getElementById('level-10-modal-title'); | |
| const content = document.getElementById('level-10-modal-content'); | |
| const feedback = document.getElementById('level-10-feedback'); | |
| const button = document.getElementById('level-10-modal-button'); | |
| feedback.textContent = message; | |
| if (isCorrect) { | |
| title.textContent = "答對了!"; | |
| title.className = "text-2xl font-bold mb-4 text-green-600"; | |
| content.className = content.className.replace('border-red-500', 'border-green-500'); | |
| button.textContent = "下一題"; | |
| button.onclick = () => hideLevel10Modal(true); | |
| } else { | |
| title.textContent = "哎呀,不對喔!"; | |
| title.className = "text-2xl font-bold mb-4 text-red-600"; | |
| content.className = content.className.replace('border-green-500', 'border-red-500'); // Ensure it's red | |
| button.textContent = "再試一次"; | |
| button.onclick = () => hideLevel10Modal(false); | |
| } | |
| modal.classList.remove('hidden'); | |
| } | |
| function hideLevel10Modal(isCorrect) { | |
| document.getElementById('level-10-modal-container').classList.add('hidden'); | |
| if (isCorrect) { | |
| correctCount_L10++; | |
| updateLevel10Progress(); | |
| level10CurrentCardTries = 0; // NEW: Reset tries for next card | |
| // Find and remove the card that was just answered | |
| const card = document.querySelector(`[data-card-id="${level10Cards[currentCardIndex_L10].id}"]`); | |
| if (card) card.remove(); | |
| currentCardIndex_L10++; | |
| setTimeout(showLevel10Card, 300); // Show next card (or bonus modal) | |
| } | |
| // if !isCorrect, do nothing, user tries again | |
| } | |
| function showLevel10BonusModal() { | |
| const modal = document.getElementById('level-8-modal-container'); | |
| const title = document.getElementById('level-8-modal-title'); | |
| const feedback = document.getElementById('level-8-feedback'); | |
| const button = document.getElementById('level-8-modal-button'); | |
| const skipButton = document.getElementById('level-8-modal-button-skip'); | |
| title.textContent = "挑戰完成!"; | |
| feedback.textContent = "太棒了,你已完成基本審核!是否要挑戰 6 個額外的「進階情境」?"; | |
| button.textContent = "開始挑戰!"; | |
| button.onclick = startLevel10Bonus; | |
| skipButton.textContent = "以後再說"; // Just in case | |
| skipButton.style.display = 'block'; | |
| skipButton.onclick = skipLevel10Bonus; | |
| modal.classList.remove('hidden'); | |
| } | |
| function startLevel10Bonus() { | |
| document.getElementById('level-8-modal-container').classList.add('hidden'); | |
| document.getElementById('level-8-modal-button-skip').style.display = 'none'; | |
| level10Cards.push(...level10BonusCards); // Add the 6 bonus cards | |
| document.getElementById('l10-progress-total').textContent = '12'; // Update total | |
| level10CurrentCardTries = 0; // NEW: Reset tries | |
| showLevel10Card(); // This will now show card index 6 | |
| } | |
| function skipLevel10Bonus() { | |
| document.getElementById('level-8-modal-container').classList.add('hidden'); | |
| document.getElementById('level-8-modal-button-skip').style.display = 'none'; | |
| // OLD: updateProgressBar(10); // NEW: Mark level 10 complete even if skipped | |
| recordLevelScore(10, level10Score, level10FirstTry); // NEW: Record L10 score (even if just 60) | |
| goToScreen(11); // Go to final completion screen | |
| } | |
| // --- END: Level 10 Ethics Game Logic --- | |
| // --- NEW: Scoreboard Logic --- | |
| async function showScoreboard() { | |
| if (!isFirebaseReady || !fbSDK) { | |
| console.warn("Firebase not ready. Cannot show scoreboard."); | |
| // alert() is blocked, just log to console | |
| return; | |
| } | |
| // 重複使用 L8 的彈出視窗 | |
| const modalContainer = document.getElementById('level-8-modal-container'); | |
| const modalContent = document.getElementById('level-8-modal-content'); | |
| const modalTitle = document.getElementById('level-8-modal-title'); | |
| const modalFeedback = document.getElementById('level-8-feedback'); | |
| const modalButton = document.getElementById('level-8-modal-button'); | |
| const modalSkipButton = document.getElementById('level-8-modal-button-skip'); | |
| // 1. 顯示讀取中... | |
| modalTitle.textContent = "排行榜 (今日戰況)"; | |
| modalFeedback.innerHTML = '<p class="text-lg">讀取中...</p>'; | |
| modalButton.textContent = "關閉"; | |
| modalButton.onclick = () => modalContainer.classList.add('hidden'); | |
| modalSkipButton.style.display = 'none'; | |
| modalContent.classList.remove('emergency-flash', 'success-flash'); | |
| modalContainer.classList.remove('hidden'); | |
| try { | |
| // 2. 取得「今天凌晨 0 點」的時間戳記 | |
| const now = new Date(); | |
| const startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0); | |
| const startOfTodayTimestamp = fbSDK.Timestamp.fromDate(startOfDay); | |
| // 3. 查詢 Firebase | |
| const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id'; | |
| const scoresCollectionRef = fbSDK.collection(db, `artifacts/${appId}/public/data/scores`); | |
| // 查詢指令:找出 "lastUpdated" >= "今天凌晨" 的所有文件 | |
| const q = fbSDK.query(scoresCollectionRef, fbSDK.where("lastUpdated", ">=", startOfTodayTimestamp)); | |
| const querySnapshot = await fbSDK.getDocs(q); | |
| let scores = []; | |
| querySnapshot.forEach((doc) => { | |
| scores.push(doc.data()); | |
| }); | |
| // 4. 在 JavaScript 中進行排序(由高到低) | |
| scores.sort((a, b) => b.score - a.score); | |
| // 5. 建立排行榜的 HTML 內容 | |
| let leaderboardHtml = '<ol class="list-decimal list-inside text-left space-y-2 max-h-60 overflow-y-auto">'; | |
| if (scores.length === 0) { | |
| leaderboardHtml = '<p class="text-lg">今天還沒有人完成遊戲!</p>'; | |
| } else { | |
| // --- FIX: 移除 .slice(0, 10) 來顯示當天所有成績 --- | |
| scores.forEach((entry, index) => { | |
| leaderboardHtml += ` | |
| <li class="p-2 rounded ${index === 0 ? 'bg-yellow-100 border border-yellow-300' : ''}"> | |
| <span class="font-bold text-lg text-blue-600">${entry.score} 分</span> - | |
| <span class="text-gray-700">${entry.displayName}</span> | |
| </li> | |
| `; | |
| }); | |
| } | |
| leaderboardHtml += '</ol>'; | |
| modalFeedback.innerHTML = leaderboardHtml; | |
| } catch (error) { | |
| console.error("Error fetching scoreboard:", error); | |
| modalFeedback.innerHTML = '<p class="text-red-500">讀取排行榜時發生錯誤。</p>'; | |
| } | |
| } | |
| // --- END NEW --- | |
| </script> | |
| </body> | |
| </html> | |