Spaces:
Running
Running
Upload 102 files
Browse files- .gitattributes +7 -0
- Assets/triangle/Case/case1.png +3 -0
- Assets/triangle/Case/case2.png +3 -0
- Assets/triangle/Case/case3.png +3 -0
- Assets/triangle/detective.svg +40 -0
- Assets/triangle/嫌疑犯/16造型師特徵.png +3 -0
- Assets/triangle//345/253/214/347/226/221/347/212/257/27/345/244/252/347/251/272/344/272/272/347/231/275.png +0 -0
- README.md +142 -141
- congruence_detective.html +1413 -0
- example/algebra.html +578 -0
- example/functionex1.png +3 -0
- example/functionex2.png +3 -0
- example/functionex3.png +3 -0
- example/gemstone.html +694 -0
- example/harbor.html +697 -0
- example/index.html +319 -0
- example/philosophy.html +431 -0
- function.html +15 -7
- index.html +9 -4
- sequence.html +0 -0
- skills.md +100 -0
- skyscraper.html +1005 -0
.gitattributes
CHANGED
|
@@ -131,3 +131,10 @@ Assets/triangle/嫌疑犯/7候選人.png filter=lfs diff=lfs merge=lfs -text
|
|
| 131 |
Assets/triangle/嫌疑犯/8列車長.png filter=lfs diff=lfs merge=lfs -text
|
| 132 |
Assets/triangle/嫌疑犯/9_DJ.png filter=lfs diff=lfs merge=lfs -text
|
| 133 |
Assets/triangle/嫌疑犯/嫌疑犯.png filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
Assets/triangle/嫌疑犯/8列車長.png filter=lfs diff=lfs merge=lfs -text
|
| 132 |
Assets/triangle/嫌疑犯/9_DJ.png filter=lfs diff=lfs merge=lfs -text
|
| 133 |
Assets/triangle/嫌疑犯/嫌疑犯.png filter=lfs diff=lfs merge=lfs -text
|
| 134 |
+
Assets/triangle/嫌疑犯/16造型師特徵.png filter=lfs diff=lfs merge=lfs -text
|
| 135 |
+
Assets/triangle/Case/case1.png filter=lfs diff=lfs merge=lfs -text
|
| 136 |
+
Assets/triangle/Case/case2.png filter=lfs diff=lfs merge=lfs -text
|
| 137 |
+
Assets/triangle/Case/case3.png filter=lfs diff=lfs merge=lfs -text
|
| 138 |
+
example/functionex1.png filter=lfs diff=lfs merge=lfs -text
|
| 139 |
+
example/functionex2.png filter=lfs diff=lfs merge=lfs -text
|
| 140 |
+
example/functionex3.png filter=lfs diff=lfs merge=lfs -text
|
Assets/triangle/Case/case1.png
ADDED
|
Git LFS Details
|
Assets/triangle/Case/case2.png
ADDED
|
Git LFS Details
|
Assets/triangle/Case/case3.png
ADDED
|
Git LFS Details
|
Assets/triangle/detective.svg
ADDED
|
|
Assets/triangle/嫌疑犯/16造型師特徵.png
ADDED
|
Git LFS Details
|
Assets/triangle//345/253/214/347/226/221/347/212/257/27/345/244/252/347/251/272/344/272/272/347/231/275.png
ADDED
|
README.md
CHANGED
|
@@ -1,141 +1,142 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
2.
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
```
|
| 32 |
-
|
| 33 |
-
#
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
* **
|
| 39 |
-
* **
|
| 40 |
-
* **
|
| 41 |
-
* **
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
* **
|
| 46 |
-
* **
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
* **
|
| 53 |
-
* **
|
| 54 |
-
|
| 55 |
-
* **Phase
|
| 56 |
-
|
| 57 |
-
* **
|
| 58 |
-
* **
|
| 59 |
-
|
| 60 |
-
* **Phase
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
* **
|
| 65 |
-
* **
|
| 66 |
-
|
| 67 |
-
* **Phase
|
| 68 |
-
* **Phase
|
| 69 |
-
* **Phase
|
| 70 |
-
|
| 71 |
-
* **Step
|
| 72 |
-
* **Step
|
| 73 |
-
|
| 74 |
-
* **Phase
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
* **
|
| 79 |
-
* **
|
| 80 |
-
|
| 81 |
-
* **Phase
|
| 82 |
-
* **Phase
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
* **
|
| 87 |
-
* **
|
| 88 |
-
|
| 89 |
-
* **
|
| 90 |
-
* **
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
- [x]
|
| 96 |
-
- [x]
|
| 97 |
-
- [x]
|
| 98 |
-
|
| 99 |
-
- [x] `
|
| 100 |
-
- [
|
| 101 |
-
|
| 102 |
-
- [ ] 開發 `
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
* **
|
| 112 |
-
* **
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
*
|
| 121 |
-
|
| 122 |
-
* **正向
|
| 123 |
-
|
| 124 |
-
* **
|
| 125 |
-
* **
|
| 126 |
-
* **
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
* **
|
| 132 |
-
|
| 133 |
-
* **
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
* **
|
| 140 |
-
|
| 141 |
-
|
|
|
|
|
|
| 1 |
+
# Math City: Cyber Chronicles (數學特區:未來都市)
|
| 2 |
+
## ⚠️ AI 行為準則 (System Instructions)
|
| 3 |
+
1. **語言要求:** 所有的對話、思考過程、Implementation Plan (實作計畫) 以及程式碼註解,請**嚴格使用「繁體中文 (Traditional Chinese)」**。
|
| 4 |
+
2. **例外:** 只有程式碼本身的變數名稱、函數名稱、專有名詞保留英文。
|
| 5 |
+
## 1. 專案願景 (Project Vision)
|
| 6 |
+
本專案為八年級下學期數學課程的遊戲化教學平台。透過「未來都市」的沉浸式包裝,讓學生以「特務/維護者」的身分,在解決城市危機的過程中學習數學概念。
|
| 7 |
+
|
| 8 |
+
**核心體驗**:
|
| 9 |
+
* **平台**:iPad 橫向全螢幕體驗 (Mobile Web App)。
|
| 10 |
+
* **風格**:Cyberpunk / Neon Noir (賽博龐克/霓虹暗黑)。
|
| 11 |
+
* **技術**:HTML5 Canvas + Tailwind CSS + Vanilla JS (無須建置工具)。
|
| 12 |
+
|
| 13 |
+
## 2. 檔案結構 (File Structure)
|
| 14 |
+
```
|
| 15 |
+
/
|
| 16 |
+
├── index.html # [入口] 城市全息地圖 (Math City Map)
|
| 17 |
+
├── sequence.html # [數列] 數列峽谷 (The Glitch Canyon)
|
| 18 |
+
├── function.html # [函數] 能源核心 (The Energy Core)
|
| 19 |
+
├── congruence.html # [全等] 全等重案組 (Congruence District) - {待開發}
|
| 20 |
+
├── parallel.html # [平行] 平行建構區 (Parallel Skyline) - {待開發}
|
| 21 |
+
└── assets/ # (建議) 存放圖片與音效資源
|
| 22 |
+
```
|
| 23 |
+
|
| 24 |
+
## 3. 視覺設計系統 (Design System)
|
| 25 |
+
|
| 26 |
+
### 分區色彩計畫 (Zone Color Coding)
|
| 27 |
+
為了區分不同的數學主題區域,每個子頁面都擁有**獨立的背景色調**與**霓虹主色**,強化玩家的空間感:
|
| 28 |
+
| 區域 (Zone) | 數學主題 (Theme) | 霓虹主色 (Neon) | 背景基調 (Background) | 狀態 |
|
| 29 |
+
| :--- | :--- | :--- | :--- | :--- |
|
| 30 |
+
| **首頁 (Hub)** | 城市全息圖 | 🔵 `#06b6d4` (Cyan) | ⚫ `#050510` (Void) | `index.html` |
|
| 31 |
+
| **摩天大樓** | 平行/幾何 | 🟢 `#22c55e` (Green) | 🌑 `#020804` (Deep Jungle) | `skyscraper.html` |
|
| 32 |
+
| **數列峽谷** | 數列/規律 | 🟠 `#f59e0b` (Amber) | 🟤 `#1a1005` (Dark Amber) | `sequence.html` |
|
| 33 |
+
| **能源核心** | 函數/邏輯 | 🔵 `#06b6d4` (Cyan) | 🔵 `#051015` (Deep Azure) | `function.html` |
|
| 34 |
+
| **全等重案組** | 全等判別 | 🟣 `#d946ef` (Fuchsia) | 🟣 `#100515` (Dark Void) | `congruence.html` |
|
| 35 |
+
|
| 36 |
+
### 色彩計畫 (Neon Palette)
|
| 37 |
+
這座城市由四個區域組成,每個區域有其代表的主色調:
|
| 38 |
+
* **🟦 能源核心 (Cyan)**: `#06b6d4` (Cyan-500) - 代表科技、冷靜、函數邏輯。
|
| 39 |
+
* **🟨 數列峽谷 (Amber)**: `#f59e0b` (Amber-500) - 代表古老遺跡、警示、規律。
|
| 40 |
+
* **🟪 全等重案組 (Magenta)**: `#d946ef` (Fuchsia-500) - 代表神秘、霓虹街頭、偵探氛圍。
|
| 41 |
+
* **🟩 平行建構區 (Green)**: `#22c55e` (Green-500) - 代表建設、雷射光束、結構。
|
| 42 |
+
* **🌑 背景基調**: `#050510` (Very Dark Blue) - 極致黑,襯托霓虹光。
|
| 43 |
+
|
| 44 |
+
### UI 規範
|
| 45 |
+
* **字體**: 'Orbitron' (數字/英文標題), 'Noto Sans TC' (中文內文)。
|
| 46 |
+
* **介面**: 玻璃擬態 (Glassmorphism),半透明深色背景 + 亮色邊框。
|
| 47 |
+
* **互動**: 針對觸控優化的大按鈕,避免依賴 Hover 效果。
|
| 48 |
+
|
| 49 |
+
## 4. 遊戲模組詳解 (Game Modules)
|
| 50 |
+
|
| 51 |
+
### A. 數列峽谷 (The Glitch Canyon) - `sequence.html` [已完成/需微調]
|
| 52 |
+
* **數學單元**:等差數列與級數。
|
| 53 |
+
* **劇情**:城市邊緣的古老程式碼層出現 Glitch,階梯消失。
|
| 54 |
+
* **玩法**:動作跳躍遊戲。
|
| 55 |
+
* **Phase 1 (觀念檢測)**:進入關卡前,先通過數列填空測驗,確認對首項與公差的理解。
|
| 56 |
+
* **Phase 2 (數列跑酷)**:動作遊戲階段。
|
| 57 |
+
* **視覺風格**:全區統一碎石 (Gravel) 紋理,數位荒野主題。
|
| 58 |
+
* **規則**:僅有數列的「前兩項」會有琥珀色外框提示,後續特定平台外觀一致,玩家需自行計算並跳上正確數字平台。
|
| 59 |
+
* **裝飾**:仙人掌與骨骸等裝飾物僅生成於無數字的安全平台上,不干擾作答。
|
| 60 |
+
* **Phase 3 (召喚儀式)**:終點的互動教學。利用「倒轉複製」概念,將階梯補成長方形,引導學生推導出等差級數求和公式 $S_n = \frac{(a_1+a_n) \times n}{2}$。
|
| 61 |
+
* **Phase 4 (核心回顧)**:(Gold Standard) 遊戲結束後顯示重點摘要,包含「等差數列定義」、「公差公式」、「級數和公式」,對應學習單內容。
|
| 62 |
+
|
| 63 |
+
### B. 能源核心 (The Energy Core) - `function.html` [已完成]
|
| 64 |
+
* **數學單元**:線型函數 ($y = ax + b$)。
|
| 65 |
+
* **劇情**:中央發電廠核心不穩,需要輸入正確的晶石量以維持能量平衡。
|
| 66 |
+
* **玩法**:數據分析與邏輯推導。
|
| 67 |
+
* **Phase 1 (蒐集數據)**:輸入 $x$ (晶石),觀察 $y$ (電力),利用描點法找出函數關係。
|
| 68 |
+
* **Phase 2 (規律分析)**:觀察圖表上的點,辨識出「直線」規律,並利用規律預測未來。
|
| 69 |
+
* **Phase 3 (過載危機)**:系統給出目標電力 $y=203$,玩家需根據 $y=2x+3$ 反求投入量 $x$。
|
| 70 |
+
* **Phase 4 (進階推導)**:(Advanced Deduction) 核心重置,公式改變 ($y=ax+b$)。
|
| 71 |
+
* **Step 1 (找截距 b)**:投入 $x=0$,觀察 $y$ 值,解出 $b$。
|
| 72 |
+
* **Step 2 (找斜率 a)**:投入 $x=1$,觀察變化量,解出 $a$。
|
| 73 |
+
* **Step 3 (驗算)**:利用新公式 $y=3x+5$ 驗算 $x=10$ 的結果,並用圖表上的點 ($10,35$) 進行再確認。
|
| 74 |
+
* **Phase 5 (核心解鎖)**:輸入正確的 $a$ 與 $b$ 值,完成任務。
|
| 75 |
+
* **Phase 6 (脈衝同步)**:節奏遊戲,在音樂節拍下進行點擊,穩定核心能量,強化回饋感。
|
| 76 |
+
|
| 77 |
+
### C. 全等重案組 (Congruence District) - `congruence.html` [規劃中]
|
| 78 |
+
* **數學單元**:全等三角形判別 (SAS, ASA, SSS, RHS)。
|
| 79 |
+
* **劇情**:追捕偽裝大師「三角魔人」。
|
| 80 |
+
* **玩法**:偵探解謎。
|
| 81 |
+
* **Phase 1 (蒐證)**:在案發現場使用放大鏡蒐集「邊長」與「角度」線索。
|
| 82 |
+
* **Phase 2 (指認)**:在警局指認牆上,利用蒐集的條件篩選嫌疑犯。
|
| 83 |
+
* **Phase 3 (審判/回顧)**:整理案情,總結全等性質(SAS, ASA...),並解釋為何 SSA (搖擺分身) 無法全等。
|
| 84 |
+
|
| 85 |
+
### D. 平行建構區 (Parallel Skyline) - `parallel.html` [規劃中]
|
| 86 |
+
* **數學單元**:平行線截角性質、特殊四邊形。
|
| 87 |
+
* **劇情**:建設通往雲端的摩天大樓。
|
| 88 |
+
* **玩法**:建築工程。
|
| 89 |
+
* **平行光束**:調整雷射發射器角度,利用「內錯角相等」或「同側內角互補」原理接通橋樑。
|
| 90 |
+
* **四邊形物流**:輸送帶上出現各種四邊形建材,需根據對角線性質 (是否等長/垂直/平分) 快速分類。
|
| 91 |
+
* **Phase 3 (完工驗收)**:展示建築藍圖,總結平行線截角性質與特殊四邊形的判別家族樹。
|
| 92 |
+
|
| 93 |
+
## 5. 開發路徑 (Roadmap)
|
| 94 |
+
- [x] **Phase 1: 基礎建設**
|
| 95 |
+
- [x] 建立 `index.html` 城市地圖。
|
| 96 |
+
- [x] 生成 iPad 專用背景圖。
|
| 97 |
+
- [x] 統一導航系統 (Back to Map 按鈕)。
|
| 98 |
+
- [x] **Phase 2: 現有程式優化**
|
| 99 |
+
- [x] `function.html`: 音量核心遊戲完整化、Phase 4 進階邏輯完備、節奏遊戲計分平衡。
|
| 100 |
+
- [x] `sequence.html`: 統一視覺風格,調整難度曲線。
|
| 101 |
+
- [ ] **Phase 3: 新關卡開發**
|
| 102 |
+
- [ ] 開發 `congruence.html` (全等重案組) 原型。
|
| 103 |
+
- [ ] 開發 `parallel.html` (平行建構區) 原型。
|
| 104 |
+
|
| 105 |
+
---
|
| 106 |
+
|
| 107 |
+
## 6. 修正經驗與開發筆記 (Dev Log)
|
| 108 |
+
此區塊記錄開發過程中的重要迭代與修正經驗,供日後參考。
|
| 109 |
+
|
| 110 |
+
### [Sequence Canyon] 視覺與機制修正 (Visual & Mechanics Polish)
|
| 111 |
+
* **視覺一致性的陷阱**: 修正了平台顏色提示過於明顯的問題,統一為碎石紋理,強制學生進行計算而非辨色。
|
| 112 |
+
* **提示機制**: 僅由數列前兩項提供視覺提示,後續完全依賴計算。
|
| 113 |
+
* **裝飾物配置**: 將裝飾物移至非遊戲路徑的安全平台,避免干擾判斷。
|
| 114 |
+
|
| 115 |
+
### [Function Core] 進階教學設計 (Advanced Pedagogy Design)
|
| 116 |
+
* **鷹架理論 (Scaffolding) 的應用**:
|
| 117 |
+
* **變數分離**: 在推導 $y=ax+b$ 時,不要同時讓學生解兩個未知數。我們設計為:
|
| 118 |
+
1. 先令 $x=0$,讓 $a$ 項消失,專注解 $b$ (截距)。
|
| 119 |
+
2. 知道 $b$ 後,再令 $x=1$,專注解 $a$ (斜率)。
|
| 120 |
+
* 這樣能讓學生理解每個係數的幾何意義,而非單純背誦解聯立方程式。
|
| 121 |
+
* **視覺與代數的連結 (Visual-Algebra Connection)**:
|
| 122 |
+
* **圖表驗證**: 在 Phase 4 Step 3,驗算出 $y=35$ 後,不僅僅是數字正確,程式還引導視線看向圖表上的 $(10, 35)$ 點。這強化了「代數解」與「幾何圖形上的點」是同一回事的概念。
|
| 123 |
+
* **正向回饋系統 (Positive Feedback Loop)**:
|
| 124 |
+
* **避免挫折感**: 將傳統的 `alert()` 錯誤彈窗,改為「介面上的脈衝紅框」與「引導式錯誤訊息」。
|
| 125 |
+
* **具體化錯誤**: 錯誤訊息不是只說「錯」,而是說「如果 x=2, y=11, 因 11 = 2a+5, 所以 a=?」。這將**錯誤轉化為另一個數學題目**,而非單純的懲罰。
|
| 126 |
+
* **節奏遊戲獎勵**: 在完成艱澀的運算後,Phase 6 的節奏遊戲提供純粹的感官刺激與高分回饋 (5000分),平衡大腦疲勞。
|
| 127 |
+
* **情境化總結 (Contextual Summary)**:
|
| 128 |
+
* 任務完成頁面不僅列出分數,更點出核心數學素養:「預測」與「建模」。讓學生知道他們剛剛做的計算,本質上就是科學家預測未來的過程。
|
| 129 |
+
|
| 130 |
+
### [Function Core] 介面優化與除錯經驗
|
| 131 |
+
* **按鈕鎖定邏輯**: 驗證成功後鎖定按鈕 (`disabled`) 非常重要,可防止重複觸發事件或重複計分。需確保 CSS Selector 選中正確的按鈕。
|
| 132 |
+
* **樣式與動畫**:
|
| 133 |
+
* **跳動 vs 呼吸**: `animate-bounce` 用於錯誤訊息有時過於滑稽且難以閱讀,改為 `animate-pulse` (呼吸燈/閃耀) 配合紅色邊框,既有警示效果又保持質感。
|
| 134 |
+
* **Canvas 文字清晰度**: 在 High-DPI 螢幕 (Retina) 上,Canvas 必須正確縮放 (Scale by DevicePixelRatio),否則文字會模糊。
|
| 135 |
+
|
| 136 |
+
---
|
| 137 |
+
|
| 138 |
+
## 7. 專案製作群 (Credits)
|
| 139 |
+
* **遊戲設計**: 新竹縣精華國中 藍星宇老師
|
| 140 |
+
* **社群**: [萬物皆數](https://www.facebook.com/groups/1554372228718393)
|
| 141 |
+
|
| 142 |
+
*Created by Antigravity for 11402 Semester Project*
|
congruence_detective.html
ADDED
|
@@ -0,0 +1,1413 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="zh-TW">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport"
|
| 7 |
+
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
| 8 |
+
<title>全等重案組 - Congruence Unit</title>
|
| 9 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 10 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 11 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 12 |
+
<link
|
| 13 |
+
href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@400;700;900&family=Orbitron:wght@400;700&display=swap"
|
| 14 |
+
rel="stylesheet">
|
| 15 |
+
<style>
|
| 16 |
+
body {
|
| 17 |
+
font-family: 'Noto Sans TC', sans-serif;
|
| 18 |
+
background-color: #050510;
|
| 19 |
+
overflow: hidden;
|
| 20 |
+
color: white;
|
| 21 |
+
touch-action: none;
|
| 22 |
+
user-select: none;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
.font-tech {
|
| 26 |
+
font-family: 'Orbitron', sans-serif;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
/* Glassmorphism Panel */
|
| 30 |
+
.glass-panel {
|
| 31 |
+
background: rgba(15, 23, 42, 0.85);
|
| 32 |
+
backdrop-filter: blur(12px);
|
| 33 |
+
-webkit-backdrop-filter: blur(12px);
|
| 34 |
+
border: 1px solid rgba(147, 51, 234, 0.3);
|
| 35 |
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
/* Cyberpunk Dialogue Box */
|
| 39 |
+
.dialogue-box {
|
| 40 |
+
position: absolute;
|
| 41 |
+
bottom: 20px;
|
| 42 |
+
left: 5%;
|
| 43 |
+
width: 90%;
|
| 44 |
+
height: 220px;
|
| 45 |
+
/* Increased height */
|
| 46 |
+
background: rgba(0, 0, 0, 0.9);
|
| 47 |
+
border: 2px solid #d946ef;
|
| 48 |
+
/* Magenta */
|
| 49 |
+
border-left-width: 8px;
|
| 50 |
+
box-shadow: 0 0 20px rgba(217, 70, 239, 0.3);
|
| 51 |
+
padding: 24px;
|
| 52 |
+
/* Increased padding */
|
| 53 |
+
z-index: 50;
|
| 54 |
+
display: flex;
|
| 55 |
+
flex-direction: column;
|
| 56 |
+
justify-content: flex-start;
|
| 57 |
+
pointer-events: auto;
|
| 58 |
+
clip-path: polygon(0 0,
|
| 59 |
+
100% 0,
|
| 60 |
+
100% 85%,
|
| 61 |
+
95% 100%,
|
| 62 |
+
0% 100%);
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
.dialogue-name {
|
| 66 |
+
font-family: 'Orbitron', sans-serif;
|
| 67 |
+
font-size: 1.5rem;
|
| 68 |
+
/* Increased size */
|
| 69 |
+
color: #d946ef;
|
| 70 |
+
font-weight: bold;
|
| 71 |
+
margin-bottom: 12px;
|
| 72 |
+
text-transform: uppercase;
|
| 73 |
+
letter-spacing: 2px;
|
| 74 |
+
text-shadow: 0 0 10px rgba(217, 70, 239, 0.5);
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
.dialogue-text {
|
| 78 |
+
font-size: 1.3rem;
|
| 79 |
+
/* Increased size */
|
| 80 |
+
line-height: 1.6;
|
| 81 |
+
color: #e2e8f0;
|
| 82 |
+
font-weight: 500;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
/* ... cursor blink styles ... */
|
| 86 |
+
.cursor-blink::after {
|
| 87 |
+
content: '▋';
|
| 88 |
+
display: inline-block;
|
| 89 |
+
animation: blink 1s infinite;
|
| 90 |
+
color: #d946ef;
|
| 91 |
+
margin-left: 5px;
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
@keyframes blink {
|
| 95 |
+
|
| 96 |
+
0%,
|
| 97 |
+
100% {
|
| 98 |
+
opacity: 1;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
50% {
|
| 102 |
+
opacity: 0;
|
| 103 |
+
}
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
/* Hitbox Styles */
|
| 107 |
+
.hitbox {
|
| 108 |
+
position: absolute;
|
| 109 |
+
width: 60px;
|
| 110 |
+
height: 60px;
|
| 111 |
+
border-radius: 50%;
|
| 112 |
+
background: rgba(255, 255, 255, 0.1);
|
| 113 |
+
border: 2px dashed rgba(255, 255, 255, 0.5);
|
| 114 |
+
cursor: pointer;
|
| 115 |
+
z-index: 100;
|
| 116 |
+
transition: all 0.2s;
|
| 117 |
+
animation: pulse-ring 2s infinite;
|
| 118 |
+
display: flex;
|
| 119 |
+
justify-content: center;
|
| 120 |
+
align-items: center;
|
| 121 |
+
color: white;
|
| 122 |
+
font-weight: 900;
|
| 123 |
+
font-size: 1.8rem;
|
| 124 |
+
text-shadow: 0 0 5px black;
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
.hitbox.found-angle {
|
| 128 |
+
background: rgba(239, 68, 68, 0.4);
|
| 129 |
+
/* Red */
|
| 130 |
+
border: 2px solid #ef4444;
|
| 131 |
+
animation: none;
|
| 132 |
+
pointer-events: none;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
.hitbox.found-side {
|
| 136 |
+
background: rgba(59, 130, 246, 0.4);
|
| 137 |
+
/* Blue */
|
| 138 |
+
border: 2px solid #3b82f6;
|
| 139 |
+
animation: none;
|
| 140 |
+
pointer-events: none;
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
.hitbox:hover {
|
| 144 |
+
transform: scale(1.1);
|
| 145 |
+
border-color: #fff;
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
@keyframes pulse-ring {
|
| 149 |
+
0% {
|
| 150 |
+
box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.4);
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
70% {
|
| 154 |
+
box-shadow: 0 0 0 15px rgba(255, 255, 255, 0);
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
100% {
|
| 158 |
+
box-shadow: 0 0 0 0 rgba(255, 255, 255, 0);
|
| 159 |
+
}
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
.evidence-counter {
|
| 163 |
+
position: fixed;
|
| 164 |
+
top: 20px;
|
| 165 |
+
right: 20px;
|
| 166 |
+
background: rgba(0, 0, 0, 0.8);
|
| 167 |
+
border: 1px solid #d946ef;
|
| 168 |
+
padding: 10px 20px;
|
| 169 |
+
border-radius: 8px;
|
| 170 |
+
z-index: 100;
|
| 171 |
+
font-family: 'Orbitron';
|
| 172 |
+
color: #d946ef;
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
.character-sprite {
|
| 178 |
+
position: absolute;
|
| 179 |
+
bottom: 80px;
|
| 180 |
+
/* Moved up significantly - hidden behind dialogue box */
|
| 181 |
+
left: -20px;
|
| 182 |
+
/* Adjusted position */
|
| 183 |
+
height: clamp(600px, 100vh, 1200px);
|
| 184 |
+
/* Even larger and higher */
|
| 185 |
+
z-index: 40;
|
| 186 |
+
/* Behind dialogue box */
|
| 187 |
+
filter: drop-shadow(0 0 20px rgba(217, 70, 239, 0.4));
|
| 188 |
+
transition: all 0.3s ease-out;
|
| 189 |
+
pointer-events: none;
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
.character-sprite.hidden {
|
| 193 |
+
opacity: 0;
|
| 194 |
+
transform: translateX(-50px);
|
| 195 |
+
pointer-events: none;
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
/* Scanlines */
|
| 199 |
+
.scanlines {
|
| 200 |
+
background: linear-gradient(to bottom,
|
| 201 |
+
rgba(255, 255, 255, 0),
|
| 202 |
+
rgba(255, 255, 255, 0) 50%,
|
| 203 |
+
rgba(0, 0, 0, 0.1) 50%,
|
| 204 |
+
rgba(0, 0, 0, 0.1));
|
| 205 |
+
background-size: 100% 4px;
|
| 206 |
+
position: fixed;
|
| 207 |
+
inset: 0;
|
| 208 |
+
pointer-events: none;
|
| 209 |
+
z-index: 200;
|
| 210 |
+
mix-blend-mode: overlay;
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
/* Quiz Layer - Fixed Fullscreen Overlay */
|
| 214 |
+
#quiz-layer {
|
| 215 |
+
position: fixed;
|
| 216 |
+
inset: 0;
|
| 217 |
+
z-index: 150;
|
| 218 |
+
background: linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0) 40%, rgba(0, 0, 0, 0.8) 100%);
|
| 219 |
+
/* Gradient mask to keep top clear */
|
| 220 |
+
backdrop-filter: blur(1px);
|
| 221 |
+
display: flex;
|
| 222 |
+
align-items: flex-end;
|
| 223 |
+
justify-content: center;
|
| 224 |
+
padding-bottom: 40px;
|
| 225 |
+
/* Lift slightly higher than dialogue */
|
| 226 |
+
pointer-events: auto;
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
#quiz-layer.hidden {
|
| 230 |
+
display: none !important;
|
| 231 |
+
pointer-events: none;
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
#quiz-layer .quiz-box {
|
| 235 |
+
position: relative;
|
| 236 |
+
width: 90%;
|
| 237 |
+
max-width: 1200px;
|
| 238 |
+
height: 250px;
|
| 239 |
+
background: rgba(5, 5, 10, 0.95);
|
| 240 |
+
border: 2px solid #d946ef;
|
| 241 |
+
border-left-width: 8px;
|
| 242 |
+
box-shadow: 0 0 30px rgba(217, 70, 239, 0.4);
|
| 243 |
+
pointer-events: auto;
|
| 244 |
+
clip-path: polygon(0 0, 100% 0, 100% 85%, 95% 100%, 0% 100%);
|
| 245 |
+
padding: 30px;
|
| 246 |
+
display: flex;
|
| 247 |
+
flex-direction: column;
|
| 248 |
+
justify-content: flex-start;
|
| 249 |
+
}
|
| 250 |
+
</style>
|
| 251 |
+
</head>
|
| 252 |
+
|
| 253 |
+
<body class="bg-slate-900 text-white selection:bg-fuchsia-500 selection:text-white">
|
| 254 |
+
|
| 255 |
+
<div class="scanlines"></div>
|
| 256 |
+
|
| 257 |
+
<!-- Background - Static for now, can be dynamic -->
|
| 258 |
+
<div class="fixed inset-0 z-0 bg-cover bg-center opacity-40"
|
| 259 |
+
style="background-image: url('Assets/index/indexbg.png'); filter: blur(4px) hue-rotate(45deg);"></div>
|
| 260 |
+
|
| 261 |
+
<!-- Game Container -->
|
| 262 |
+
<div id="game-stage" class="relative w-full h-full min-h-screen overflow-hidden flex flex-col">
|
| 263 |
+
|
| 264 |
+
<!-- Header -->
|
| 265 |
+
<div class="absolute top-0 left-0 w-full p-4 z-50 flex justify-between items-start pointer-events-none">
|
| 266 |
+
<div class="glass-panel px-6 py-2 rounded-br-2xl border-l-4 border-l-fuchsia-500 pointer-events-auto">
|
| 267 |
+
<h1 class="text-xl font-bold text-fuchsia-400 tracking-wider">全等重案組<br><span
|
| 268 |
+
class="font-tech text-sm">CONGRUENCE UNIT</span></h1>
|
| 269 |
+
<div class="text-xs text-slate-400 font-mono mt-1">CASE FILE #001: THE STYLIST</div>
|
| 270 |
+
</div>
|
| 271 |
+
|
| 272 |
+
<button onclick="window.history.back()"
|
| 273 |
+
class="glass-panel px-4 py-2 rounded-lg hover:bg-white/10 transition-colors pointer-events-auto text-sm text-slate-300">
|
| 274 |
+
EXIT UNIT
|
| 275 |
+
</button>
|
| 276 |
+
</div>
|
| 277 |
+
|
| 278 |
+
<!-- Evidence/Status Display (Hidden by default) -->
|
| 279 |
+
<div id="evidence-panel" class="hidden">
|
| 280 |
+
<div class="evidence-counter">
|
| 281 |
+
<div class="text-xs text-secondary mb-1">EVIDENCE COLLECTED</div>
|
| 282 |
+
<div class="text-2xl font-bold" id="evidence-count">0 / 6</div>
|
| 283 |
+
</div>
|
| 284 |
+
</div>
|
| 285 |
+
|
| 286 |
+
<!-- Main Interaction Area -->
|
| 287 |
+
<div id="scene-layer" class="relative flex-1 z-10 flex items-center justify-center">
|
| 288 |
+
<!-- Target Container for Investigation -->
|
| 289 |
+
<div id="target-container" class="flex-1 flex justify-center items-center relative p-4 hidden">
|
| 290 |
+
<div id="suspect-wrapper" class="relative inline-block">
|
| 291 |
+
<img id="suspect-image" src="Assets/triangle/嫌疑犯/16造型師.png" alt="Suspect"
|
| 292 |
+
class="max-h-[60vh] object-contain shadow-lg shadow-fuchsia-500/20 rounded-xl border border-slate-700">
|
| 293 |
+
</div>
|
| 294 |
+
</div> <!-- Hitboxes will be appended here -->
|
| 295 |
+
</div>
|
| 296 |
+
|
| 297 |
+
<!-- Dialogue Layer -->
|
| 298 |
+
<div id="dialogue-layer" class="hidden" onclick="window.game.next()">
|
| 299 |
+
<img id="speaker-sprite" src="" class="character-sprite hidden" alt="Character">
|
| 300 |
+
<div class="dialogue-box cursor-pointer hover:bg-black/95 transition-colors">
|
| 301 |
+
<div id="speaker-name" class="dialogue-name">SENIOR DETECTIVE</div>
|
| 302 |
+
<div id="dialogue-text" class="dialogue-text cursor-blink"></div>
|
| 303 |
+
<div class="absolute bottom-4 right-4 text-xs text-fuchsia-500 animate-bounce">▼ CLICK TO CONTINUE
|
| 304 |
+
</div>
|
| 305 |
+
</div>
|
| 306 |
+
</div>
|
| 307 |
+
|
| 308 |
+
<!-- Quiz Layer (Modified) -->
|
| 309 |
+
<div id="quiz-layer" class="hidden">
|
| 310 |
+
<div class="quiz-box">
|
| 311 |
+
<h2 class="text-lg font-bold text-fuchsia-400 font-tech mb-1">SYSTEM AUTHENTICATION</h2>
|
| 312 |
+
<div id="quiz-content" class="w-full"></div>
|
| 313 |
+
</div>
|
| 314 |
+
</div>
|
| 315 |
+
</div>
|
| 316 |
+
|
| 317 |
+
<script>
|
| 318 |
+
// Game Script
|
| 319 |
+
let playerName = localStorage.getItem('player_nickname') || '菜鳥探員';
|
| 320 |
+
|
| 321 |
+
// Initial Script Placeholder - populated dynamically
|
| 322 |
+
let SCRIPT = [];
|
| 323 |
+
|
| 324 |
+
class GameEngine {
|
| 325 |
+
constructor() {
|
| 326 |
+
this.step = 0;
|
| 327 |
+
this.isTyping = false;
|
| 328 |
+
this.elDialogue = document.getElementById('dialogue-layer');
|
| 329 |
+
this.elText = document.getElementById('dialogue-text');
|
| 330 |
+
this.elName = document.getElementById('speaker-name');
|
| 331 |
+
this.elScene = document.getElementById('scene-layer');
|
| 332 |
+
this.elTarget = document.getElementById('target-container');
|
| 333 |
+
|
| 334 |
+
this.foundFeatures = new Set();
|
| 335 |
+
this.angleCount = 0;
|
| 336 |
+
this.sideCount = 0;
|
| 337 |
+
this.requiredFeatures = 6;
|
| 338 |
+
|
| 339 |
+
this.init();
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
init() {
|
| 343 |
+
this.addStyles();
|
| 344 |
+
this.setupInitialScript();
|
| 345 |
+
this.showDialogue(true);
|
| 346 |
+
this.processStep();
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
setupInitialScript() {
|
| 350 |
+
// Formatting helper
|
| 351 |
+
const fmtName = (n) => `<span class="text-amber-400 font-bold mx-1 text-xl">${n}</span>`;
|
| 352 |
+
|
| 353 |
+
SCRIPT = [
|
| 354 |
+
{
|
| 355 |
+
type: 'dialogue',
|
| 356 |
+
speaker: '資深警探',
|
| 357 |
+
text: '歡迎來到 Math City 的重案組,我是負責帶領你的前輩,請問你怎麼稱呼?',
|
| 358 |
+
},
|
| 359 |
+
{
|
| 360 |
+
type: 'input_name',
|
| 361 |
+
speaker: 'SYSTEM',
|
| 362 |
+
text: '請輸入你的暱稱...'
|
| 363 |
+
},
|
| 364 |
+
{
|
| 365 |
+
type: 'dialogue',
|
| 366 |
+
speaker: '資深警探',
|
| 367 |
+
text: (name) => `${fmtName(name)}!真是個不錯的名字呢,時間緊迫,我就有話直說了。`
|
| 368 |
+
},
|
| 369 |
+
{
|
| 370 |
+
type: 'dialogue',
|
| 371 |
+
speaker: '資深警探',
|
| 372 |
+
text: '雖然城裡看似和平,但最近發生了一連串的案件,我們需要你的協助!'
|
| 373 |
+
},
|
| 374 |
+
{
|
| 375 |
+
type: 'dialogue',
|
| 376 |
+
speaker: '資深警探',
|
| 377 |
+
text: (name) => `對於這些犯罪,我們已經掌握了一部分的證據,但是緝凶人手不足,${fmtName(name)}看起來很聰明,我們需要你的幫忙。`
|
| 378 |
+
},
|
| 379 |
+
{
|
| 380 |
+
type: 'dialogue',
|
| 381 |
+
speaker: '資深警探',
|
| 382 |
+
text: '在三角形中,就像你臉上的<span class="text-yellow-400 font-bold">鼻子</span>、<span class="text-yellow-400 font-bold">嘴巴</span>、<span class="text-yellow-400 font-bold">眼睛</span>一樣,也有獨特的<span class="text-yellow-400 font-bold">特徵</span>,你知道是哪些嗎?'
|
| 383 |
+
},
|
| 384 |
+
{
|
| 385 |
+
type: 'choice',
|
| 386 |
+
question: '你知道三角形的特徵嗎?',
|
| 387 |
+
options: [
|
| 388 |
+
{ text: '我知道!', next: 'next_step' },
|
| 389 |
+
{ text: '不知道...', next: 'next_step' }
|
| 390 |
+
]
|
| 391 |
+
},
|
| 392 |
+
{
|
| 393 |
+
type: 'action',
|
| 394 |
+
action: 'show_suspect_investigation',
|
| 395 |
+
speaker: '資深警探',
|
| 396 |
+
text: '現在我們來練習一下。看看這張照片,這是一位嫌疑犯,請你找出他的<span class="text-yellow-400 font-bold">特徵點</span>。<br>三角形的<span class="text-yellow-400 font-bold">特徵���</span>就是他所有的<span class="text-yellow-400 font-bold">邊</span>和<span class="text-yellow-400 font-bold">角</span>。<br><span id="stats-display" class="text-yellow-400 font-mono mt-2 block text-lg"></span>'
|
| 397 |
+
}
|
| 398 |
+
];
|
| 399 |
+
}
|
| 400 |
+
|
| 401 |
+
next() {
|
| 402 |
+
if (this.isTyping) {
|
| 403 |
+
this.finishTyping();
|
| 404 |
+
return;
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
if (SCRIPT[this.step] && (SCRIPT[this.step].type === 'action' || SCRIPT[this.step].type === 'input_name' || SCRIPT[this.step].type === 'choice') && !this.actionCompleted) {
|
| 408 |
+
return;
|
| 409 |
+
}
|
| 410 |
+
|
| 411 |
+
this.step++;
|
| 412 |
+
if (this.step < SCRIPT.length) {
|
| 413 |
+
this.processStep();
|
| 414 |
+
} else {
|
| 415 |
+
console.log('Script Finished');
|
| 416 |
+
}
|
| 417 |
+
}
|
| 418 |
+
|
| 419 |
+
processStep() {
|
| 420 |
+
const data = SCRIPT[this.step];
|
| 421 |
+
let text = typeof data.text === 'function' ? data.text(playerName) : data.text;
|
| 422 |
+
|
| 423 |
+
if (data.type === 'dialogue') {
|
| 424 |
+
this.showDialogue(true);
|
| 425 |
+
this.setSpeaker(data.speaker);
|
| 426 |
+
this.typeText(text);
|
| 427 |
+
this.actionCompleted = true; // Auto-complete purely dialogue steps? No, wait for click.
|
| 428 |
+
this.actionCompleted = true; // Actually previous logic relied on next() call, which checks actionCompleted only for actions.
|
| 429 |
+
// My logic in next() was: if type is action/input/choice and !actionCompleted, return.
|
| 430 |
+
// For dialogue, we don't block.
|
| 431 |
+
} else if (data.type === 'action') {
|
| 432 |
+
this.showDialogue(true);
|
| 433 |
+
this.setSpeaker(data.speaker);
|
| 434 |
+
this.typeText(text);
|
| 435 |
+
this.handleAction(data.action);
|
| 436 |
+
} else if (data.type === 'input_name') {
|
| 437 |
+
this.showDialogue(true);
|
| 438 |
+
this.setSpeaker(data.speaker);
|
| 439 |
+
this.typeText(text);
|
| 440 |
+
this.showNameInput();
|
| 441 |
+
} else if (data.type === 'choice') {
|
| 442 |
+
this.showDialogue(true); // Keep dialogue visible asking question? Or hide?
|
| 443 |
+
// Usually hide for choice or overlay. Let's overlay.
|
| 444 |
+
this.showChoices(data.options);
|
| 445 |
+
} else if (data.type === 'quiz') {
|
| 446 |
+
this.showDialogue(false);
|
| 447 |
+
this.showQuiz(data);
|
| 448 |
+
}
|
| 449 |
+
}
|
| 450 |
+
|
| 451 |
+
setSpeaker(name) {
|
| 452 |
+
this.elName.innerText = name;
|
| 453 |
+
|
| 454 |
+
const sprite = document.getElementById('speaker-sprite');
|
| 455 |
+
if (name === '資深警探') {
|
| 456 |
+
sprite.src = 'Assets/triangle/detective.svg'; // Use our new SVG
|
| 457 |
+
sprite.classList.remove('hidden');
|
| 458 |
+
// Add some animation or style if needed
|
| 459 |
+
sprite.style.filter = "drop-shadow(0 0 10px #d946ef)";
|
| 460 |
+
} else if (name === 'SYSTEM') {
|
| 461 |
+
sprite.classList.add('hidden');
|
| 462 |
+
} else {
|
| 463 |
+
// Default behavior for other speakers
|
| 464 |
+
sprite.classList.add('hidden');
|
| 465 |
+
}
|
| 466 |
+
}
|
| 467 |
+
|
| 468 |
+
typeText(text) {
|
| 469 |
+
this.isTyping = true;
|
| 470 |
+
this.elText.innerHTML = '';
|
| 471 |
+
this.fullText = text;
|
| 472 |
+
let i = 0;
|
| 473 |
+
|
| 474 |
+
clearInterval(this.typeInterval);
|
| 475 |
+
this.typeInterval = setInterval(() => {
|
| 476 |
+
this.elText.innerHTML = this.fullText.substring(0, i + 1);
|
| 477 |
+
i++;
|
| 478 |
+
if (i === this.fullText.length) {
|
| 479 |
+
this.finishTyping();
|
| 480 |
+
}
|
| 481 |
+
}, 30);
|
| 482 |
+
}
|
| 483 |
+
|
| 484 |
+
finishTyping() {
|
| 485 |
+
clearInterval(this.typeInterval);
|
| 486 |
+
this.elText.innerHTML = this.fullText;
|
| 487 |
+
this.isTyping = false;
|
| 488 |
+
|
| 489 |
+
// If it's the investigation step, ensure stats are updated
|
| 490 |
+
if (SCRIPT[this.step] && SCRIPT[this.step].action === 'show_suspect_investigation') {
|
| 491 |
+
this.updateStatsDisplay();
|
| 492 |
+
}
|
| 493 |
+
}
|
| 494 |
+
|
| 495 |
+
showDialogue(show) {
|
| 496 |
+
if (show) this.elDialogue.classList.remove('hidden');
|
| 497 |
+
else this.elDialogue.classList.add('hidden');
|
| 498 |
+
}
|
| 499 |
+
|
| 500 |
+
// Input Name Logic
|
| 501 |
+
showNameInput() {
|
| 502 |
+
this.actionCompleted = false;
|
| 503 |
+
const inputHtml = `
|
| 504 |
+
<div id="name-input-overlay" class="fixed inset-0 z-[200] flex items-center justify-center bg-black/80 backdrop-blur-sm">
|
| 505 |
+
<div class="glass-panel p-8 rounded-xl flex flex-col gap-4 w-full max-w-md">
|
| 506 |
+
<h2 class="text-xl font-bold text-fuchsia-400 text-center">輸入代號 INPUT ID</h2>
|
| 507 |
+
<input type="text" id="player-name-input" class="bg-slate-900 border border-fuchsia-500 rounded p-3 text-white text-center text-xl focus:outline-none" placeholder="你的暱稱" maxlength="10">
|
| 508 |
+
<button id="name-submit-btn" class="bg-fuchsia-600 hover:bg-fuchsia-500 text-white font-bold py-3 rounded transition-colors">確認 CONFIRM</button>
|
| 509 |
+
</div>
|
| 510 |
+
</div>
|
| 511 |
+
`;
|
| 512 |
+
document.body.insertAdjacentHTML('beforeend', inputHtml);
|
| 513 |
+
|
| 514 |
+
const btn = document.getElementById('name-submit-btn');
|
| 515 |
+
const input = document.getElementById('player-name-input');
|
| 516 |
+
input.focus();
|
| 517 |
+
|
| 518 |
+
const submit = () => {
|
| 519 |
+
const val = input.value.trim();
|
| 520 |
+
if (val) {
|
| 521 |
+
playerName = val;
|
| 522 |
+
localStorage.setItem('player_nickname', playerName);
|
| 523 |
+
document.getElementById('name-input-overlay').remove();
|
| 524 |
+
this.actionCompleted = true;
|
| 525 |
+
this.next();
|
| 526 |
+
}
|
| 527 |
+
};
|
| 528 |
+
|
| 529 |
+
btn.onclick = submit;
|
| 530 |
+
input.onkeypress = (e) => { if (e.key === 'Enter') submit(); };
|
| 531 |
+
}
|
| 532 |
+
|
| 533 |
+
// Choice Logic
|
| 534 |
+
showChoices(options) {
|
| 535 |
+
this.actionCompleted = false;
|
| 536 |
+
const choiceContainer = document.createElement('div');
|
| 537 |
+
choiceContainer.id = 'choice-overlay';
|
| 538 |
+
choiceContainer.className = 'fixed inset-0 z-[200] flex items-center justify-center bg-black/60 backdrop-blur-sm';
|
| 539 |
+
|
| 540 |
+
let html = '<div class="flex flex-col gap-4 min-w-[300px]">';
|
| 541 |
+
options.forEach((opt, idx) => {
|
| 542 |
+
html += `<button class="choice-btn glass-panel px-6 py-4 rounded-lg text-xl font-bold text-white hover:bg-fuchsia-600 transition-colors border-l-4 border-fuchsia-400 hover:scale-105 transform duration-200" data-idx="${idx}">${opt.text}</button>`;
|
| 543 |
+
});
|
| 544 |
+
html += '</div>';
|
| 545 |
+
|
| 546 |
+
choiceContainer.innerHTML = html;
|
| 547 |
+
document.body.appendChild(choiceContainer);
|
| 548 |
+
|
| 549 |
+
const btns = choiceContainer.querySelectorAll('.choice-btn');
|
| 550 |
+
btns.forEach(btn => {
|
| 551 |
+
btn.onclick = () => {
|
| 552 |
+
document.getElementById('choice-overlay').remove();
|
| 553 |
+
this.actionCompleted = true;
|
| 554 |
+
this.next();
|
| 555 |
+
};
|
| 556 |
+
});
|
| 557 |
+
}
|
| 558 |
+
|
| 559 |
+
// ====== CASE DATA ======
|
| 560 |
+
caseData = {
|
| 561 |
+
case1: {
|
| 562 |
+
id: 1,
|
| 563 |
+
title: '霓虹暗巷重傷害案',
|
| 564 |
+
titleEn: 'NEON ALLEY ASSAULT',
|
| 565 |
+
caseImage: 'Assets/triangle/Case/case1.png',
|
| 566 |
+
description: '凌晨兩點,幾何酒吧後巷發生了一起惡意鬥毆。犯人使用自己三角形的頭部作為兇器,給了被害人致命一擊,甚至把巷口的鈦合金垃圾桶撞出了一個深深的凹痕。金屬凹痕與路口監視器留下了關鍵的蛛絲馬跡。警方已經鎖定了三位嫌疑犯,請你證明哪一位是犯人,並且說明證明方法!',
|
| 567 |
+
evidenceImage: 'Assets/triangle/蛛絲馬跡/12.png',
|
| 568 |
+
suspects: [
|
| 569 |
+
{ id: 'suspect_1', src: 'Assets/triangle/嫌疑犯/12教宗.png', name: '教宗' },
|
| 570 |
+
{ id: 'suspect_2', src: 'Assets/triangle/嫌疑犯/2農夫.png', name: '農夫' },
|
| 571 |
+
{ id: 'suspect_3', src: 'Assets/triangle/嫌疑犯/13軍人.png', name: '軍人' }
|
| 572 |
+
],
|
| 573 |
+
correctSuspect: 'suspect_1',
|
| 574 |
+
correctMethod: 'SSS',
|
| 575 |
+
nextCase: 'case2'
|
| 576 |
+
},
|
| 577 |
+
case2: {
|
| 578 |
+
id: 2,
|
| 579 |
+
title: '頂級俱樂部入侵案',
|
| 580 |
+
titleEn: 'ELITE CLUB INTRUSION',
|
| 581 |
+
caseImage: 'Assets/triangle/Case/case2.png',
|
| 582 |
+
description: '只容許上流社會進入的「等腰俱樂部」昨晚遭人強行破壞闖入。犯人沒有用炸藥,而是利用自己天生如利刃般鋒利的「頭頂」,加熱後直接在防彈門上熔出了一個缺口。安檢門的破壞痕跡與監視攝影機拍到了蛛絲馬跡的內容。警方已經鎖定了三位嫌疑犯,請你證明哪一位是犯人,並且說明證明方法!',
|
| 583 |
+
evidenceImage: 'Assets/triangle/蛛絲馬跡/3.png',
|
| 584 |
+
suspects: [
|
| 585 |
+
{ id: 'suspect_1', src: 'Assets/triangle/嫌疑犯/5冰淇淋師傅.png', name: '冰淇淋師傅' },
|
| 586 |
+
{ id: 'suspect_2', src: 'Assets/triangle/嫌疑犯/8列車長.png', name: '列車長' },
|
| 587 |
+
{ id: 'suspect_3', src: 'Assets/triangle/嫌疑犯/11醫生.png', name: '醫生' }
|
| 588 |
+
],
|
| 589 |
+
correctSuspect: 'suspect_2',
|
| 590 |
+
correctMethod: 'SAS',
|
| 591 |
+
nextCase: 'case3'
|
| 592 |
+
},
|
| 593 |
+
case3: {
|
| 594 |
+
id: 3,
|
| 595 |
+
title: '市政廳塗鴉案',
|
| 596 |
+
titleEn: 'CITY HALL GRAFFITI',
|
| 597 |
+
caseImage: 'Assets/triangle/Case/case3.png',
|
| 598 |
+
description: '街頭塗鴉客「幻影角」昨夜觸發了市政廳的隱藏防盜陷阱——「瞬間定型漆」。在他正貼著牆面作畫時,防盜漆瞬間噴發,雖然他及時逃脫,但牆面上留下了一個完美的「三角形空白剪影」。牆上的空白輪廓與警用無人機拍到了蛛絲馬跡的內容。警方已經鎖定了三位嫌疑犯,請你證明哪一位是犯人,並且說明證明方法!',
|
| 599 |
+
evidenceImage: 'Assets/triangle/蛛絲馬跡/17.png',
|
| 600 |
+
suspects: [
|
| 601 |
+
{ id: 'suspect_1', src: 'Assets/triangle/嫌疑犯/9_DJ.png', name: 'DJ' },
|
| 602 |
+
{ id: 'suspect_2', src: 'Assets/triangle/嫌疑犯/13軍人.png', name: '軍人' },
|
| 603 |
+
{ id: 'suspect_3', src: 'Assets/triangle/嫌疑犯/11醫生.png', name: '醫生' }
|
| 604 |
+
],
|
| 605 |
+
correctSuspect: 'suspect_3',
|
| 606 |
+
correctMethod: 'SAS',
|
| 607 |
+
nextCase: null
|
| 608 |
+
}
|
| 609 |
+
};
|
| 610 |
+
|
| 611 |
+
handleAction(actionName) {
|
| 612 |
+
this.actionCompleted = false;
|
| 613 |
+
if (actionName === 'show_suspect_investigation') {
|
| 614 |
+
this.startInvestigation();
|
| 615 |
+
} else if (actionName === 'show_sss_sas_images') {
|
| 616 |
+
this.showSSSSASImages();
|
| 617 |
+
} else if (actionName.startsWith('show_practice_')) {
|
| 618 |
+
this.showPracticeImages(actionName);
|
| 619 |
+
} else if (actionName.startsWith('start_case_')) {
|
| 620 |
+
const caseNum = actionName.replace('start_case_', '');
|
| 621 |
+
this.startCase('case' + caseNum);
|
| 622 |
+
} else if (actionName === 'end_game') {
|
| 623 |
+
// Ends...
|
| 624 |
+
}
|
| 625 |
+
}
|
| 626 |
+
|
| 627 |
+
// ====== CASE INVESTIGATION SYSTEM ======
|
| 628 |
+
startCase(caseKey) {
|
| 629 |
+
const caseInfo = this.caseData[caseKey];
|
| 630 |
+
if (!caseInfo) { console.error('Case not found:', caseKey); return; }
|
| 631 |
+
|
| 632 |
+
this.currentCase = caseInfo;
|
| 633 |
+
this.caseStartTime = null;
|
| 634 |
+
this.caseHadError = false;
|
| 635 |
+
if (!this.caseGrades) this.caseGrades = {};
|
| 636 |
+
|
| 637 |
+
// Phase 1: Show case title card
|
| 638 |
+
this.showCaseTitleCard(caseInfo);
|
| 639 |
+
}
|
| 640 |
+
|
| 641 |
+
showCaseTitleCard(caseInfo) {
|
| 642 |
+
// Hide any leftover images from previous phases
|
| 643 |
+
this.elTarget.classList.add('hidden');
|
| 644 |
+
const wrapper = document.getElementById('suspect-wrapper');
|
| 645 |
+
if (wrapper) wrapper.innerHTML = '';
|
| 646 |
+
|
| 647 |
+
// Full screen overlay with case title
|
| 648 |
+
const overlay = document.createElement('div');
|
| 649 |
+
overlay.id = 'case-title-overlay';
|
| 650 |
+
overlay.className = 'fixed inset-0 z-[200] flex flex-col items-center justify-center bg-black/90 backdrop-blur-sm cursor-pointer';
|
| 651 |
+
overlay.innerHTML = `
|
| 652 |
+
<div class="font-tech text-fuchsia-500 text-lg tracking-[0.5em] mb-4 animate-pulse">CASE FILE #00${caseInfo.id}</div>
|
| 653 |
+
<div class="text-5xl font-black text-white mb-4 tracking-wider" style="text-shadow: 0 0 30px rgba(217,70,239,0.6);">${caseInfo.title}</div>
|
| 654 |
+
<div class="font-tech text-2xl text-slate-400 tracking-[0.3em]">${caseInfo.titleEn}</div>
|
| 655 |
+
<div class="absolute bottom-12 text-fuchsia-400 text-sm animate-bounce font-tech">▼ CLICK TO START</div>
|
| 656 |
+
`;
|
| 657 |
+
|
| 658 |
+
overlay.onclick = () => {
|
| 659 |
+
overlay.remove();
|
| 660 |
+
this.showCaseDescription(caseInfo);
|
| 661 |
+
};
|
| 662 |
+
|
| 663 |
+
document.body.appendChild(overlay);
|
| 664 |
+
}
|
| 665 |
+
|
| 666 |
+
showCaseDescription(caseInfo) {
|
| 667 |
+
// Show case image + description in an overlay
|
| 668 |
+
const overlay = document.createElement('div');
|
| 669 |
+
overlay.id = 'case-desc-overlay';
|
| 670 |
+
overlay.className = 'fixed inset-0 z-[200] flex items-center justify-center bg-black/85 backdrop-blur-sm cursor-pointer';
|
| 671 |
+
overlay.innerHTML = `
|
| 672 |
+
<div class="flex flex-col md:flex-row gap-8 max-w-5xl w-full px-8 items-center">
|
| 673 |
+
<img src="${caseInfo.caseImage}" class="h-[50vh] object-contain rounded-xl border-2 border-fuchsia-500/40 shadow-lg shadow-fuchsia-500/20" alt="Case Image">
|
| 674 |
+
<div class="flex flex-col gap-4 flex-1">
|
| 675 |
+
<div class="font-tech text-fuchsia-400 text-sm tracking-[0.3em]">CASE FILE #00${caseInfo.id}</div>
|
| 676 |
+
<h2 class="text-3xl font-black text-white">${caseInfo.title}</h2>
|
| 677 |
+
<p class="text-slate-300 text-lg leading-relaxed">${caseInfo.description}</p>
|
| 678 |
+
<div class="text-fuchsia-400 text-sm animate-bounce font-tech mt-4">▼ CLICK TO INVESTIGATE</div>
|
| 679 |
+
</div>
|
| 680 |
+
</div>
|
| 681 |
+
`;
|
| 682 |
+
|
| 683 |
+
overlay.onclick = () => {
|
| 684 |
+
overlay.remove();
|
| 685 |
+
this.showCaseSuspectsPhase(caseInfo);
|
| 686 |
+
};
|
| 687 |
+
|
| 688 |
+
document.body.appendChild(overlay);
|
| 689 |
+
}
|
| 690 |
+
|
| 691 |
+
showCaseSuspectsPhase(caseInfo) {
|
| 692 |
+
// Start timer NOW
|
| 693 |
+
this.caseStartTime = Date.now();
|
| 694 |
+
|
| 695 |
+
// Show evidence + suspects in the main scene area
|
| 696 |
+
this.showDialogue(false); // hide dialogue
|
| 697 |
+
this.elTarget.classList.remove('hidden');
|
| 698 |
+
document.getElementById('evidence-panel').classList.add('hidden');
|
| 699 |
+
const sprite = document.getElementById('speaker-sprite');
|
| 700 |
+
if (sprite) sprite.classList.add('hidden');
|
| 701 |
+
|
| 702 |
+
const wrapper = document.getElementById('suspect-wrapper');
|
| 703 |
+
wrapper.innerHTML = '';
|
| 704 |
+
wrapper.className = 'relative w-full flex flex-col items-center gap-6 py-4';
|
| 705 |
+
|
| 706 |
+
// Top row: Evidence image
|
| 707 |
+
const evidenceSection = document.createElement('div');
|
| 708 |
+
evidenceSection.className = 'flex flex-col items-center gap-2';
|
| 709 |
+
evidenceSection.innerHTML = `
|
| 710 |
+
<div class="font-tech text-fuchsia-400 text-sm tracking-widest">EVIDENCE / 蛛絲馬跡</div>
|
| 711 |
+
<img src="${caseInfo.evidenceImage}" class="h-[35vh] object-contain rounded-xl border-2 border-fuchsia-500/40 shadow-lg shadow-fuchsia-500/20" alt="Evidence">
|
| 712 |
+
`;
|
| 713 |
+
|
| 714 |
+
// Bottom row: Suspects (clickable)
|
| 715 |
+
const suspectSection = document.createElement('div');
|
| 716 |
+
suspectSection.className = 'flex flex-col items-center gap-2';
|
| 717 |
+
suspectSection.innerHTML = `<div class="font-tech text-cyan-400 text-sm tracking-widest">SUSPECTS / 嫌疑犯 <span class="text-xs text-slate-500">(點擊選擇犯人)</span></div>`;
|
| 718 |
+
|
| 719 |
+
const suspectRow = document.createElement('div');
|
| 720 |
+
suspectRow.className = 'flex gap-6 justify-center items-end';
|
| 721 |
+
|
| 722 |
+
caseInfo.suspects.forEach(s => {
|
| 723 |
+
const card = document.createElement('div');
|
| 724 |
+
card.className = 'flex flex-col items-center gap-1 cursor-pointer group transition-all duration-300';
|
| 725 |
+
card.innerHTML = `
|
| 726 |
+
<img src="${s.src}" class="h-[32vh] object-contain rounded-xl border-2 border-slate-600 group-hover:border-fuchsia-500 shadow-lg group-hover:shadow-fuchsia-500/30 transition-all duration-300 group-hover:scale-105" alt="${s.name}">
|
| 727 |
+
<span class="text-slate-400 group-hover:text-fuchsia-400 text-sm font-bold transition-colors">${s.name}</span>
|
| 728 |
+
`;
|
| 729 |
+
card.onclick = () => this.onSuspectClick(s, caseInfo);
|
| 730 |
+
suspectRow.appendChild(card);
|
| 731 |
+
});
|
| 732 |
+
|
| 733 |
+
suspectSection.appendChild(suspectRow);
|
| 734 |
+
|
| 735 |
+
wrapper.appendChild(evidenceSection);
|
| 736 |
+
wrapper.appendChild(suspectSection);
|
| 737 |
+
}
|
| 738 |
+
|
| 739 |
+
onSuspectClick(suspect, caseInfo) {
|
| 740 |
+
// Confirm dialog
|
| 741 |
+
const overlay = document.createElement('div');
|
| 742 |
+
overlay.id = 'confirm-overlay';
|
| 743 |
+
overlay.className = 'fixed inset-0 z-[200] flex items-center justify-center bg-black/70 backdrop-blur-sm';
|
| 744 |
+
overlay.innerHTML = `
|
| 745 |
+
<div class="glass-panel p-8 rounded-xl flex flex-col gap-6 max-w-md w-full items-center">
|
| 746 |
+
<img src="${suspect.src}" class="h-[20vh] object-contain rounded-lg border border-fuchsia-500/40" alt="${suspect.name}">
|
| 747 |
+
<h3 class="text-2xl font-bold text-white">確認 <span class="text-fuchsia-400">${suspect.name}</span> 是犯人嗎?</h3>
|
| 748 |
+
<div class="flex gap-4 w-full">
|
| 749 |
+
<button id="confirm-yes" class="flex-1 bg-fuchsia-600 hover:bg-fuchsia-500 text-white font-bold py-3 rounded-lg text-lg transition-all border border-fuchsia-400/50 hover:scale-105 transform duration-200">確認逮捕</button>
|
| 750 |
+
<button id="confirm-no" class="flex-1 bg-slate-700 hover:bg-slate-600 text-white font-bold py-3 rounded-lg text-lg transition-all border border-slate-500/50 hover:scale-105 transform duration-200">再想想</button>
|
| 751 |
+
</div>
|
| 752 |
+
</div>
|
| 753 |
+
`;
|
| 754 |
+
|
| 755 |
+
document.body.appendChild(overlay);
|
| 756 |
+
|
| 757 |
+
document.getElementById('confirm-no').onclick = () => overlay.remove();
|
| 758 |
+
document.getElementById('confirm-yes').onclick = () => {
|
| 759 |
+
overlay.remove();
|
| 760 |
+
this.onSuspectConfirmed(suspect, caseInfo);
|
| 761 |
+
};
|
| 762 |
+
}
|
| 763 |
+
|
| 764 |
+
onSuspectConfirmed(suspect, caseInfo) {
|
| 765 |
+
const isCorrectSuspect = (suspect.id === caseInfo.correctSuspect);
|
| 766 |
+
|
| 767 |
+
if (!isCorrectSuspect) {
|
| 768 |
+
this.caseHadError = true;
|
| 769 |
+
// Show wrong feedback but let them try again
|
| 770 |
+
const wrongOverlay = document.createElement('div');
|
| 771 |
+
wrongOverlay.id = 'wrong-suspect-overlay';
|
| 772 |
+
wrongOverlay.className = 'fixed inset-0 z-[200] flex items-center justify-center bg-black/70 backdrop-blur-sm cursor-pointer';
|
| 773 |
+
wrongOverlay.innerHTML = `
|
| 774 |
+
<div class="glass-panel p-8 rounded-xl flex flex-col gap-4 max-w-md w-full items-center border-2 border-red-500/50">
|
| 775 |
+
<div class="text-red-500 font-tech text-3xl font-bold">✗ WRONG SUSPECT</div>
|
| 776 |
+
<p class="text-slate-300 text-lg text-center">這不是犯人!請再仔細比對蛛絲馬跡和嫌疑犯的特徵。</p>
|
| 777 |
+
<div class="text-red-400 text-sm font-tech animate-bounce">▼ CLICK TO RETRY</div>
|
| 778 |
+
</div>
|
| 779 |
+
`;
|
| 780 |
+
wrongOverlay.onclick = () => wrongOverlay.remove();
|
| 781 |
+
document.body.appendChild(wrongOverlay);
|
| 782 |
+
return;
|
| 783 |
+
}
|
| 784 |
+
|
| 785 |
+
// Correct suspect! Now ask for proof method
|
| 786 |
+
this.askProofMethod(suspect, caseInfo);
|
| 787 |
+
}
|
| 788 |
+
|
| 789 |
+
askProofMethod(suspect, caseInfo) {
|
| 790 |
+
const suspectImg = suspect ? suspect.src : caseInfo.suspects.find(s => s.id === caseInfo.correctSuspect).src;
|
| 791 |
+
const suspectName = suspect ? suspect.name : caseInfo.suspects.find(s => s.id === caseInfo.correctSuspect).name;
|
| 792 |
+
|
| 793 |
+
const overlay = document.createElement('div');
|
| 794 |
+
overlay.id = 'proof-overlay';
|
| 795 |
+
overlay.className = 'fixed inset-0 z-[200] flex items-center justify-center bg-black/70 backdrop-blur-sm';
|
| 796 |
+
overlay.innerHTML = `
|
| 797 |
+
<div class="glass-panel p-8 rounded-xl flex flex-col gap-6 max-w-4xl w-full items-center">
|
| 798 |
+
<div class="font-tech text-green-400 text-2xl font-bold">✓ SUSPECT IDENTIFIED</div>
|
| 799 |
+
|
| 800 |
+
<div class="flex gap-8 justify-center items-start w-full">
|
| 801 |
+
<div class="flex flex-col items-center gap-2">
|
| 802 |
+
<div class="font-tech text-fuchsia-400 text-xs tracking-widest">EVIDENCE / 蛛絲馬跡</div>
|
| 803 |
+
<img src="${caseInfo.evidenceImage}" class="h-[30vh] object-contain rounded-xl border-2 border-fuchsia-500/40 shadow-lg shadow-fuchsia-500/20" alt="Evidence">
|
| 804 |
+
</div>
|
| 805 |
+
<div class="flex flex-col items-center gap-2">
|
| 806 |
+
<div class="font-tech text-cyan-400 text-xs tracking-widest">SUSPECT / ${suspectName}</div>
|
| 807 |
+
<img src="${suspectImg}" class="h-[30vh] object-contain rounded-xl border-2 border-cyan-500/40 shadow-lg shadow-cyan-500/20" alt="Suspect">
|
| 808 |
+
</div>
|
| 809 |
+
</div>
|
| 810 |
+
|
| 811 |
+
<p class="text-slate-300 text-lg text-center">你要用哪種方法來 <span class="text-fuchsia-400 font-bold">證明</span> 這位嫌疑犯就是犯人?</p>
|
| 812 |
+
<div class="flex gap-4 w-full max-w-md">
|
| 813 |
+
<button class="proof-btn flex-1 bg-gradient-to-br from-pink-600 to-fuchsia-700 hover:from-pink-500 hover:to-fuchsia-600 text-white font-bold py-4 rounded-lg text-2xl font-tech transition-all border border-fuchsia-400/50 hover:scale-105 transform duration-200 shadow-lg shadow-fuchsia-500/20" data-method="SSS">SSS</button>
|
| 814 |
+
<button class="proof-btn flex-1 bg-gradient-to-br from-amber-600 to-yellow-700 hover:from-amber-500 hover:to-yellow-600 text-white font-bold py-4 rounded-lg text-2xl font-tech transition-all border border-yellow-400/50 hover:scale-105 transform duration-200 shadow-lg shadow-yellow-500/20" data-method="SAS">SAS</button>
|
| 815 |
+
</div>
|
| 816 |
+
</div>
|
| 817 |
+
`;
|
| 818 |
+
|
| 819 |
+
document.body.appendChild(overlay);
|
| 820 |
+
|
| 821 |
+
overlay.querySelectorAll('.proof-btn').forEach(btn => {
|
| 822 |
+
btn.onclick = () => {
|
| 823 |
+
const method = btn.dataset.method;
|
| 824 |
+
overlay.remove();
|
| 825 |
+
this.onProofMethodSelected(method, caseInfo);
|
| 826 |
+
};
|
| 827 |
+
});
|
| 828 |
+
}
|
| 829 |
+
|
| 830 |
+
onProofMethodSelected(method, caseInfo) {
|
| 831 |
+
const isCorrect = (method === caseInfo.correctMethod);
|
| 832 |
+
|
| 833 |
+
if (!isCorrect) {
|
| 834 |
+
this.caseHadError = true;
|
| 835 |
+
// Wrong method, let them pick again
|
| 836 |
+
const wrongOverlay = document.createElement('div');
|
| 837 |
+
wrongOverlay.id = 'wrong-method-overlay';
|
| 838 |
+
wrongOverlay.className = 'fixed inset-0 z-[200] flex items-center justify-center bg-black/70 backdrop-blur-sm cursor-pointer';
|
| 839 |
+
wrongOverlay.innerHTML = `
|
| 840 |
+
<div class="glass-panel p-8 rounded-xl flex flex-col gap-4 max-w-md w-full items-center border-2 border-red-500/50">
|
| 841 |
+
<div class="text-red-500 font-tech text-3xl font-bold">✗ WRONG METHOD</div>
|
| 842 |
+
<p class="text-slate-300 text-lg text-center">這個證明方法不正確!請重新觀察證據中的邊和角。</p>
|
| 843 |
+
<div class="text-red-400 text-sm font-tech animate-bounce">▼ CLICK TO RETRY</div>
|
| 844 |
+
</div>
|
| 845 |
+
`;
|
| 846 |
+
wrongOverlay.onclick = () => {
|
| 847 |
+
wrongOverlay.remove();
|
| 848 |
+
this.askProofMethod(null, caseInfo);
|
| 849 |
+
};
|
| 850 |
+
document.body.appendChild(wrongOverlay);
|
| 851 |
+
return;
|
| 852 |
+
}
|
| 853 |
+
|
| 854 |
+
// All correct! Calculate score
|
| 855 |
+
const elapsed = Math.floor((Date.now() - this.caseStartTime) / 1000);
|
| 856 |
+
const grade = this.calculateGrade(elapsed, this.caseHadError);
|
| 857 |
+
this.showCaseResult(caseInfo, elapsed, grade);
|
| 858 |
+
}
|
| 859 |
+
|
| 860 |
+
calculateGrade(seconds, hadError) {
|
| 861 |
+
if (hadError) {
|
| 862 |
+
// Max B if had any error
|
| 863 |
+
if (seconds <= 30) return 'B';
|
| 864 |
+
return 'C';
|
| 865 |
+
}
|
| 866 |
+
// All correct, grading by speed
|
| 867 |
+
if (seconds <= 10) return 'S';
|
| 868 |
+
if (seconds <= 20) return 'A++';
|
| 869 |
+
if (seconds <= 30) return 'A+';
|
| 870 |
+
if (seconds <= 60) return 'A';
|
| 871 |
+
return 'B';
|
| 872 |
+
}
|
| 873 |
+
|
| 874 |
+
showCaseResult(caseInfo, seconds, grade) {
|
| 875 |
+
const gradeColors = {
|
| 876 |
+
'S': 'from-amber-400 to-yellow-600',
|
| 877 |
+
'A++': 'from-fuchsia-400 to-pink-600',
|
| 878 |
+
'A+': 'from-purple-400 to-violet-600',
|
| 879 |
+
'A': 'from-cyan-400 to-blue-600',
|
| 880 |
+
'B': 'from-green-400 to-emerald-600',
|
| 881 |
+
'C': 'from-slate-400 to-gray-600'
|
| 882 |
+
};
|
| 883 |
+
|
| 884 |
+
const gradeMessages = {
|
| 885 |
+
'S': '神探降臨!完美破案!',
|
| 886 |
+
'A++': '超凡的推理能力!',
|
| 887 |
+
'A+': '出色的辦案速度!',
|
| 888 |
+
'A': '幹得漂亮!',
|
| 889 |
+
'B': '案件已解決。',
|
| 890 |
+
'C': '勉強過關...'
|
| 891 |
+
};
|
| 892 |
+
|
| 893 |
+
const overlay = document.createElement('div');
|
| 894 |
+
overlay.id = 'result-overlay';
|
| 895 |
+
overlay.className = 'fixed inset-0 z-[200] flex items-center justify-center bg-black/85 backdrop-blur-md cursor-pointer';
|
| 896 |
+
overlay.innerHTML = `
|
| 897 |
+
<div class="flex flex-col items-center gap-6">
|
| 898 |
+
<div class="font-tech text-fuchsia-500 text-lg tracking-[0.5em]">CASE #00${caseInfo.id} CLOSED</div>
|
| 899 |
+
<div class="text-3xl font-bold text-white">${caseInfo.title}</div>
|
| 900 |
+
<div class="text-8xl font-black bg-gradient-to-br ${gradeColors[grade]} bg-clip-text text-transparent" style="text-shadow: 0 0 40px rgba(217,70,239,0.4); -webkit-text-stroke: 1px rgba(255,255,255,0.1);">
|
| 901 |
+
${grade}
|
| 902 |
+
</div>
|
| 903 |
+
<div class="text-xl text-slate-300">${gradeMessages[grade]}</div>
|
| 904 |
+
<div class="font-tech text-slate-500 text-sm">耗時 ${seconds} 秒</div>
|
| 905 |
+
<div class="text-fuchsia-400 text-sm animate-bounce font-tech mt-4">▼ CLICK TO CONTINUE</div>
|
| 906 |
+
</div>
|
| 907 |
+
`;
|
| 908 |
+
|
| 909 |
+
overlay.onclick = () => {
|
| 910 |
+
overlay.remove();
|
| 911 |
+
this.caseGrades[caseInfo.id] = grade;
|
| 912 |
+
this.onCaseComplete(caseInfo);
|
| 913 |
+
};
|
| 914 |
+
|
| 915 |
+
document.body.appendChild(overlay);
|
| 916 |
+
}
|
| 917 |
+
|
| 918 |
+
onCaseComplete(caseInfo) {
|
| 919 |
+
if (caseInfo.nextCase) {
|
| 920 |
+
// Chain to next case
|
| 921 |
+
this.startCase(caseInfo.nextCase);
|
| 922 |
+
} else {
|
| 923 |
+
// All cases done — show summary
|
| 924 |
+
this.showFinalSummary();
|
| 925 |
+
}
|
| 926 |
+
}
|
| 927 |
+
|
| 928 |
+
showFinalSummary() {
|
| 929 |
+
const gradeOrder = ['S', 'A++', 'A+', 'A', 'B', 'C'];
|
| 930 |
+
const gradeScores = { 'S': 100, 'A++': 90, 'A+': 80, 'A': 70, 'B': 50, 'C': 30 };
|
| 931 |
+
const gradeColors = {
|
| 932 |
+
'S': 'text-amber-400', 'A++': 'text-fuchsia-400', 'A+': 'text-purple-400',
|
| 933 |
+
'A': 'text-cyan-400', 'B': 'text-green-400', 'C': 'text-slate-400'
|
| 934 |
+
};
|
| 935 |
+
|
| 936 |
+
// Calculate overall grade from average score
|
| 937 |
+
const grades = this.caseGrades;
|
| 938 |
+
const totalScore = Object.values(grades).reduce((sum, g) => sum + (gradeScores[g] || 0), 0);
|
| 939 |
+
const avgScore = totalScore / Object.keys(grades).length;
|
| 940 |
+
let overallGrade = 'C';
|
| 941 |
+
if (avgScore >= 95) overallGrade = 'S';
|
| 942 |
+
else if (avgScore >= 85) overallGrade = 'A++';
|
| 943 |
+
else if (avgScore >= 75) overallGrade = 'A+';
|
| 944 |
+
else if (avgScore >= 65) overallGrade = 'A';
|
| 945 |
+
else if (avgScore >= 45) overallGrade = 'B';
|
| 946 |
+
|
| 947 |
+
// Save to localStorage (only if better than existing)
|
| 948 |
+
const storageKey = 'math_city_score_congruence';
|
| 949 |
+
const currentBest = localStorage.getItem(storageKey) || '';
|
| 950 |
+
const currentBestIdx = gradeOrder.indexOf(currentBest);
|
| 951 |
+
const newIdx = gradeOrder.indexOf(overallGrade);
|
| 952 |
+
if (currentBestIdx === -1 || newIdx < currentBestIdx) {
|
| 953 |
+
localStorage.setItem(storageKey, overallGrade);
|
| 954 |
+
}
|
| 955 |
+
|
| 956 |
+
const caseTitles = { 1: '霓虹暗巷重傷害案', 2: '頂級俱樂部入侵案', 3: '市政廳塗鴉案' };
|
| 957 |
+
|
| 958 |
+
// Build case grade cards
|
| 959 |
+
let caseCardsHtml = '';
|
| 960 |
+
for (let i = 1; i <= 3; i++) {
|
| 961 |
+
const g = grades[i] || 'C';
|
| 962 |
+
caseCardsHtml += `
|
| 963 |
+
<div class="flex items-center gap-4 bg-slate-800/60 rounded-xl px-6 py-3 border border-slate-700">
|
| 964 |
+
<span class="font-tech text-fuchsia-500 text-sm">CASE ${i}</span>
|
| 965 |
+
<span class="text-white text-sm flex-1">${caseTitles[i]}</span>
|
| 966 |
+
<span class="text-2xl font-black ${gradeColors[g]}">${g}</span>
|
| 967 |
+
</div>
|
| 968 |
+
`;
|
| 969 |
+
}
|
| 970 |
+
|
| 971 |
+
const overallGradeColors = {
|
| 972 |
+
'S': 'from-amber-400 to-yellow-600', 'A++': 'from-fuchsia-400 to-pink-600',
|
| 973 |
+
'A+': 'from-purple-400 to-violet-600', 'A': 'from-cyan-400 to-blue-600',
|
| 974 |
+
'B': 'from-green-400 to-emerald-600', 'C': 'from-slate-400 to-gray-600'
|
| 975 |
+
};
|
| 976 |
+
|
| 977 |
+
const hl = (t) => `<span class="text-yellow-400 font-bold">${t}</span>`;
|
| 978 |
+
|
| 979 |
+
const overlay = document.createElement('div');
|
| 980 |
+
overlay.id = 'final-summary-overlay';
|
| 981 |
+
overlay.className = 'fixed inset-0 z-[200] flex items-center justify-center bg-black/90 backdrop-blur-md overflow-y-auto py-8';
|
| 982 |
+
overlay.innerHTML = `
|
| 983 |
+
<div class="flex flex-col items-center gap-6 max-w-3xl w-full px-6">
|
| 984 |
+
<div class="font-tech text-fuchsia-500 text-lg tracking-[0.5em]">MISSION COMPLETE</div>
|
| 985 |
+
<div class="text-4xl font-black text-white">辦案總結</div>
|
| 986 |
+
|
| 987 |
+
<!-- Overall Grade -->
|
| 988 |
+
<div class="text-8xl font-black bg-gradient-to-br ${overallGradeColors[overallGrade]} bg-clip-text text-transparent" style="-webkit-text-stroke: 1px rgba(255,255,255,0.1);">
|
| 989 |
+
${overallGrade}
|
| 990 |
+
</div>
|
| 991 |
+
|
| 992 |
+
<!-- Case Breakdown -->
|
| 993 |
+
<div class="flex flex-col gap-3 w-full max-w-lg">
|
| 994 |
+
${caseCardsHtml}
|
| 995 |
+
</div>
|
| 996 |
+
|
| 997 |
+
<!-- Summary Text -->
|
| 998 |
+
<div class="glass-panel rounded-xl p-6 max-w-lg w-full text-slate-300 leading-relaxed text-base space-y-4 border border-fuchsia-500/20">
|
| 999 |
+
<p>恭喜你成功協助警方破案了,未來或許你真的有機會成為一個好警探呢!</p>
|
| 1000 |
+
<p>三角形全等是你學習數學以來,第一次碰到要${hl('「證明」')}的內容!證明的邏輯對於判斷事情的${hl('正確與否')}非常重要,或許也能提升你的${hl('吵架能力')}(?</p>
|
| 1001 |
+
<p>一個三角形中,有${hl('3個邊')},${hl('3個角')},但每次都要看${hl('6個')}條件才能證明全等,實在太麻煩了,於是數學家想盡辦法將6個${hl('簡化')}成${hl('3��')}條件即可判斷,除了剛剛學會的${hl('SSS')}和${hl('SAS')}外,${hl('還有其他幾種')}喔!剩下的細節等上課的時候我們再慢慢說吧~</p>
|
| 1002 |
+
</div>
|
| 1003 |
+
|
| 1004 |
+
<!-- Back Button -->
|
| 1005 |
+
<a href="index.html" class="bg-fuchsia-600 hover:bg-fuchsia-500 text-white font-bold py-4 px-12 rounded-xl text-xl font-tech tracking-widest border border-fuchsia-400/50 transition-all hover:scale-105 transform duration-200 shadow-lg shadow-fuchsia-500/30">
|
| 1006 |
+
回到 Math City
|
| 1007 |
+
</a>
|
| 1008 |
+
</div>
|
| 1009 |
+
`;
|
| 1010 |
+
|
| 1011 |
+
document.body.appendChild(overlay);
|
| 1012 |
+
}
|
| 1013 |
+
|
| 1014 |
+
showSSSSASImages() {
|
| 1015 |
+
this.elTarget.classList.remove('hidden');
|
| 1016 |
+
document.getElementById('evidence-panel').classList.add('hidden'); // Hide evidence panel
|
| 1017 |
+
const wrapper = document.getElementById('suspect-wrapper');
|
| 1018 |
+
// clear wrapper
|
| 1019 |
+
wrapper.innerHTML = '';
|
| 1020 |
+
|
| 1021 |
+
// Add images side by side
|
| 1022 |
+
const container = document.createElement('div');
|
| 1023 |
+
container.className = 'flex gap-8 justify-center items-center';
|
| 1024 |
+
|
| 1025 |
+
const img1 = document.createElement('img');
|
| 1026 |
+
img1.src = 'Assets/triangle/犯罪證明/紅.png';
|
| 1027 |
+
img1.className = 'h-[40vh] object-contain shadow-lg shadow-fuchsia-500/20 rounded-xl border border-slate-700';
|
| 1028 |
+
|
| 1029 |
+
const img2 = document.createElement('img');
|
| 1030 |
+
img2.src = 'Assets/triangle/犯罪證明/SAS黃.png';
|
| 1031 |
+
img2.className = 'h-[40vh] object-contain shadow-lg shadow-yellow-500/20 rounded-xl border border-slate-700';
|
| 1032 |
+
|
| 1033 |
+
container.appendChild(img1);
|
| 1034 |
+
container.appendChild(img2);
|
| 1035 |
+
wrapper.appendChild(container);
|
| 1036 |
+
|
| 1037 |
+
this.actionCompleted = true; // No interaction needed, just show
|
| 1038 |
+
this.next(); // Since it's an action that completes immediately visually
|
| 1039 |
+
}
|
| 1040 |
+
|
| 1041 |
+
showPracticeImages(actionName) {
|
| 1042 |
+
this.elTarget.classList.remove('hidden');
|
| 1043 |
+
document.getElementById('evidence-panel').classList.add('hidden');
|
| 1044 |
+
const wrapper = document.getElementById('suspect-wrapper');
|
| 1045 |
+
wrapper.innerHTML = '';
|
| 1046 |
+
|
| 1047 |
+
const container = document.createElement('div');
|
| 1048 |
+
container.className = 'flex gap-8 justify-center items-center';
|
| 1049 |
+
|
| 1050 |
+
let src1, src2;
|
| 1051 |
+
if (actionName === 'show_practice_1') {
|
| 1052 |
+
src1 = 'Assets/triangle/蛛絲馬跡/4.png';
|
| 1053 |
+
src2 = 'Assets/triangle/嫌疑犯/2農夫.png';
|
| 1054 |
+
} else if (actionName === 'show_practice_2') {
|
| 1055 |
+
src1 = 'Assets/triangle/蛛絲馬跡/1.png';
|
| 1056 |
+
src2 = 'Assets/triangle/嫌疑犯/5冰淇淋師傅.png';
|
| 1057 |
+
}
|
| 1058 |
+
|
| 1059 |
+
const img1 = document.createElement('img');
|
| 1060 |
+
img1.src = src1;
|
| 1061 |
+
img1.className = 'h-[70vh] object-contain shadow-lg shadow-fuchsia-500/20 rounded-xl border border-slate-700'; // Increased size
|
| 1062 |
+
|
| 1063 |
+
const img2 = document.createElement('img');
|
| 1064 |
+
img2.src = src2;
|
| 1065 |
+
img2.className = 'h-[70vh] object-contain shadow-lg shadow-cyan-500/20 rounded-xl border border-slate-700'; // Increased size
|
| 1066 |
+
|
| 1067 |
+
container.appendChild(img1);
|
| 1068 |
+
container.appendChild(img2);
|
| 1069 |
+
wrapper.appendChild(container);
|
| 1070 |
+
|
| 1071 |
+
this.actionCompleted = true;
|
| 1072 |
+
this.next();
|
| 1073 |
+
}
|
| 1074 |
+
|
| 1075 |
+
startInvestigation() {
|
| 1076 |
+
this.elTarget.classList.remove('hidden');
|
| 1077 |
+
document.getElementById('evidence-panel').classList.remove('hidden');
|
| 1078 |
+
|
| 1079 |
+
// Switch to the Feature Logic Image
|
| 1080 |
+
const img = document.getElementById('suspect-image');
|
| 1081 |
+
img.src = 'Assets/triangle/嫌疑犯/16造型師.png'; // Use new image with marks
|
| 1082 |
+
|
| 1083 |
+
this.foundFeatures.clear();
|
| 1084 |
+
this.angleCount = 0;
|
| 1085 |
+
this.sideCount = 0;
|
| 1086 |
+
this.updateStatsDisplay();
|
| 1087 |
+
this.updateEvidenceCount();
|
| 1088 |
+
|
| 1089 |
+
// Adjusted positions to match "Red/Blue Circles"
|
| 1090 |
+
const hitboxes = [
|
| 1091 |
+
// Angles (Red)
|
| 1092 |
+
// Top Vertex (Right-ish) - Averaged
|
| 1093 |
+
{ id: 'angle_top', x: 70, y: 26, type: 'Angle', class: 'found-angle' },
|
| 1094 |
+
// Bottom Left Vertex - Averaged
|
| 1095 |
+
{ id: 'angle_left', x: 20, y: 53, type: 'Angle', class: 'found-angle' },
|
| 1096 |
+
// Bottom Right Vertex - Averaged
|
| 1097 |
+
{ id: 'angle_right', x: 70, y: 53, type: 'Angle', class: 'found-angle' },
|
| 1098 |
+
|
| 1099 |
+
// Sides (Blue)
|
| 1100 |
+
// Hypotenuse (Midpoint of Top and Left) - Averaged
|
| 1101 |
+
{ id: 'side_hypotenuse', x: 45, y: 38, type: 'Side', class: 'found-side' },
|
| 1102 |
+
// Right Side (Midpoint of Top and Right) - Averaged
|
| 1103 |
+
{ id: 'side_right', x: 70, y: 40, type: 'Side', class: 'found-side' },
|
| 1104 |
+
// Bottom Side (Midpoint of Left and Right) - Averaged
|
| 1105 |
+
{ id: 'side_bottom', x: 45, y: 53, type: 'Side', class: 'found-side' }
|
| 1106 |
+
];
|
| 1107 |
+
|
| 1108 |
+
// Target the wrapper logic
|
| 1109 |
+
const wrapper = document.getElementById('suspect-wrapper');
|
| 1110 |
+
|
| 1111 |
+
const existing = wrapper.querySelectorAll('.hitbox');
|
| 1112 |
+
existing.forEach(e => e.remove());
|
| 1113 |
+
|
| 1114 |
+
hitboxes.forEach(hb => {
|
| 1115 |
+
const el = document.createElement('div');
|
| 1116 |
+
el.className = 'hitbox';
|
| 1117 |
+
el.style.left = hb.x + '%';
|
| 1118 |
+
el.style.top = hb.y + '%';
|
| 1119 |
+
el.style.transform = 'translate(-50%, -50%)';
|
| 1120 |
+
el.dataset.id = hb.id;
|
| 1121 |
+
el.onclick = (e) => this.onFeatureClick(e, el, hb);
|
| 1122 |
+
wrapper.appendChild(el);
|
| 1123 |
+
});
|
| 1124 |
+
}
|
| 1125 |
+
|
| 1126 |
+
onFeatureClick(e, el, hb) {
|
| 1127 |
+
e.stopPropagation();
|
| 1128 |
+
if (this.foundFeatures.has(el.dataset.id)) return;
|
| 1129 |
+
|
| 1130 |
+
el.classList.add(hb.class); // Use specific color class
|
| 1131 |
+
this.foundFeatures.add(el.dataset.id);
|
| 1132 |
+
|
| 1133 |
+
if (hb.type === 'Angle') this.angleCount++;
|
| 1134 |
+
if (hb.type === 'Side') this.sideCount++;
|
| 1135 |
+
|
| 1136 |
+
this.updateStatsDisplay();
|
| 1137 |
+
this.updateEvidenceCount(); // Keeps the top-right counter too
|
| 1138 |
+
|
| 1139 |
+
// this.showFloatingText(e.clientX, e.clientY, hb.type + ' Found!');
|
| 1140 |
+
if (hb.type === 'Angle') el.innerText = 'A';
|
| 1141 |
+
if (hb.type === 'Side') el.innerText = 'S';
|
| 1142 |
+
|
| 1143 |
+
if (this.foundFeatures.size >= this.requiredFeatures) {
|
| 1144 |
+
this.completeInvestigation();
|
| 1145 |
+
}
|
| 1146 |
+
}
|
| 1147 |
+
|
| 1148 |
+
updateStatsDisplay() {
|
| 1149 |
+
const display = document.getElementById('stats-display');
|
| 1150 |
+
if (display) {
|
| 1151 |
+
display.innerText = `[已找到邊數:${this.sideCount},已找到角數:${this.angleCount}]`;
|
| 1152 |
+
}
|
| 1153 |
+
}
|
| 1154 |
+
|
| 1155 |
+
updateEvidenceCount() {
|
| 1156 |
+
const count = document.getElementById('evidence-count');
|
| 1157 |
+
count.innerText = `${this.foundFeatures.size} / ${this.requiredFeatures}`;
|
| 1158 |
+
}
|
| 1159 |
+
|
| 1160 |
+
showFloatingText(x, y, text) {
|
| 1161 |
+
const el = document.createElement('div');
|
| 1162 |
+
el.className = 'fixed pointer-events-none text-white font-bold text-shadow animate-float-up z-50 text-xl';
|
| 1163 |
+
el.style.left = x + 'px';
|
| 1164 |
+
el.style.top = y + 'px';
|
| 1165 |
+
el.innerText = text;
|
| 1166 |
+
document.body.appendChild(el);
|
| 1167 |
+
setTimeout(() => el.remove(), 1000);
|
| 1168 |
+
}
|
| 1169 |
+
|
| 1170 |
+
completeInvestigation() {
|
| 1171 |
+
this.actionCompleted = true;
|
| 1172 |
+
setTimeout(() => {
|
| 1173 |
+
this.step++;
|
| 1174 |
+
|
| 1175 |
+
// Add next dialogue/logic
|
| 1176 |
+
const fmtName = (n) => `<span class="text-amber-400 font-bold mx-1 text-xl">${n}</span>`;
|
| 1177 |
+
const nextPart = [
|
| 1178 |
+
{
|
| 1179 |
+
type: 'dialogue',
|
| 1180 |
+
speaker: '資深警探',
|
| 1181 |
+
type: 'dialogue',
|
| 1182 |
+
speaker: '資深警探',
|
| 1183 |
+
text: '很好,看來我們真的找對人了,三角形的所有<span class="text-yellow-400 font-bold">特徵</span>就是他的<span class="text-yellow-400 font-bold">三個邊</span>和<span class="text-yellow-400 font-bold">三個角</span>,如果六項特徵全部都符合的話,那兩個三角形一定能<span class="text-yellow-400 font-bold">完全重疊</span>在一起,表示這是<span class="text-yellow-400 font-bold">同一個三角形</span>。'
|
| 1184 |
+
},
|
| 1185 |
+
{
|
| 1186 |
+
type: 'dialogue',
|
| 1187 |
+
speaker: '資深警探',
|
| 1188 |
+
text: '但是我們在犯罪現場的證據,並沒有辦法這麼完整,現在我要教你<span class="text-yellow-400 font-bold">2種更好用的</span>辨認方式!'
|
| 1189 |
+
},
|
| 1190 |
+
{
|
| 1191 |
+
type: 'dialogue',
|
| 1192 |
+
speaker: '資深警探',
|
| 1193 |
+
text: '首先要教你一些我們的專用術語 <span class="text-yellow-400">S=Side=三角形的邊</span>,<span class="text-yellow-400">A=Angle=三角形的角</span>'
|
| 1194 |
+
},
|
| 1195 |
+
{
|
| 1196 |
+
type: 'quiz',
|
| 1197 |
+
question: '現在請你告訴我,將「SSS」換成中文是什麼?',
|
| 1198 |
+
answer: ['邊邊邊', '三邊由', '三邊', '三個邊'],
|
| 1199 |
+
correctMsg: '沒錯!S就是邊,SSS就是邊邊邊!',
|
| 1200 |
+
wrongMsg: '不對喔... (提示:S是邊,SSS就是?)'
|
| 1201 |
+
},
|
| 1202 |
+
{
|
| 1203 |
+
type: 'quiz',
|
| 1204 |
+
question: '那麼,「SAS」換成中文是什麼?',
|
| 1205 |
+
answer: ['邊角邊'],
|
| 1206 |
+
correctMsg: '太好了,你學得很快嘛!',
|
| 1207 |
+
wrongMsg: '再想想... (提示:S是邊,A是角,SAS就是?)'
|
| 1208 |
+
},
|
| 1209 |
+
{
|
| 1210 |
+
type: 'dialogue',
|
| 1211 |
+
speaker: '資深警探',
|
| 1212 |
+
text: '接下來我要教你的高級刑偵技術,也就是證明犯人的方法<span class="text-yellow-400 font-bold">SSS</span>和<span class="text-yellow-400 font-bold">SAS</span>'
|
| 1213 |
+
},
|
| 1214 |
+
{
|
| 1215 |
+
type: 'action',
|
| 1216 |
+
action: 'show_sss_sas_images',
|
| 1217 |
+
speaker: '資深警探',
|
| 1218 |
+
text: '<span class="text-yellow-400 font-bold">SSS</span>:線索中的三個邊長和嫌疑犯的三個邊長完全相等<br><span class="text-yellow-400 font-bold">SAS</span>:線索中的兩個邊和夾起來的角,與嫌疑犯的兩個邊和夾起來的角相等<br><br>只要有找到其中一種,就能幫我們證明他就是犯人!這樣你學會了嗎?'
|
| 1219 |
+
},
|
| 1220 |
+
{
|
| 1221 |
+
type: 'quiz',
|
| 1222 |
+
question: '請問你剛剛學到的證明方法其中一個是?(英文)',
|
| 1223 |
+
answer: ['SSS', 'SAS'],
|
| 1224 |
+
correctMsg: '沒錯!這是其中一個!',
|
| 1225 |
+
wrongMsg: '請輸入SSS或SAS'
|
| 1226 |
+
},
|
| 1227 |
+
{
|
| 1228 |
+
type: 'quiz',
|
| 1229 |
+
question: '另一個是?',
|
| 1230 |
+
answer: ['SSS', 'SAS'], // Logic: User should enter the other one, but simplified game engine checks inclusion.
|
| 1231 |
+
// To properly 'exclude' the previous answer without complex logic injection, we'll just accept both for now
|
| 1232 |
+
// but the user phrasing implies they should know the other.
|
| 1233 |
+
// Given constraints, I'll accept both to avoid getting stuck if they repeat.
|
| 1234 |
+
correctMsg: '太棒了!你都記住了!',
|
| 1235 |
+
wrongMsg: '請輸入另一個證明方法 (SSS或SAS)'
|
| 1236 |
+
},
|
| 1237 |
+
{
|
| 1238 |
+
type: 'quiz',
|
| 1239 |
+
question: '那SSS的中文是?',
|
| 1240 |
+
answer: ['邊邊邊'],
|
| 1241 |
+
correctMsg: '完全正確!',
|
| 1242 |
+
wrongMsg: '提示:S是邊...'
|
| 1243 |
+
},
|
| 1244 |
+
{
|
| 1245 |
+
type: 'quiz',
|
| 1246 |
+
question: '還有SAS的中文是?',
|
| 1247 |
+
answer: ['邊角邊'],
|
| 1248 |
+
correctMsg: '太強了!你已經具備資深警探的潛力了!',
|
| 1249 |
+
wrongMsg: '提示:A是角...'
|
| 1250 |
+
},
|
| 1251 |
+
{
|
| 1252 |
+
type: 'dialogue',
|
| 1253 |
+
speaker: '資深警探',
|
| 1254 |
+
text: '接下來要測驗你剛剛學到的證明方法囉!'
|
| 1255 |
+
},
|
| 1256 |
+
{
|
| 1257 |
+
type: 'action',
|
| 1258 |
+
action: 'show_practice_1',
|
| 1259 |
+
speaker: '資深警探',
|
| 1260 |
+
text: '請看這兩張圖...'
|
| 1261 |
+
},
|
| 1262 |
+
{
|
| 1263 |
+
type: 'quiz',
|
| 1264 |
+
question: '請問要用哪個證明方法說明兩者是同一人呢?(SSS或SAS)',
|
| 1265 |
+
answer: ['SSS'],
|
| 1266 |
+
correctMsg: '答對了!三邊對應相等!',
|
| 1267 |
+
wrongMsg: '再仔細看看,是三個邊相等還是兩邊一角?'
|
| 1268 |
+
},
|
| 1269 |
+
{
|
| 1270 |
+
type: 'action',
|
| 1271 |
+
action: 'show_practice_2',
|
| 1272 |
+
speaker: '資深警探',
|
| 1273 |
+
text: '再來試試這一題...'
|
| 1274 |
+
},
|
| 1275 |
+
{
|
| 1276 |
+
type: 'quiz',
|
| 1277 |
+
question: '請問這題要用哪個證明方法說明兩者是同一人呢?(SSS或SAS)',
|
| 1278 |
+
answer: ['SAS'],
|
| 1279 |
+
correctMsg: '太厲害了!兩邊一夾角對應相等!',
|
| 1280 |
+
wrongMsg: '再仔細看看,有幾個邊?有幾個角?'
|
| 1281 |
+
},
|
| 1282 |
+
{
|
| 1283 |
+
type: 'dialogue',
|
| 1284 |
+
speaker: '資深警探',
|
| 1285 |
+
text: (name) => `太好了,既然${fmtName(name)}成功通過測驗了,接下來有幾個案件要請你幫忙了!`
|
| 1286 |
+
},
|
| 1287 |
+
{
|
| 1288 |
+
type: 'dialogue',
|
| 1289 |
+
speaker: '資深警探',
|
| 1290 |
+
text: '我們會為你的辦案狀況打分數,請以<span class="text-yellow-400 font-bold">正確性</span>為<span class="text-yellow-400 font-bold">優先</span>,速度為次,抓錯犯人可是很傷腦筋的呢~'
|
| 1291 |
+
},
|
| 1292 |
+
{
|
| 1293 |
+
type: 'action',
|
| 1294 |
+
action: 'start_case_1',
|
| 1295 |
+
speaker: 'SYSTEM',
|
| 1296 |
+
text: '案件載入中...'
|
| 1297 |
+
}
|
| 1298 |
+
];
|
| 1299 |
+
|
| 1300 |
+
// Replace remaining script or append?
|
| 1301 |
+
// Since SCRIPT is simpler now, we can just splice.
|
| 1302 |
+
// NOTE: Previous logic used `startInvestigation` at step X.
|
| 1303 |
+
// We need to inject these steps AFTER the current step.
|
| 1304 |
+
// Remove old placeholder steps if any.
|
| 1305 |
+
|
| 1306 |
+
SCRIPT.splice(this.step, SCRIPT.length - this.step, ...nextPart);
|
| 1307 |
+
// Reset step index to process the first inserted item
|
| 1308 |
+
this.processStep();
|
| 1309 |
+
|
| 1310 |
+
}, 1000);
|
| 1311 |
+
}
|
| 1312 |
+
|
| 1313 |
+
appendNextPhase() {
|
| 1314 |
+
// Deprecated in favor of direct injection in completeInvestigation
|
| 1315 |
+
}
|
| 1316 |
+
|
| 1317 |
+
showQuiz(data) {
|
| 1318 |
+
const quizLayer = document.getElementById('quiz-layer');
|
| 1319 |
+
const quizContent = document.getElementById('quiz-content');
|
| 1320 |
+
|
| 1321 |
+
quizLayer.classList.remove('hidden');
|
| 1322 |
+
|
| 1323 |
+
// Integrated layout matching dialogue style exactly
|
| 1324 |
+
|
| 1325 |
+
// Integrated layout matching dialogue style exactly
|
| 1326 |
+
quizContent.innerHTML = `
|
| 1327 |
+
<div class="dialogue-name">資深警探 <span class="text-xs text-slate-500 ml-2 tracking-normal">// AUTHENTICATION REQUIRED</span></div>
|
| 1328 |
+
<div class="dialogue-text mb-4">${data.question}</div>
|
| 1329 |
+
|
| 1330 |
+
<div class="flex gap-4 w-full mt-auto items-end">
|
| 1331 |
+
<div class="text-fuchsia-500 font-tech text-xl animate-pulse">></div>
|
| 1332 |
+
<input type="text" id="quiz-input" class="flex-1 bg-transparent border-b-2 border-fuchsia-500/50 text-white text-xl p-2 focus:outline-none focus:border-fuchsia-400 font-mono tracking-wider placeholder-slate-600" placeholder="輸入答案..." autocomplete="off">
|
| 1333 |
+
<button id="quiz-submit" class="bg-fuchsia-600 hover:bg-fuchsia-500 text-white font-bold px-8 py-2 rounded clip-path-slant text-lg font-tech tracking-widest border border-fuchsia-400/50 transition-all shadow-lg shadow-fuchsia-500/20">CONFIRM</button>
|
| 1334 |
+
</div>
|
| 1335 |
+
<div id="quiz-feedback" class="absolute top-6 right-8 font-tech text-xl font-bold"></div>
|
| 1336 |
+
`;
|
| 1337 |
+
|
| 1338 |
+
const input = document.getElementById('quiz-input');
|
| 1339 |
+
const btn = document.getElementById('quiz-submit');
|
| 1340 |
+
const feedback = document.getElementById('quiz-feedback');
|
| 1341 |
+
|
| 1342 |
+
input.focus();
|
| 1343 |
+
|
| 1344 |
+
const checkAnswer = () => {
|
| 1345 |
+
const val = input.value.trim();
|
| 1346 |
+
if (data.answer.includes(val)) {
|
| 1347 |
+
// Correct
|
| 1348 |
+
feedback.className = 'text-green-400 font-bold';
|
| 1349 |
+
feedback.innerText = 'ACCESS GRANTED';
|
| 1350 |
+
input.classList.add('text-green-400');
|
| 1351 |
+
input.disabled = true;
|
| 1352 |
+
|
| 1353 |
+
setTimeout(() => {
|
| 1354 |
+
quizLayer.classList.add('hidden');
|
| 1355 |
+
|
| 1356 |
+
// Add temporary dialogue for success reaction
|
| 1357 |
+
this.showDialogue(true);
|
| 1358 |
+
this.setSpeaker('資深警探');
|
| 1359 |
+
this.typeText(data.correctMsg);
|
| 1360 |
+
}, 1000);
|
| 1361 |
+
} else {
|
| 1362 |
+
// Wrong
|
| 1363 |
+
input.value = '';
|
| 1364 |
+
input.classList.add('animate-shake');
|
| 1365 |
+
|
| 1366 |
+
// Clear shake separately
|
| 1367 |
+
setTimeout(() => {
|
| 1368 |
+
input.classList.remove('animate-shake');
|
| 1369 |
+
}, 500);
|
| 1370 |
+
|
| 1371 |
+
// Show persistent wrong message
|
| 1372 |
+
feedback.className = 'text-red-400 text-lg font-bold'; // Increased size for visibility
|
| 1373 |
+
feedback.innerText = data.wrongMsg;
|
| 1374 |
+
}
|
| 1375 |
+
};
|
| 1376 |
+
|
| 1377 |
+
btn.onclick = checkAnswer;
|
| 1378 |
+
input.onkeypress = (e) => { if (e.key === 'Enter') checkAnswer(); };
|
| 1379 |
+
}
|
| 1380 |
+
|
| 1381 |
+
// Add shake animation style dynamically
|
| 1382 |
+
addStyles() {
|
| 1383 |
+
const style = document.createElement('style');
|
| 1384 |
+
style.innerHTML = `
|
| 1385 |
+
@keyframes float-up {
|
| 1386 |
+
0% { transform: translateY(0); opacity: 1; }
|
| 1387 |
+
100% { transform: translateY(-50px); opacity: 0; }
|
| 1388 |
+
}
|
| 1389 |
+
.animate-float-up {
|
| 1390 |
+
animation: float-up 1s ease-out forwards;
|
| 1391 |
+
}
|
| 1392 |
+
@keyframes shake {
|
| 1393 |
+
0%, 100% { transform: translateX(0); }
|
| 1394 |
+
25% { transform: translateX(-5px); }
|
| 1395 |
+
75% { transform: translateX(5px); }
|
| 1396 |
+
}
|
| 1397 |
+
.animate-shake {
|
| 1398 |
+
animation: shake 0.3s ease-in-out;
|
| 1399 |
+
}
|
| 1400 |
+
`;
|
| 1401 |
+
document.head.appendChild(style);
|
| 1402 |
+
}
|
| 1403 |
+
}
|
| 1404 |
+
|
| 1405 |
+
// Init Game
|
| 1406 |
+
window.onload = () => {
|
| 1407 |
+
window.game = new GameEngine();
|
| 1408 |
+
};
|
| 1409 |
+
|
| 1410 |
+
</script>
|
| 1411 |
+
</body>
|
| 1412 |
+
|
| 1413 |
+
</html>
|
example/algebra.html
ADDED
|
@@ -0,0 +1,578 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="zh-Hant">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>數學探險島 - 代數之丘</title>
|
| 7 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 8 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 9 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 10 |
+
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@400;500;700&display=swap" rel="stylesheet">
|
| 11 |
+
<style>
|
| 12 |
+
body {
|
| 13 |
+
font-family: 'Noto Sans TC', sans-serif;
|
| 14 |
+
touch-action: none; /* 防止在觸控拖曳時滾動頁面 */
|
| 15 |
+
background-image: url('https://i.meee.com.tw/AF01kln.png');
|
| 16 |
+
background-size: cover;
|
| 17 |
+
background-position: center;
|
| 18 |
+
}
|
| 19 |
+
/* 自訂積木顏色 */
|
| 20 |
+
.block-a-squared { background-color: #fca5a5; } /* red-300 */
|
| 21 |
+
.block-b-squared { background-color: #818cf8; } /* indigo-400 */
|
| 22 |
+
.block-ab { background-color: #fcd34d; } /* amber-300 */
|
| 23 |
+
.block-distractor { background-color: #9ca3af; } /* gray-400 */
|
| 24 |
+
|
| 25 |
+
/* 拼圖區格線 */
|
| 26 |
+
#grid-container {
|
| 27 |
+
display: grid;
|
| 28 |
+
background-image:
|
| 29 |
+
linear-gradient(to right, #d1d5db 1px, transparent 1px),
|
| 30 |
+
linear-gradient(to bottom, #d1d5db 1px, transparent 1px);
|
| 31 |
+
background-size: var(--unit-size) var(--unit-size);
|
| 32 |
+
border: 2px solid #6b7280;
|
| 33 |
+
}
|
| 34 |
+
/* 拖曳中的複製物件 */
|
| 35 |
+
.dragging-clone {
|
| 36 |
+
position: absolute;
|
| 37 |
+
pointer-events: none; /* 讓滑鼠事件穿透複製物件 */
|
| 38 |
+
z-index: 1000;
|
| 39 |
+
opacity: 0.8;
|
| 40 |
+
border: 2px dashed #4f46e5;
|
| 41 |
+
}
|
| 42 |
+
/* 已放置在選項區的積木 */
|
| 43 |
+
.palette-block.placed {
|
| 44 |
+
opacity: 0.3;
|
| 45 |
+
cursor: not-allowed;
|
| 46 |
+
pointer-events: none;
|
| 47 |
+
}
|
| 48 |
+
/* 已放置在拼圖區的積木 */
|
| 49 |
+
.placed-block {
|
| 50 |
+
position: absolute;
|
| 51 |
+
cursor: pointer;
|
| 52 |
+
border: 2px solid #1f2937;
|
| 53 |
+
transition: all 0.2s ease-in-out;
|
| 54 |
+
}
|
| 55 |
+
.placed-block:hover {
|
| 56 |
+
transform: scale(1.05);
|
| 57 |
+
box-shadow: 0 0 15px rgba(0,0,0,0.5);
|
| 58 |
+
}
|
| 59 |
+
/* 過關時的特別框線 */
|
| 60 |
+
.highlight-win {
|
| 61 |
+
background-color: #fef9c3; /* yellow-100 */
|
| 62 |
+
border: 2px solid #f59e0b; /* amber-500 */
|
| 63 |
+
border-radius: 8px;
|
| 64 |
+
padding: 2px 6px;
|
| 65 |
+
display: inline-block;
|
| 66 |
+
}
|
| 67 |
+
/* 隱藏滾動條,但在觸控設備上仍可滾動 */
|
| 68 |
+
#block-palette::-webkit-scrollbar {
|
| 69 |
+
display: none;
|
| 70 |
+
}
|
| 71 |
+
#block-palette {
|
| 72 |
+
-ms-overflow-style: none; /* IE and Edge */
|
| 73 |
+
scrollbar-width: none; /* Firefox */
|
| 74 |
+
}
|
| 75 |
+
</style>
|
| 76 |
+
</head>
|
| 77 |
+
<body class="w-screen h-screen overflow-hidden flex items-center justify-center relative">
|
| 78 |
+
|
| 79 |
+
<!-- 任務說明畫面 -->
|
| 80 |
+
<div id="start-screen" class="absolute inset-0 w-screen h-screen flex items-center justify-center p-4 transition-opacity duration-500 bg-gray-900/50">
|
| 81 |
+
<div class="container mx-auto p-6 md:p-8 bg-white/90 backdrop-blur-sm rounded-xl shadow-2xl max-w-2xl text-center">
|
| 82 |
+
<h1 class="text-3xl md:text-4xl font-bold text-indigo-600 mb-6">任務說明:代數之丘</h1>
|
| 83 |
+
<p class="text-lg text-gray-700 mb-4">歡迎來到代數之丘!在這裡,我們不用死背公式,而是用雙手「拼」出數學!</p>
|
| 84 |
+
<p class="text-lg text-gray-700 mb-8">你的任務是拖曳下方的彩色積木,將左邊的灰色正方形完全填滿,親身體驗 <span class="font-mono font-bold text-indigo-700">(a+b)² = a² + 2ab + b²</span> 這個公式是如何誕生的!</p>
|
| 85 |
+
<button id="start-button" class="w-full md:w-1/2 bg-indigo-600 text-white font-bold py-3 md:py-4 px-6 rounded-lg hover:bg-indigo-700 transition-colors shadow-lg text-lg md:text-xl">
|
| 86 |
+
開始挑戰
|
| 87 |
+
</button>
|
| 88 |
+
</div>
|
| 89 |
+
</div>
|
| 90 |
+
|
| 91 |
+
<!-- 遊戲主畫面 -->
|
| 92 |
+
<div id="game-container" class="flex flex-row w-full h-full max-w-screen-xl mx-auto p-4 lg:p-8 gap-8 items-start transition-opacity duration-500 opacity-0 hidden">
|
| 93 |
+
|
| 94 |
+
<!-- 左欄:包含拼圖區和過關訊息 -->
|
| 95 |
+
<div class="w-1/3 flex flex-col items-center justify-start gap-6">
|
| 96 |
+
<div id="grid-container" class="relative bg-gray-200/80 backdrop-blur-sm shadow-xl rounded-lg">
|
| 97 |
+
<!-- 拼圖區的積木會由 JS 動態加入這裡 -->
|
| 98 |
+
</div>
|
| 99 |
+
<!-- 過關訊息與按鈕 (已移至此處) -->
|
| 100 |
+
<div id="win-message-container" class="w-full text-center p-4 bg-white/90 backdrop-blur-sm rounded-xl shadow-lg hidden">
|
| 101 |
+
<p class="text-2xl font-bold text-green-600">恭喜過關!</p>
|
| 102 |
+
<p id="win-formula-explanation" class="text-lg text-gray-700 mt-2"></p>
|
| 103 |
+
<button id="next-level-button" class="mt-4 w-full bg-indigo-600 text-white font-bold py-3 px-4 rounded-lg hover:bg-indigo-700 transition-colors shadow-md">
|
| 104 |
+
挑戰下一關
|
| 105 |
+
</button>
|
| 106 |
+
</div>
|
| 107 |
+
</div>
|
| 108 |
+
|
| 109 |
+
<!-- 右欄:控制項 -->
|
| 110 |
+
<div id="controls-container" class="w-2/3 flex-shrink-0 flex flex-col bg-white/90 backdrop-blur-sm rounded-xl shadow-lg p-6 h-full">
|
| 111 |
+
<h1 class="text-3xl font-bold text-gray-800 text-center">代數之丘</h1>
|
| 112 |
+
<hr class="my-4">
|
| 113 |
+
|
| 114 |
+
<!-- 算式區 -->
|
| 115 |
+
<div class="bg-blue-50/80 p-4 rounded-lg mb-4 space-y-2">
|
| 116 |
+
<!-- 題目行 -->
|
| 117 |
+
<div class="grid grid-cols-4 items-baseline">
|
| 118 |
+
<p class="text-lg text-gray-600 font-semibold col-span-1">題目:</p>
|
| 119 |
+
<p id="equation-title" class="col-span-3 text-2xl font-bold text-indigo-700 text-center"></p>
|
| 120 |
+
</div>
|
| 121 |
+
<!-- 面積結果行 -->
|
| 122 |
+
<div class="grid grid-cols-4 items-baseline">
|
| 123 |
+
<p class="text-lg text-gray-600 font-semibold col-span-1">面積結果:</p>
|
| 124 |
+
<div id="equation-result" class="col-span-3 text-2xl font-mono text-gray-800 text-center"></div>
|
| 125 |
+
</div>
|
| 126 |
+
</div>
|
| 127 |
+
|
| 128 |
+
<!-- 說明文字 -->
|
| 129 |
+
<div id="instructions" class="text-center text-gray-500 mb-4 text-sm">
|
| 130 |
+
<p>請從下方拖曳積木,拼滿左邊的正方形。</p>
|
| 131 |
+
<p class="mt-1">💡 <span class="font-semibold">提示:</span>點擊已放置的積木可以將它移除。</p>
|
| 132 |
+
</div>
|
| 133 |
+
|
| 134 |
+
<!-- 積木選項區 (包含捲動按鈕) -->
|
| 135 |
+
<div class="relative mt-auto">
|
| 136 |
+
<div id="block-palette" class="flex flex-row flex-nowrap overflow-x-auto items-center gap-4 p-4 bg-gray-100/80 rounded-md">
|
| 137 |
+
<!-- 積木選項會由 JS 動態加入這裡 -->
|
| 138 |
+
</div>
|
| 139 |
+
<!-- 左捲動按鈕 -->
|
| 140 |
+
<button id="scroll-left-button" class="absolute left-0 top-1/2 -translate-y-1/2 bg-white/80 hover:bg-white rounded-full p-1 shadow-md hidden transition-opacity">
|
| 141 |
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-gray-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
| 142 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
| 143 |
+
</svg>
|
| 144 |
+
</button>
|
| 145 |
+
<!-- 右捲動按鈕 -->
|
| 146 |
+
<button id="scroll-right-button" class="absolute right-0 top-1/2 -translate-y-1/2 bg-white/80 hover:bg-white rounded-full p-1 shadow-md hidden transition-opacity">
|
| 147 |
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-gray-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
| 148 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
| 149 |
+
</svg>
|
| 150 |
+
</button>
|
| 151 |
+
</div>
|
| 152 |
+
</div>
|
| 153 |
+
</div>
|
| 154 |
+
|
| 155 |
+
<!-- 秘密揭曉畫面 (從 secret.html 整合進來) -->
|
| 156 |
+
<div id="secret-view" class="absolute inset-0 w-screen h-screen flex items-center justify-center p-4 opacity-0 hidden transition-opacity duration-500 bg-gray-900/50">
|
| 157 |
+
<div class="container mx-auto p-6 md:p-8 bg-white rounded-xl shadow-2xl max-w-4xl text-center overflow-y-auto max-h-[90vh]">
|
| 158 |
+
<h1 class="text-3xl md:text-4xl font-bold text-indigo-600 mb-8">代數之丘最大的秘密</h1>
|
| 159 |
+
|
| 160 |
+
<div class="grid md:grid-cols-3 gap-6 md:gap-8 mb-8">
|
| 161 |
+
<!-- Image 1 -->
|
| 162 |
+
<div class="flex flex-col items-center p-2 border rounded-lg">
|
| 163 |
+
<img src="https://i.meee.com.tw/Sx68qrS.png" alt="拼圖 (6+4)²" class="rounded-md shadow-md mb-4 w-full" onerror="this.onerror=null;this.src='https://placehold.co/400x400/fca5a5/ffffff?text=(6%2B4)%C2%B2';">
|
| 164 |
+
<p class="text-lg md:text-xl font-mono font-bold">(6+4)² = 6² + <span class="highlight-win">2×6×4</span> + 4²</p>
|
| 165 |
+
</div>
|
| 166 |
+
<!-- Image 2 -->
|
| 167 |
+
<div class="flex flex-col items-center p-2 border rounded-lg">
|
| 168 |
+
<img src="https://i.meee.com.tw/7gGAB80.png" alt="拼圖 (4+9)²" class="rounded-md shadow-md mb-4 w-full" onerror="this.onerror=null;this.src='https://placehold.co/400x400/818cf8/ffffff?text=(4%2B9)%C2%B2';">
|
| 169 |
+
<p class="text-lg md:text-xl font-mono font-bold">(4+9)² = 4² + <span class="highlight-win">2×4×9</span> + 9²</p>
|
| 170 |
+
</div>
|
| 171 |
+
<!-- Image 3 -->
|
| 172 |
+
<div class="flex flex-col items-center p-2 border rounded-lg">
|
| 173 |
+
<img src="https://i.meee.com.tw/0hdfRvP.png" alt="拼圖 (7+5)²" class="rounded-md shadow-md mb-4 w-full" onerror="this.onerror=null;this.src='https://placehold.co/400x400/fcd34d/ffffff?text=(7%2B5)%C2%B2';">
|
| 174 |
+
<p class="text-lg md:text-xl font-mono font-bold">(7+5)² = 7² + <span class="highlight-win">2×7×5</span> + 5²</p>
|
| 175 |
+
</div>
|
| 176 |
+
</div>
|
| 177 |
+
|
| 178 |
+
<div class="text-left text-base md:text-lg text-gray-700 space-y-4 border-t-2 pt-8">
|
| 179 |
+
<p>選舉最大的祕密就是:票多的贏,票少的輸...<span class="italic text-gray-500">咳咳不是這個啦</span></p>
|
| 180 |
+
<p class="text-xl md:text-2xl font-bold text-red-600">代數之丘的最大秘密就是:(a+b)²展開後一定會有ab這項,而且還是<span class="underline decoration-wavy decoration-amber-500">2ab</span>,學會這個,這單元就已經學會一半了!</p>
|
| 181 |
+
<p class="font-semibold text-indigo-700">(請在學習單上做紀錄!)</p>
|
| 182 |
+
</div>
|
| 183 |
+
|
| 184 |
+
<div class="mt-12">
|
| 185 |
+
<a href="index.html" class="inline-block w-full md:w-1/2 bg-indigo-600 text-white font-bold py-3 md:py-4 px-6 rounded-lg hover:bg-indigo-700 transition-colors shadow-lg text-lg md:text-xl">
|
| 186 |
+
回到探險島地圖
|
| 187 |
+
</a>
|
| 188 |
+
</div>
|
| 189 |
+
</div>
|
| 190 |
+
</div>
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
<script>
|
| 194 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 195 |
+
// --- 常數與設定 ---
|
| 196 |
+
const UNIT_SIZE = 30; // 每個單位格的像素大小 (px)
|
| 197 |
+
const levels = [
|
| 198 |
+
{ a: 6, b: 4, distractors: [] },
|
| 199 |
+
{ a: 4, b: 9, distractors: [{w: 5, h: 8}, {w: 7, h: 6}] },
|
| 200 |
+
{ a: 7, b: 5, distractors: [{w: 8, h: 6}, {w: 6, h: 8}] }
|
| 201 |
+
];
|
| 202 |
+
let currentLevel = 0;
|
| 203 |
+
|
| 204 |
+
// --- DOM 元素 ---
|
| 205 |
+
const startScreen = document.getElementById('start-screen');
|
| 206 |
+
const startButton = document.getElementById('start-button');
|
| 207 |
+
const gameContainer = document.getElementById('game-container');
|
| 208 |
+
const secretView = document.getElementById('secret-view');
|
| 209 |
+
const gridContainer = document.getElementById('grid-container');
|
| 210 |
+
const blockPalette = document.getElementById('block-palette');
|
| 211 |
+
const equationTitle = document.getElementById('equation-title');
|
| 212 |
+
const equationResult = document.getElementById('equation-result');
|
| 213 |
+
const winMessageContainer = document.getElementById('win-message-container');
|
| 214 |
+
const winFormulaExplanation = document.getElementById('win-formula-explanation');
|
| 215 |
+
const nextLevelButton = document.getElementById('next-level-button');
|
| 216 |
+
const scrollLeftButton = document.getElementById('scroll-left-button');
|
| 217 |
+
const scrollRightButton = document.getElementById('scroll-right-button');
|
| 218 |
+
|
| 219 |
+
// --- 遊戲狀態 ---
|
| 220 |
+
let gridState = []; // 2D 陣列,記錄拼圖區每個格子的佔用情況
|
| 221 |
+
let placedBlocks = new Map(); // 記錄已放置的積木 (key: placedId, value: { el, originalId, type })
|
| 222 |
+
let draggingElement = null; // 目前正在拖曳的原始積木
|
| 223 |
+
let clone = null; // 拖曳時的複製物件
|
| 224 |
+
let offset = { x: 0, y: 0 }; // 拖曳時的滑鼠偏移量
|
| 225 |
+
|
| 226 |
+
// --- 核心函式 ---
|
| 227 |
+
|
| 228 |
+
function initLevel(levelIndex) {
|
| 229 |
+
currentLevel = levelIndex;
|
| 230 |
+
const level = levels[levelIndex];
|
| 231 |
+
const totalSize = level.a + level.b;
|
| 232 |
+
|
| 233 |
+
gridContainer.innerHTML = '';
|
| 234 |
+
blockPalette.innerHTML = '';
|
| 235 |
+
winMessageContainer.classList.add('hidden');
|
| 236 |
+
placedBlocks.clear();
|
| 237 |
+
|
| 238 |
+
gridContainer.style.width = `${totalSize * UNIT_SIZE}px`;
|
| 239 |
+
gridContainer.style.height = `${totalSize * UNIT_SIZE}px`;
|
| 240 |
+
gridContainer.style.setProperty('--unit-size', `${UNIT_SIZE}px`);
|
| 241 |
+
|
| 242 |
+
gridState = Array(totalSize).fill(null).map(() => Array(totalSize).fill(null));
|
| 243 |
+
|
| 244 |
+
createBlocks(level);
|
| 245 |
+
updateEquation();
|
| 246 |
+
|
| 247 |
+
if (currentLevel === levels.length - 1) {
|
| 248 |
+
nextLevelButton.textContent = '查看代數的秘密';
|
| 249 |
+
} else {
|
| 250 |
+
nextLevelButton.textContent = '挑戰下一關';
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
// 延遲執行以確保 DOM 尺寸已更新
|
| 254 |
+
setTimeout(updateScrollButtonsVisibility, 100);
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
function createBlocks(level) {
|
| 258 |
+
const { a, b, distractors } = level;
|
| 259 |
+
const blocksData = [
|
| 260 |
+
{ id: 'a_squared', w: a, h: a, type: 'correct', class: 'block-a-squared', label: `${a}×${a}` },
|
| 261 |
+
{ id: 'b_squared', w: b, h: b, type: 'correct', class: 'block-b-squared', label: `${b}×${b}` },
|
| 262 |
+
{ id: 'ab1', w: a, h: b, type: 'correct', class: 'block-ab', label: `${a}×${b}` },
|
| 263 |
+
{ id: 'ab2', w: b, h: a, type: 'correct', class: 'block-ab', label: `${b}×${a}` },
|
| 264 |
+
...distractors.map((d, i) => ({
|
| 265 |
+
id: `distractor_${i}`, w: d.w, h: d.h, type: 'distractor', class: 'block-distractor', label: `${d.w}×${d.h}`
|
| 266 |
+
}))
|
| 267 |
+
];
|
| 268 |
+
|
| 269 |
+
blocksData.sort(() => Math.random() - 0.5);
|
| 270 |
+
|
| 271 |
+
blocksData.forEach(data => {
|
| 272 |
+
const blockEl = document.createElement('div');
|
| 273 |
+
blockEl.id = `palette-${data.id}`;
|
| 274 |
+
blockEl.className = `palette-block flex-shrink-0 ${data.class} rounded-md shadow-sm cursor-grab flex items-center justify-center text-white font-bold text-base`;
|
| 275 |
+
blockEl.style.width = `${data.w * UNIT_SIZE}px`;
|
| 276 |
+
blockEl.style.height = `${data.h * UNIT_SIZE}px`;
|
| 277 |
+
blockEl.textContent = data.label;
|
| 278 |
+
|
| 279 |
+
blockEl.dataset.id = data.id;
|
| 280 |
+
blockEl.dataset.w = data.w;
|
| 281 |
+
blockEl.dataset.h = data.h;
|
| 282 |
+
blockEl.dataset.type = data.type;
|
| 283 |
+
blockEl.dataset.class = data.class;
|
| 284 |
+
|
| 285 |
+
blockPalette.appendChild(blockEl);
|
| 286 |
+
});
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
function updateEquation() {
|
| 290 |
+
const level = levels[currentLevel];
|
| 291 |
+
equationTitle.textContent = `(${level.a} + ${level.b})²`;
|
| 292 |
+
|
| 293 |
+
const placedCorrectBlocks = Array.from(placedBlocks.values()).filter(b => b.type === 'correct');
|
| 294 |
+
|
| 295 |
+
let parts = [];
|
| 296 |
+
if (placedCorrectBlocks.some(b => b.originalId === 'a_squared')) parts.push(`${level.a}²`);
|
| 297 |
+
if (placedCorrectBlocks.some(b => b.originalId === 'b_squared')) parts.push(`${level.b}²`);
|
| 298 |
+
|
| 299 |
+
const abCount = placedCorrectBlocks.filter(b => b.originalId.startsWith('ab')).length;
|
| 300 |
+
if (abCount === 1) parts.push(`(${level.a}×${level.b})`);
|
| 301 |
+
if (abCount === 2) parts.push(`2(${level.a}×${level.b})`);
|
| 302 |
+
|
| 303 |
+
if (parts.length > 0) {
|
| 304 |
+
equationResult.innerHTML = parts.join(' + ');
|
| 305 |
+
} else {
|
| 306 |
+
equationResult.innerHTML = '...';
|
| 307 |
+
}
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
// --- 拖曳事件處理 ---
|
| 311 |
+
|
| 312 |
+
function onDragStart(e) {
|
| 313 |
+
const target = e.target.closest('.palette-block');
|
| 314 |
+
if (!target || target.classList.contains('placed')) return;
|
| 315 |
+
|
| 316 |
+
e.preventDefault();
|
| 317 |
+
draggingElement = target;
|
| 318 |
+
|
| 319 |
+
const rect = target.getBoundingClientRect();
|
| 320 |
+
const pointer = getPointer(e);
|
| 321 |
+
offset.x = pointer.x - rect.left;
|
| 322 |
+
offset.y = pointer.y - rect.top;
|
| 323 |
+
|
| 324 |
+
clone = target.cloneNode(true);
|
| 325 |
+
clone.classList.remove('palette-block');
|
| 326 |
+
clone.classList.add('dragging-clone');
|
| 327 |
+
clone.style.width = `${target.dataset.w * UNIT_SIZE}px`;
|
| 328 |
+
clone.style.height = `${target.dataset.h * UNIT_SIZE}px`;
|
| 329 |
+
document.body.appendChild(clone);
|
| 330 |
+
|
| 331 |
+
moveClone(pointer.x, pointer.y);
|
| 332 |
+
|
| 333 |
+
document.addEventListener('mousemove', onDragMove);
|
| 334 |
+
document.addEventListener('touchmove', onDragMove, { passive: false });
|
| 335 |
+
document.addEventListener('mouseup', onDragEnd);
|
| 336 |
+
document.addEventListener('touchend', onDragEnd);
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
function onDragMove(e) {
|
| 340 |
+
if (!clone) return;
|
| 341 |
+
e.preventDefault();
|
| 342 |
+
const pointer = getPointer(e);
|
| 343 |
+
moveClone(pointer.x, pointer.y);
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
function onDragEnd(e) {
|
| 347 |
+
if (!draggingElement || !clone) return;
|
| 348 |
+
|
| 349 |
+
const gridRect = gridContainer.getBoundingClientRect();
|
| 350 |
+
const pointer = getPointer(e);
|
| 351 |
+
|
| 352 |
+
const blockTopLeftX = pointer.x - gridRect.left - offset.x;
|
| 353 |
+
const blockTopLeftY = pointer.y - gridRect.top - offset.y;
|
| 354 |
+
|
| 355 |
+
const gridX = Math.round(blockTopLeftX / UNIT_SIZE);
|
| 356 |
+
const gridY = Math.round(blockTopLeftY / UNIT_SIZE);
|
| 357 |
+
|
| 358 |
+
const w = parseInt(draggingElement.dataset.w);
|
| 359 |
+
const h = parseInt(draggingElement.dataset.h);
|
| 360 |
+
const type = draggingElement.dataset.type;
|
| 361 |
+
|
| 362 |
+
if (canPlace(gridX, gridY, w, h, type)) {
|
| 363 |
+
placeBlock(gridX, gridY, w, h);
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
document.body.removeChild(clone);
|
| 367 |
+
clone = null;
|
| 368 |
+
draggingElement = null;
|
| 369 |
+
document.removeEventListener('mousemove', onDragMove);
|
| 370 |
+
document.removeEventListener('touchmove', onDragMove);
|
| 371 |
+
document.removeEventListener('mouseup', onDragEnd);
|
| 372 |
+
document.removeEventListener('touchend', onDragEnd);
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
function canPlace(gridX, gridY, w, h, type) {
|
| 376 |
+
const totalSize = levels[currentLevel].a + levels[currentLevel].b;
|
| 377 |
+
|
| 378 |
+
if (currentLevel === 1 && type === 'distractor') {
|
| 379 |
+
return false;
|
| 380 |
+
}
|
| 381 |
+
|
| 382 |
+
if (gridX < 0 || gridY < 0 || gridX + w > totalSize || gridY + h > totalSize) {
|
| 383 |
+
return false;
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
for (let i = gridY; i < gridY + h; i++) {
|
| 387 |
+
for (let j = gridX; j < gridX + w; j++) {
|
| 388 |
+
if (i >= totalSize || j >= totalSize || gridState[i][j] !== null) {
|
| 389 |
+
return false;
|
| 390 |
+
}
|
| 391 |
+
}
|
| 392 |
+
}
|
| 393 |
+
return true;
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
+
function placeBlock(gridX, gridY, w, h) {
|
| 397 |
+
const placedId = `placed-${Date.now()}`;
|
| 398 |
+
|
| 399 |
+
for (let i = gridY; i < gridY + h; i++) {
|
| 400 |
+
for (let j = gridX; j < gridX + w; j++) {
|
| 401 |
+
gridState[i][j] = placedId;
|
| 402 |
+
}
|
| 403 |
+
}
|
| 404 |
+
|
| 405 |
+
const placedEl = document.createElement('div');
|
| 406 |
+
placedEl.id = placedId;
|
| 407 |
+
placedEl.className = `placed-block ${draggingElement.dataset.class} flex items-center justify-center text-white font-bold`;
|
| 408 |
+
placedEl.style.left = `${gridX * UNIT_SIZE}px`;
|
| 409 |
+
placedEl.style.top = `${gridY * UNIT_SIZE}px`;
|
| 410 |
+
placedEl.style.width = `${w * UNIT_SIZE}px`;
|
| 411 |
+
placedEl.style.height = `${h * UNIT_SIZE}px`;
|
| 412 |
+
placedEl.textContent = `${w}×${h}`;
|
| 413 |
+
gridContainer.appendChild(placedEl);
|
| 414 |
+
|
| 415 |
+
placedBlocks.set(placedId, {
|
| 416 |
+
el: placedEl,
|
| 417 |
+
originalId: draggingElement.dataset.id,
|
| 418 |
+
type: draggingElement.dataset.type,
|
| 419 |
+
gridX, gridY, w, h
|
| 420 |
+
});
|
| 421 |
+
|
| 422 |
+
draggingElement.classList.add('placed');
|
| 423 |
+
placedEl.addEventListener('click', () => removeBlock(placedId));
|
| 424 |
+
|
| 425 |
+
updateEquation();
|
| 426 |
+
checkWinCondition();
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
function removeBlock(placedId) {
|
| 430 |
+
const block = placedBlocks.get(placedId);
|
| 431 |
+
if (!block) return;
|
| 432 |
+
|
| 433 |
+
gridContainer.removeChild(block.el);
|
| 434 |
+
|
| 435 |
+
for (let i = block.gridY; i < block.gridY + block.h; i++) {
|
| 436 |
+
for (let j = block.gridX; j < block.gridX + block.w; j++) {
|
| 437 |
+
if (gridState[i][j] === placedId) {
|
| 438 |
+
gridState[i][j] = null;
|
| 439 |
+
}
|
| 440 |
+
}
|
| 441 |
+
}
|
| 442 |
+
|
| 443 |
+
placedBlocks.delete(placedId);
|
| 444 |
+
|
| 445 |
+
const originalBlock = document.getElementById(`palette-${block.originalId}`);
|
| 446 |
+
if (originalBlock) {
|
| 447 |
+
originalBlock.classList.remove('placed');
|
| 448 |
+
}
|
| 449 |
+
|
| 450 |
+
updateEquation();
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
+
function checkWinCondition() {
|
| 454 |
+
const level = levels[currentLevel];
|
| 455 |
+
const totalSize = level.a + level.b;
|
| 456 |
+
|
| 457 |
+
const placedCorrectCount = Array.from(placedBlocks.values()).filter(b => b.type === 'correct').length;
|
| 458 |
+
|
| 459 |
+
if (placedCorrectCount !== 4) return;
|
| 460 |
+
|
| 461 |
+
let isFull = true;
|
| 462 |
+
for (let i = 0; i < totalSize; i++) {
|
| 463 |
+
for (let j = 0; j < totalSize; j++) {
|
| 464 |
+
if (gridState[i][j] === null) {
|
| 465 |
+
isFull = false;
|
| 466 |
+
break;
|
| 467 |
+
}
|
| 468 |
+
}
|
| 469 |
+
if (!isFull) break;
|
| 470 |
+
}
|
| 471 |
+
|
| 472 |
+
if (isFull) {
|
| 473 |
+
showWinState();
|
| 474 |
+
}
|
| 475 |
+
}
|
| 476 |
+
|
| 477 |
+
function showWinState() {
|
| 478 |
+
const level = levels[currentLevel];
|
| 479 |
+
const finalFormula = `(${level.a}+${level.b})² = ${level.a}² + <span class="highlight-win">2×${level.a}×${level.b}</span> + ${level.b}²`;
|
| 480 |
+
winFormulaExplanation.innerHTML = finalFormula;
|
| 481 |
+
winMessageContainer.classList.remove('hidden');
|
| 482 |
+
|
| 483 |
+
blockPalette.style.pointerEvents = 'none';
|
| 484 |
+
gridContainer.style.pointerEvents = 'none';
|
| 485 |
+
}
|
| 486 |
+
|
| 487 |
+
// --- 捲動與輔助函式 ---
|
| 488 |
+
|
| 489 |
+
function updateScrollButtonsVisibility() {
|
| 490 |
+
const palette = blockPalette;
|
| 491 |
+
const scrollLeft = palette.scrollLeft;
|
| 492 |
+
const scrollWidth = palette.scrollWidth;
|
| 493 |
+
const clientWidth = palette.clientWidth;
|
| 494 |
+
|
| 495 |
+
if (scrollWidth <= clientWidth) {
|
| 496 |
+
scrollLeftButton.classList.add('hidden');
|
| 497 |
+
scrollRightButton.classList.add('hidden');
|
| 498 |
+
return;
|
| 499 |
+
}
|
| 500 |
+
|
| 501 |
+
if (scrollLeft > 0) {
|
| 502 |
+
scrollLeftButton.classList.remove('hidden');
|
| 503 |
+
} else {
|
| 504 |
+
scrollLeftButton.classList.add('hidden');
|
| 505 |
+
}
|
| 506 |
+
|
| 507 |
+
if (scrollLeft < scrollWidth - clientWidth - 1) {
|
| 508 |
+
scrollRightButton.classList.remove('hidden');
|
| 509 |
+
} else {
|
| 510 |
+
scrollRightButton.classList.add('hidden');
|
| 511 |
+
}
|
| 512 |
+
}
|
| 513 |
+
|
| 514 |
+
function getPointer(e) {
|
| 515 |
+
// For touchend event, we need to use changedTouches because touches is empty.
|
| 516 |
+
if (e.changedTouches && e.changedTouches.length > 0) {
|
| 517 |
+
return { x: e.changedTouches[0].clientX, y: e.changedTouches[0].clientY };
|
| 518 |
+
}
|
| 519 |
+
// For touchstart and touchmove events.
|
| 520 |
+
if (e.touches && e.touches.length > 0) {
|
| 521 |
+
return { x: e.touches[0].clientX, y: e.touches[0].clientY };
|
| 522 |
+
}
|
| 523 |
+
// Fallback for mouse events.
|
| 524 |
+
return { x: e.clientX, y: e.clientY };
|
| 525 |
+
}
|
| 526 |
+
|
| 527 |
+
|
| 528 |
+
function moveClone(x, y) {
|
| 529 |
+
if (!clone) return;
|
| 530 |
+
clone.style.left = `${x - offset.x}px`;
|
| 531 |
+
clone.style.top = `${y - offset.y}px`;
|
| 532 |
+
}
|
| 533 |
+
|
| 534 |
+
// --- 事件監聽 ---
|
| 535 |
+
startButton.addEventListener('click', () => {
|
| 536 |
+
startScreen.style.opacity = '0';
|
| 537 |
+
gameContainer.classList.remove('hidden');
|
| 538 |
+
setTimeout(() => {
|
| 539 |
+
startScreen.classList.add('hidden');
|
| 540 |
+
gameContainer.style.opacity = '1';
|
| 541 |
+
initLevel(currentLevel);
|
| 542 |
+
}, 500);
|
| 543 |
+
});
|
| 544 |
+
|
| 545 |
+
blockPalette.addEventListener('mousedown', onDragStart);
|
| 546 |
+
blockPalette.addEventListener('touchstart', onDragStart, { passive: false });
|
| 547 |
+
blockPalette.addEventListener('scroll', updateScrollButtonsVisibility);
|
| 548 |
+
window.addEventListener('resize', updateScrollButtonsVisibility);
|
| 549 |
+
|
| 550 |
+
scrollLeftButton.addEventListener('click', () => {
|
| 551 |
+
blockPalette.scrollBy({ left: -200, behavior: 'smooth' });
|
| 552 |
+
});
|
| 553 |
+
|
| 554 |
+
scrollRightButton.addEventListener('click', () => {
|
| 555 |
+
blockPalette.scrollBy({ left: 200, behavior: 'smooth' });
|
| 556 |
+
});
|
| 557 |
+
|
| 558 |
+
nextLevelButton.addEventListener('click', () => {
|
| 559 |
+
if (currentLevel < levels.length - 1) {
|
| 560 |
+
initLevel(currentLevel + 1);
|
| 561 |
+
blockPalette.style.pointerEvents = 'auto';
|
| 562 |
+
gridContainer.style.pointerEvents = 'auto';
|
| 563 |
+
} else {
|
| 564 |
+
// 顯示秘密畫面
|
| 565 |
+
gameContainer.style.opacity = '0';
|
| 566 |
+
secretView.classList.remove('hidden');
|
| 567 |
+
setTimeout(() => {
|
| 568 |
+
gameContainer.classList.add('hidden');
|
| 569 |
+
secretView.style.opacity = '1';
|
| 570 |
+
}, 500);
|
| 571 |
+
}
|
| 572 |
+
});
|
| 573 |
+
|
| 574 |
+
});
|
| 575 |
+
</script>
|
| 576 |
+
|
| 577 |
+
</body>
|
| 578 |
+
</html>
|
example/functionex1.png
ADDED
|
Git LFS Details
|
example/functionex2.png
ADDED
|
Git LFS Details
|
example/functionex3.png
ADDED
|
Git LFS Details
|
example/gemstone.html
ADDED
|
@@ -0,0 +1,694 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="zh-Hant">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>數學探險島 - 寶石洞窟</title>
|
| 7 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 8 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 9 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 10 |
+
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@400;500;700&display=swap" rel="stylesheet">
|
| 11 |
+
<style>
|
| 12 |
+
body {
|
| 13 |
+
font-family: 'Noto Sans TC', sans-serif;
|
| 14 |
+
background-image: url('https://i.meee.com.tw/pI0c5ue.png');
|
| 15 |
+
background-size: cover;
|
| 16 |
+
background-position: center;
|
| 17 |
+
}
|
| 18 |
+
canvas {
|
| 19 |
+
background-color: #2d241d;
|
| 20 |
+
display: block;
|
| 21 |
+
border-radius: 0.5rem;
|
| 22 |
+
box-shadow: inset 0 0 20px rgba(0,0,0,0.5);
|
| 23 |
+
}
|
| 24 |
+
.action-button {
|
| 25 |
+
transition: all 0.2s ease-in-out;
|
| 26 |
+
box-shadow: 0 5px #995f00;
|
| 27 |
+
}
|
| 28 |
+
.action-button:active {
|
| 29 |
+
transform: translateY(3px);
|
| 30 |
+
box-shadow: 0 2px #995f00;
|
| 31 |
+
}
|
| 32 |
+
.move-button {
|
| 33 |
+
transition: all 0.2s ease-in-out;
|
| 34 |
+
box-shadow: 0 5px #1e3a8a;
|
| 35 |
+
}
|
| 36 |
+
.move-button:active {
|
| 37 |
+
transform: translateY(3px);
|
| 38 |
+
box-shadow: 0 2px #1e3a8a;
|
| 39 |
+
}
|
| 40 |
+
.gem-button {
|
| 41 |
+
transition: transform 0.1s ease-in-out;
|
| 42 |
+
}
|
| 43 |
+
.gem-button:active {
|
| 44 |
+
transform: scale(0.9);
|
| 45 |
+
}
|
| 46 |
+
.secret-bg {
|
| 47 |
+
background-image: url('https://i.meee.com.tw/EKZpYKI.png');
|
| 48 |
+
background-size: cover;
|
| 49 |
+
background-position: center;
|
| 50 |
+
}
|
| 51 |
+
</style>
|
| 52 |
+
</head>
|
| 53 |
+
<body class="flex items-center justify-center h-screen overflow-hidden">
|
| 54 |
+
|
| 55 |
+
<div id="container" class="w-full max-w-md mx-auto text-white p-4 flex flex-col h-full">
|
| 56 |
+
|
| 57 |
+
<!-- 引導畫面 -->
|
| 58 |
+
<div id="start-screen" class="flex flex-col items-center justify-center text-center p-8 bg-black bg-opacity-60 rounded-lg my-auto">
|
| 59 |
+
<h1 class="text-4xl font-bold text-amber-300 mb-4">寶石洞窟</h1>
|
| 60 |
+
<p class="text-lg text-gray-200 mb-8">這是一個已經廢棄的寶石洞窟,接下來你必須駕駛一台礦車,成功閃避障礙物來獲得璀璨寶石!</p>
|
| 61 |
+
<button id="start-button" class="action-button bg-amber-500 hover:bg-amber-600 text-gray-900 font-bold py-4 px-8 rounded-lg text-2xl">
|
| 62 |
+
開始採礦
|
| 63 |
+
</button>
|
| 64 |
+
</div>
|
| 65 |
+
|
| 66 |
+
<!-- 遊戲畫面 -->
|
| 67 |
+
<div id="game-screen" class="hidden flex-col h-full">
|
| 68 |
+
<div id="canvas-wrapper" class="relative">
|
| 69 |
+
<canvas id="gameCanvas"></canvas>
|
| 70 |
+
<!-- 操作說明畫面 -->
|
| 71 |
+
<div id="instructions-screen" class="hidden absolute inset-0 bg-black bg-opacity-80 flex flex-col items-center justify-center text-center p-4 rounded-lg">
|
| 72 |
+
<h2 class="text-3xl font-bold text-amber-300 mb-6">操作說明</h2>
|
| 73 |
+
<div class="space-y-4 text-lg">
|
| 74 |
+
<p>點擊畫面<span class="text-blue-400 font-bold">左半邊</span>或按<span class="text-blue-400 font-bold">左方向鍵</span> ← 向左移動</p>
|
| 75 |
+
<p>點擊畫面<span class="text-blue-400 font-bold">右半邊</span>或按<span class="text-blue-400 font-bold">右方向鍵</span> → 向右移動</p>
|
| 76 |
+
<p class="mt-4">也可以使用下方的按鈕操作!</p>
|
| 77 |
+
</div>
|
| 78 |
+
<button id="play-game-button" class="action-button bg-green-500 hover:bg-green-600 text-white font-bold py-3 px-8 rounded-lg text-xl mt-8">
|
| 79 |
+
了解!
|
| 80 |
+
</button>
|
| 81 |
+
</div>
|
| 82 |
+
<!-- 遊戲結束畫面 -->
|
| 83 |
+
<div id="game-over-screen" class="hidden absolute inset-0 bg-black bg-opacity-70 flex flex-col items-center justify-center text-center p-4 rounded-lg">
|
| 84 |
+
<h2 class="text-4xl font-bold text-red-500 mb-4">挑戰失敗!</h2>
|
| 85 |
+
<p class="text-xl mb-6">礦車撞毀了!</p>
|
| 86 |
+
<button id="restart-button" class="action-button bg-amber-500 hover:bg-amber-600 text-gray-900 font-bold py-3 px-6 rounded-lg text-xl">
|
| 87 |
+
重新挑戰
|
| 88 |
+
</button>
|
| 89 |
+
</div>
|
| 90 |
+
<!-- 遊戲獲勝畫面 -->
|
| 91 |
+
<div id="win-screen" class="hidden absolute inset-0 bg-black bg-opacity-70 flex flex-col items-center justify-center text-center p-4 rounded-lg">
|
| 92 |
+
<h2 class="text-4xl font-bold text-green-400 mb-4">成功!</h2>
|
| 93 |
+
<p class="text-xl mb-2">你成功抵達了寶石區域!</p>
|
| 94 |
+
<p id="win-perfect-dodges" class="text-2xl text-amber-300 font-bold mb-6"></p>
|
| 95 |
+
<button id="continue-button" class="action-button bg-green-500 hover:bg-green-600 text-white font-bold py-3 px-6 rounded-lg text-xl">
|
| 96 |
+
繼續前進
|
| 97 |
+
</button>
|
| 98 |
+
</div>
|
| 99 |
+
</div>
|
| 100 |
+
<!-- 移動按鈕 -->
|
| 101 |
+
<div id="controls" class="flex justify-between mt-4">
|
| 102 |
+
<button id="move-left-button" class="move-button bg-blue-600 hover:bg-blue-700 text-white font-bold p-4 rounded-lg w-24 h-16">
|
| 103 |
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 mx-auto" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" /></svg>
|
| 104 |
+
</button>
|
| 105 |
+
<button id="move-right-button" class="move-button bg-blue-600 hover:bg-blue-700 text-white font-bold p-4 rounded-lg w-24 h-16">
|
| 106 |
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 mx-auto" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" /></svg>
|
| 107 |
+
</button>
|
| 108 |
+
</div>
|
| 109 |
+
</div>
|
| 110 |
+
|
| 111 |
+
<!-- 寶石解謎畫面 -->
|
| 112 |
+
<div id="puzzle-screen" class="hidden flex-col h-full p-6 bg-black bg-opacity-60 rounded-lg">
|
| 113 |
+
<div class="flex-grow overflow-y-auto">
|
| 114 |
+
<h1 class="text-3xl font-bold text-amber-300 text-center mb-4">寶石組合挑戰</h1>
|
| 115 |
+
<p class="text-center mb-4">觀察大寶石,點擊下方的小寶石,組合出正確的配方!</p>
|
| 116 |
+
|
| 117 |
+
<div class="flex justify-center mb-4">
|
| 118 |
+
<img id="large-gem-image" src="" class="h-40 object-contain" alt="[大寶石的Image]">
|
| 119 |
+
</div>
|
| 120 |
+
|
| 121 |
+
<div id="answer-slots" class="bg-gray-900/50 min-h-[80px] rounded-lg p-2 flex flex-wrap items-center justify-center gap-2 mb-4 border-2 border-amber-400"></div>
|
| 122 |
+
|
| 123 |
+
<div id="small-gem-options" class="flex justify-center items-center gap-4 mb-6 flex-wrap"></div>
|
| 124 |
+
|
| 125 |
+
<div id="puzzle-feedback" class="text-center text-xl font-bold min-h-[32px] mb-4"></div>
|
| 126 |
+
</div>
|
| 127 |
+
|
| 128 |
+
<div class="flex gap-4 mt-auto pt-4">
|
| 129 |
+
<button id="reset-puzzle-button" class="w-1/2 bg-gray-500 hover:bg-gray-600 text-white font-bold py-3 rounded-lg">重置</button>
|
| 130 |
+
<button id="check-puzzle-button" class="w-1/2 bg-green-500 hover:bg-green-600 text-white font-bold py-3 rounded-lg">鑑定寶石</button>
|
| 131 |
+
</div>
|
| 132 |
+
</div>
|
| 133 |
+
|
| 134 |
+
<!-- 過場畫面 -->
|
| 135 |
+
<div id="intermission-screen" class="hidden flex-col text-center p-8 bg-black bg-opacity-60 rounded-lg my-auto">
|
| 136 |
+
<h1 class="text-3xl font-bold text-amber-300 mb-4">準備挑戰下一區!</h1>
|
| 137 |
+
<p class="text-lg text-gray-200 mb-8">前方的礦道更加危險,速度會更快!</p>
|
| 138 |
+
<button id="start-next-stage-button" class="action-button bg-amber-500 hover:bg-amber-600 text-gray-900 font-bold py-4 px-8 rounded-lg text-2xl">
|
| 139 |
+
繼續採礦
|
| 140 |
+
</button>
|
| 141 |
+
</div>
|
| 142 |
+
|
| 143 |
+
<!-- 最終通關畫面 -->
|
| 144 |
+
<div id="final-win-screen" class="hidden flex-col text-center p-8 bg-black bg-opacity-60 rounded-lg my-auto">
|
| 145 |
+
<h1 class="text-4xl font-bold text-green-400 mb-4">恭喜你完成所有寶石挑戰!</h1>
|
| 146 |
+
<button id="unlock-secret-button" class="action-button bg-purple-600 hover:bg-purple-700 text-white font-bold py-4 px-8 rounded-lg text-2xl">
|
| 147 |
+
解鎖寶石洞窟的秘密
|
| 148 |
+
</button>
|
| 149 |
+
</div>
|
| 150 |
+
</div>
|
| 151 |
+
|
| 152 |
+
<!-- 秘密揭曉畫面 (獨立於 container 之外) -->
|
| 153 |
+
<div id="secret-screen" class="hidden absolute inset-0 w-screen h-screen secret-bg p-8 text-white flex-col items-center justify-center">
|
| 154 |
+
<div class="w-full max-w-2xl bg-black bg-opacity-70 p-8 rounded-xl">
|
| 155 |
+
<div id="secret-question">
|
| 156 |
+
<h2 class="text-3xl font-bold text-amber-300 mb-6">你認為這個洞窟的秘密是...</h2>
|
| 157 |
+
<div class="space-y-4 text-lg">
|
| 158 |
+
<button data-choice="A" class="secret-choice w-full text-left p-4 bg-white/10 hover:bg-white/20 rounded-lg transition-colors">A. 如何完美駕駛礦車,一次通關</button>
|
| 159 |
+
<button data-choice="B" class="secret-choice w-full text-left p-4 bg-white/10 hover:bg-white/20 rounded-lg transition-colors">B. 如何增加完美閃避的次數</button>
|
| 160 |
+
<button data-choice="C" class="secret-choice w-full text-left p-4 bg-white/10 hover:bg-white/20 rounded-lg transition-colors">C. 寶石分析術</button>
|
| 161 |
+
</div>
|
| 162 |
+
<p id="secret-feedback" class="mt-6 min-h-[28px] text-xl"></p>
|
| 163 |
+
</div>
|
| 164 |
+
<div id="secret-reveal" class="hidden text-left space-y-4">
|
| 165 |
+
<p class="text-lg">從最小的寶石到浩瀚的星辰,萬事萬物都由更基本的元素構成。你學會的『寶石分析術』,在數學的世界裡,它被稱為『因式分解』。</p>
|
| 166 |
+
<p class="text-2xl font-bold text-amber-300">這個洞窟的秘密就是:學會分析與拆解,就是看透事物本質的第一步。</p>
|
| 167 |
+
<p class="text-lg">無論是分析一顆寶石的成分、一道數學題的結構,還是未來你遇到的任何複雜問題,這種化繁為簡的智慧,才是你從這裡帶走、永不消失的真正寶藏。</p>
|
| 168 |
+
</div>
|
| 169 |
+
<div class="mt-12 text-center">
|
| 170 |
+
<a href="index.html" id="back-to-map-button" class="action-button inline-block bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-3 px-8 rounded-lg text-xl">
|
| 171 |
+
回到探險島地圖
|
| 172 |
+
</a>
|
| 173 |
+
</div>
|
| 174 |
+
</div>
|
| 175 |
+
</div>
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
<script>
|
| 179 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 180 |
+
// --- 畫面元素 ---
|
| 181 |
+
const container = document.getElementById('container');
|
| 182 |
+
const startScreen = document.getElementById('start-screen');
|
| 183 |
+
const gameScreen = document.getElementById('game-screen');
|
| 184 |
+
const puzzleScreen = document.getElementById('puzzle-screen');
|
| 185 |
+
const intermissionScreen = document.getElementById('intermission-screen');
|
| 186 |
+
const finalWinScreen = document.getElementById('final-win-screen');
|
| 187 |
+
const secretScreen = document.getElementById('secret-screen');
|
| 188 |
+
const startButton = document.getElementById('start-button');
|
| 189 |
+
const instructionsScreen = document.getElementById('instructions-screen');
|
| 190 |
+
const playGameButton = document.getElementById('play-game-button');
|
| 191 |
+
const gameOverScreen = document.getElementById('game-over-screen');
|
| 192 |
+
const winScreen = document.getElementById('win-screen');
|
| 193 |
+
const winPerfectDodges = document.getElementById('win-perfect-dodges');
|
| 194 |
+
const restartButton = document.getElementById('restart-button');
|
| 195 |
+
const continueButton = document.getElementById('continue-button');
|
| 196 |
+
const startNextStageButton = document.getElementById('start-next-stage-button');
|
| 197 |
+
const unlockSecretButton = document.getElementById('unlock-secret-button');
|
| 198 |
+
const canvas = document.getElementById('gameCanvas');
|
| 199 |
+
const canvasWrapper = document.getElementById('canvas-wrapper');
|
| 200 |
+
const moveLeftButton = document.getElementById('move-left-button');
|
| 201 |
+
const moveRightButton = document.getElementById('move-right-button');
|
| 202 |
+
const ctx = canvas.getContext('2d');
|
| 203 |
+
|
| 204 |
+
// --- 寶石解謎元素 ---
|
| 205 |
+
const largeGemImage = document.getElementById('large-gem-image');
|
| 206 |
+
const answerSlots = document.getElementById('answer-slots');
|
| 207 |
+
const smallGemOptions = document.getElementById('small-gem-options');
|
| 208 |
+
const puzzleFeedback = document.getElementById('puzzle-feedback');
|
| 209 |
+
const resetPuzzleButton = document.getElementById('reset-puzzle-button');
|
| 210 |
+
const checkPuzzleButton = document.getElementById('check-puzzle-button');
|
| 211 |
+
|
| 212 |
+
// --- 秘密揭曉元素 ---
|
| 213 |
+
const secretQuestion = document.getElementById('secret-question');
|
| 214 |
+
const secretReveal = document.getElementById('secret-reveal');
|
| 215 |
+
const secretFeedback = document.getElementById('secret-feedback');
|
| 216 |
+
const secretChoiceButtons = document.querySelectorAll('.secret-choice');
|
| 217 |
+
|
| 218 |
+
// --- 遊戲設定 ---
|
| 219 |
+
const LANE_COUNT = 3;
|
| 220 |
+
let laneWidth;
|
| 221 |
+
let gameInterval, timer, obstacleInterval;
|
| 222 |
+
let timeLeft;
|
| 223 |
+
let perfectDodges = 0;
|
| 224 |
+
const perfectDodgeTexts = [];
|
| 225 |
+
let keySequence = [];
|
| 226 |
+
const cheatCode = "kkkkk";
|
| 227 |
+
let currentStage = 0;
|
| 228 |
+
let currentStageSettings;
|
| 229 |
+
|
| 230 |
+
const gameStages = [
|
| 231 |
+
{ speed: 2.5, time: 20, spawnRate: 1800 },
|
| 232 |
+
{ speed: 3.5, time: 15, spawnRate: 1200 },
|
| 233 |
+
{ speed: 4.0, time: 12, spawnRate: 1000 }
|
| 234 |
+
];
|
| 235 |
+
|
| 236 |
+
// --- 寶石解謎設定 ---
|
| 237 |
+
const allSmallGems = [
|
| 238 |
+
{ id: 1, src: 'https://i.meee.com.tw/FeaxgIn.png' },
|
| 239 |
+
{ id: 2, src: 'https://i.meee.com.tw/VgnnNxo.png' },
|
| 240 |
+
{ id: 3, src: 'https://i.meee.com.tw/5vu7Qk1.png' },
|
| 241 |
+
{ id: 4, src: 'https://i.meee.com.tw/eoel84q.png' },
|
| 242 |
+
{ id: 5, src: 'https://i.meee.com.tw/b2uL22E.png' },
|
| 243 |
+
{ id: 6, src: 'https://i.meee.com.tw/HeduyDE.png' },
|
| 244 |
+
{ id: 7, src: 'https://i.meee.com.tw/2gorePd.png' },
|
| 245 |
+
{ id: 8, src: 'https://i.meee.com.tw/HNNEM09.png' },
|
| 246 |
+
{ id: 9, src: 'https://i.meee.com.tw/cpw8ccy.png' },
|
| 247 |
+
{ id: 10, src: 'https://i.meee.com.tw/Rk8hY7H.png' }
|
| 248 |
+
];
|
| 249 |
+
|
| 250 |
+
const puzzleLevels = [
|
| 251 |
+
{
|
| 252 |
+
largeGemSrc: 'https://i.meee.com.tw/PU1busV.png',
|
| 253 |
+
options: [1, 2, 3],
|
| 254 |
+
answer: { 1: 1, 2: 3, 3: 1 },
|
| 255 |
+
maxGems: 5
|
| 256 |
+
},
|
| 257 |
+
{
|
| 258 |
+
largeGemSrc: 'https://i.meee.com.tw/JCK7B2S.png',
|
| 259 |
+
options: [2, 3, 4, 5, 6, 7],
|
| 260 |
+
answer: { 3: 2, 7: 2, 2: 1, 4: 1, 5: 1, 6: 1 },
|
| 261 |
+
maxGems: 8
|
| 262 |
+
},
|
| 263 |
+
{
|
| 264 |
+
largeGemSrc: 'https://i.meee.com.tw/wFJquPy.png',
|
| 265 |
+
options: [2, 3, 4, 5, 6, 8, 9, 10],
|
| 266 |
+
answer: { 8: 3, 10: 3, 5: 6, 9: 1 },
|
| 267 |
+
maxGems: 13
|
| 268 |
+
}
|
| 269 |
+
];
|
| 270 |
+
let playerSelection = {};
|
| 271 |
+
|
| 272 |
+
// --- 玩家設定 ---
|
| 273 |
+
const player = {
|
| 274 |
+
x: 0, y: 0, width: 100, height: 100, lane: 1, image: new Image()
|
| 275 |
+
};
|
| 276 |
+
const normalCartSrc = 'https://i.meee.com.tw/w6krwUz.png';
|
| 277 |
+
const crashedCartSrc = 'https://i.meee.com.tw/4wSy5uX.png';
|
| 278 |
+
player.image.src = normalCartSrc;
|
| 279 |
+
player.image.onerror = () => { console.error("礦車圖片載入失敗!"); };
|
| 280 |
+
|
| 281 |
+
// --- 障礙物設定 ---
|
| 282 |
+
const obstacles = [];
|
| 283 |
+
const trackTies = [];
|
| 284 |
+
const obstacleImage = new Image();
|
| 285 |
+
obstacleImage.src = 'https://i.meee.com.tw/L0tV6le.png';
|
| 286 |
+
obstacleImage.onerror = () => { console.error("障礙物圖片載入失敗!"); };
|
| 287 |
+
let lastTieY = 0;
|
| 288 |
+
const TIE_SPACING = 50;
|
| 289 |
+
|
| 290 |
+
// --- 畫面管理 ---
|
| 291 |
+
function showScreen(screenId) {
|
| 292 |
+
['start-screen', 'game-screen', 'puzzle-screen', 'intermission-screen', 'final-win-screen'].forEach(id => {
|
| 293 |
+
const screen = document.getElementById(id);
|
| 294 |
+
if (id === screenId) {
|
| 295 |
+
screen.style.display = 'flex';
|
| 296 |
+
screen.classList.remove('hidden');
|
| 297 |
+
if(id === 'start-screen' || id === 'intermission-screen' || id === 'final-win-screen') {
|
| 298 |
+
screen.classList.add('my-auto');
|
| 299 |
+
} else {
|
| 300 |
+
screen.classList.remove('my-auto');
|
| 301 |
+
}
|
| 302 |
+
} else {
|
| 303 |
+
screen.style.display = 'none';
|
| 304 |
+
screen.classList.add('hidden');
|
| 305 |
+
}
|
| 306 |
+
});
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
// --- 礦車遊戲函式 ---
|
| 310 |
+
function resizeCanvas() {
|
| 311 |
+
const container = document.getElementById('container');
|
| 312 |
+
const controls = document.getElementById('controls');
|
| 313 |
+
canvas.width = container.clientWidth;
|
| 314 |
+
canvas.height = window.innerHeight * 0.9 - controls.offsetHeight - 20;
|
| 315 |
+
canvasWrapper.style.width = `${canvas.width}px`;
|
| 316 |
+
canvasWrapper.style.height = `${canvas.height}px`;
|
| 317 |
+
laneWidth = canvas.width / LANE_COUNT;
|
| 318 |
+
player.y = canvas.height - player.height - 10;
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
function getLaneCenterX(lane) {
|
| 322 |
+
return (lane * laneWidth) + (laneWidth / 2);
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
function spawnTrackTies() {
|
| 326 |
+
while (lastTieY < canvas.height + TIE_SPACING) {
|
| 327 |
+
trackTies.push({ y: lastTieY });
|
| 328 |
+
lastTieY += TIE_SPACING;
|
| 329 |
+
}
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
function drawTrack() {
|
| 333 |
+
ctx.fillStyle = '#57402c';
|
| 334 |
+
trackTies.forEach(tie => {
|
| 335 |
+
for (let i = 0; i < LANE_COUNT; i++) {
|
| 336 |
+
const laneX = getLaneCenterX(i);
|
| 337 |
+
const rail1X = laneX - laneWidth * 0.25;
|
| 338 |
+
const rail2X = laneX + laneWidth * 0.25;
|
| 339 |
+
ctx.fillRect(rail1X, tie.y, rail2X - rail1X, 10);
|
| 340 |
+
}
|
| 341 |
+
});
|
| 342 |
+
const railColor = '#858585', highlightColor = '#b0b0b0', railWidth = 8;
|
| 343 |
+
for (let i = 0; i < LANE_COUNT; i++) {
|
| 344 |
+
const laneX = getLaneCenterX(i);
|
| 345 |
+
const rail1X = laneX - laneWidth * 0.25;
|
| 346 |
+
const rail2X = laneX + laneWidth * 0.25;
|
| 347 |
+
ctx.fillStyle = railColor;
|
| 348 |
+
ctx.fillRect(rail1X - railWidth / 2, 0, railWidth, canvas.height);
|
| 349 |
+
ctx.fillRect(rail2X - railWidth / 2, 0, railWidth, canvas.height);
|
| 350 |
+
ctx.fillStyle = highlightColor;
|
| 351 |
+
ctx.fillRect(rail1X - railWidth / 2, 0, railWidth / 2, canvas.height);
|
| 352 |
+
ctx.fillRect(rail2X - railWidth / 2, 0, railWidth / 2, canvas.height);
|
| 353 |
+
}
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
function drawPlayer() {
|
| 357 |
+
player.x = getLaneCenterX(player.lane) - player.width / 2;
|
| 358 |
+
if (player.image.complete && player.image.naturalHeight !== 0) {
|
| 359 |
+
ctx.drawImage(player.image, player.x, player.y, player.width, player.height);
|
| 360 |
+
} else {
|
| 361 |
+
ctx.fillStyle = 'blue';
|
| 362 |
+
ctx.fillRect(player.x, player.y, player.width, player.height);
|
| 363 |
+
}
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
function drawObstacles() {
|
| 367 |
+
obstacles.forEach(obstacle => {
|
| 368 |
+
if (obstacleImage.complete && obstacleImage.naturalHeight !== 0) {
|
| 369 |
+
ctx.drawImage(obstacleImage, obstacle.x, obstacle.y, obstacle.width, obstacle.height);
|
| 370 |
+
} else {
|
| 371 |
+
ctx.fillStyle = '#a16207';
|
| 372 |
+
ctx.beginPath();
|
| 373 |
+
ctx.roundRect(obstacle.x, obstacle.y, obstacle.width, obstacle.height, [10]);
|
| 374 |
+
ctx.fill();
|
| 375 |
+
}
|
| 376 |
+
});
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
function drawUI() {
|
| 380 |
+
ctx.fillStyle = 'white';
|
| 381 |
+
ctx.font = 'bold 20px "Noto Sans TC"';
|
| 382 |
+
ctx.textAlign = 'center';
|
| 383 |
+
ctx.fillText(`倒數計時: ${timeLeft.toFixed(1)} 秒`, canvas.width / 2, 30);
|
| 384 |
+
ctx.textAlign = 'right';
|
| 385 |
+
ctx.fillText(`完美閃避: ${perfectDodges}`, canvas.width - 20, 30);
|
| 386 |
+
perfectDodgeTexts.forEach(text => {
|
| 387 |
+
ctx.save();
|
| 388 |
+
ctx.globalAlpha = text.alpha;
|
| 389 |
+
ctx.fillStyle = '#fde047';
|
| 390 |
+
ctx.font = 'bold 24px "Noto Sans TC"';
|
| 391 |
+
ctx.textAlign = 'center';
|
| 392 |
+
ctx.fillText(text.text, text.x, text.y);
|
| 393 |
+
ctx.restore();
|
| 394 |
+
});
|
| 395 |
+
}
|
| 396 |
+
|
| 397 |
+
function update() {
|
| 398 |
+
for (let i = trackTies.length - 1; i >= 0; i--) {
|
| 399 |
+
trackTies[i].y += currentStageSettings.speed;
|
| 400 |
+
if (trackTies[i].y > canvas.height) {
|
| 401 |
+
trackTies.splice(i, 1);
|
| 402 |
+
trackTies.unshift({ y: (trackTies[0]?.y || 0) - TIE_SPACING });
|
| 403 |
+
}
|
| 404 |
+
}
|
| 405 |
+
for (let i = obstacles.length - 1; i >= 0; i--) {
|
| 406 |
+
obstacles[i].y += currentStageSettings.speed;
|
| 407 |
+
if (obstacles[i].y > canvas.height) obstacles.splice(i, 1);
|
| 408 |
+
}
|
| 409 |
+
for (let i = perfectDodgeTexts.length - 1; i >= 0; i--) {
|
| 410 |
+
perfectDodgeTexts[i].y -= 1;
|
| 411 |
+
perfectDodgeTexts[i].alpha -= 0.02;
|
| 412 |
+
if (perfectDodgeTexts[i].alpha <= 0) perfectDodgeTexts.splice(i, 1);
|
| 413 |
+
}
|
| 414 |
+
obstacles.forEach(obstacle => {
|
| 415 |
+
if (player.lane === obstacle.lane && player.y < obstacle.y + obstacle.height && player.y + player.height > obstacle.y) {
|
| 416 |
+
gameOver();
|
| 417 |
+
}
|
| 418 |
+
});
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
function draw() {
|
| 422 |
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
| 423 |
+
drawTrack();
|
| 424 |
+
drawObstacles();
|
| 425 |
+
drawPlayer();
|
| 426 |
+
drawUI();
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
function spawnObstacle() {
|
| 430 |
+
const lane = Math.floor(Math.random() * LANE_COUNT);
|
| 431 |
+
const size = laneWidth * 0.5;
|
| 432 |
+
const x = getLaneCenterX(lane) - size / 2;
|
| 433 |
+
obstacles.push({ x, y: -size, width: size, height: size, lane, dodged: false });
|
| 434 |
+
}
|
| 435 |
+
|
| 436 |
+
function gameLoop() {
|
| 437 |
+
update();
|
| 438 |
+
draw();
|
| 439 |
+
gameInterval = requestAnimationFrame(gameLoop);
|
| 440 |
+
}
|
| 441 |
+
|
| 442 |
+
function stopGame() {
|
| 443 |
+
if (gameInterval) cancelAnimationFrame(gameInterval);
|
| 444 |
+
if (timer) clearInterval(timer);
|
| 445 |
+
if (obstacleInterval) clearInterval(obstacleInterval);
|
| 446 |
+
gameInterval = timer = obstacleInterval = null;
|
| 447 |
+
}
|
| 448 |
+
|
| 449 |
+
function startGame(stageIndex) {
|
| 450 |
+
currentStage = stageIndex;
|
| 451 |
+
currentStageSettings = gameStages[stageIndex];
|
| 452 |
+
player.image.src = normalCartSrc; // Reset to normal cart image
|
| 453 |
+
stopGame();
|
| 454 |
+
instructionsScreen.classList.add('hidden');
|
| 455 |
+
gameOverScreen.classList.add('hidden');
|
| 456 |
+
winScreen.classList.add('hidden');
|
| 457 |
+
resizeCanvas();
|
| 458 |
+
obstacles.length = 0;
|
| 459 |
+
trackTies.length = 0;
|
| 460 |
+
perfectDodgeTexts.length = 0;
|
| 461 |
+
perfectDodges = 0;
|
| 462 |
+
lastTieY = 0;
|
| 463 |
+
spawnTrackTies();
|
| 464 |
+
player.lane = 1;
|
| 465 |
+
timeLeft = currentStageSettings.time;
|
| 466 |
+
timer = setInterval(() => {
|
| 467 |
+
timeLeft -= 0.1;
|
| 468 |
+
if (timeLeft <= 0) {
|
| 469 |
+
timeLeft = 0;
|
| 470 |
+
winGame();
|
| 471 |
+
}
|
| 472 |
+
}, 100);
|
| 473 |
+
obstacleInterval = setInterval(spawnObstacle, currentStageSettings.spawnRate);
|
| 474 |
+
gameLoop();
|
| 475 |
+
}
|
| 476 |
+
|
| 477 |
+
function gameOver() {
|
| 478 |
+
stopGame();
|
| 479 |
+
player.image.src = crashedCartSrc; // Change to crashed cart image
|
| 480 |
+
draw(); // Redraw canvas one last time with the new image
|
| 481 |
+
gameOverScreen.classList.remove('hidden');
|
| 482 |
+
}
|
| 483 |
+
|
| 484 |
+
function winGame() {
|
| 485 |
+
stopGame();
|
| 486 |
+
winPerfectDodges.textContent = `完美閃避: ${perfectDodges} 次`;
|
| 487 |
+
winScreen.classList.remove('hidden');
|
| 488 |
+
}
|
| 489 |
+
|
| 490 |
+
function handleMove(direction) {
|
| 491 |
+
if (!gameInterval) return;
|
| 492 |
+
const oldLane = player.lane;
|
| 493 |
+
let newLane = player.lane;
|
| 494 |
+
if (direction === 'left') newLane = Math.max(0, player.lane - 1);
|
| 495 |
+
else if (direction === 'right') newLane = Math.min(LANE_COUNT - 1, player.lane + 1);
|
| 496 |
+
if (oldLane !== newLane) {
|
| 497 |
+
player.lane = newLane;
|
| 498 |
+
checkPerfectDodge(oldLane);
|
| 499 |
+
}
|
| 500 |
+
}
|
| 501 |
+
|
| 502 |
+
function checkPerfectDodge(fromLane) {
|
| 503 |
+
const perfectDodgeZoneTop = player.y - player.height / 2;
|
| 504 |
+
const perfectDodgeZoneBottom = player.y + player.height;
|
| 505 |
+
obstacles.forEach(obstacle => {
|
| 506 |
+
if (obstacle.lane === fromLane && !obstacle.dodged) {
|
| 507 |
+
if (obstacle.y + obstacle.height > perfectDodgeZoneTop && obstacle.y < perfectDodgeZoneBottom) {
|
| 508 |
+
perfectDodges++;
|
| 509 |
+
obstacle.dodged = true;
|
| 510 |
+
perfectDodgeTexts.push({ text: '完美!', x: getLaneCenterX(fromLane), y: player.y, alpha: 1 });
|
| 511 |
+
}
|
| 512 |
+
}
|
| 513 |
+
});
|
| 514 |
+
}
|
| 515 |
+
|
| 516 |
+
// --- 寶石解謎函式 ---
|
| 517 |
+
function showPuzzleScreen() {
|
| 518 |
+
showScreen('puzzle-screen');
|
| 519 |
+
loadPuzzleLevel(currentStage);
|
| 520 |
+
}
|
| 521 |
+
|
| 522 |
+
function loadPuzzleLevel(levelIndex) {
|
| 523 |
+
const level = puzzleLevels[levelIndex];
|
| 524 |
+
largeGemImage.src = level.largeGemSrc;
|
| 525 |
+
smallGemOptions.innerHTML = '';
|
| 526 |
+
level.options.forEach(gemId => {
|
| 527 |
+
const gemData = allSmallGems.find(g => g.id === gemId);
|
| 528 |
+
if (gemData) {
|
| 529 |
+
const gemBtn = document.createElement('button');
|
| 530 |
+
gemBtn.className = 'gem-button';
|
| 531 |
+
gemBtn.innerHTML = `<img src="${gemData.src}" alt="小寶石 ${gemData.id}" class="h-16 w-16 object-contain">`;
|
| 532 |
+
gemBtn.onclick = () => addGemToSelection(gemData);
|
| 533 |
+
smallGemOptions.appendChild(gemBtn);
|
| 534 |
+
}
|
| 535 |
+
});
|
| 536 |
+
resetPuzzle();
|
| 537 |
+
}
|
| 538 |
+
|
| 539 |
+
function addGemToSelection(gem) {
|
| 540 |
+
const currentTotal = Object.values(playerSelection).reduce((sum, count) => sum + count, 0);
|
| 541 |
+
const level = puzzleLevels[currentStage];
|
| 542 |
+
|
| 543 |
+
if (currentTotal >= level.maxGems) {
|
| 544 |
+
puzzleFeedback.textContent = '數量太多了喔!';
|
| 545 |
+
puzzleFeedback.className = 'text-center text-xl font-bold min-h-[32px] mb-4 text-yellow-400';
|
| 546 |
+
setTimeout(() => {
|
| 547 |
+
if(puzzleFeedback.textContent === '數量太多了喔!') {
|
| 548 |
+
puzzleFeedback.textContent = '';
|
| 549 |
+
}
|
| 550 |
+
}, 2000);
|
| 551 |
+
return;
|
| 552 |
+
}
|
| 553 |
+
|
| 554 |
+
playerSelection[gem.id] = (playerSelection[gem.id] || 0) + 1;
|
| 555 |
+
renderSelection();
|
| 556 |
+
}
|
| 557 |
+
|
| 558 |
+
function removeGemFromSelection(gemId) {
|
| 559 |
+
if (playerSelection[gemId]) {
|
| 560 |
+
playerSelection[gemId]--;
|
| 561 |
+
if (playerSelection[gemId] === 0) {
|
| 562 |
+
delete playerSelection[gemId];
|
| 563 |
+
}
|
| 564 |
+
}
|
| 565 |
+
renderSelection();
|
| 566 |
+
}
|
| 567 |
+
|
| 568 |
+
function renderSelection() {
|
| 569 |
+
answerSlots.innerHTML = '';
|
| 570 |
+
puzzleLevels[currentStage].options.forEach(gemId => {
|
| 571 |
+
const gemData = allSmallGems.find(g => g.id === gemId);
|
| 572 |
+
if (playerSelection[gemId]) {
|
| 573 |
+
const count = playerSelection[gemId];
|
| 574 |
+
for (let i = 0; i < count; i++) {
|
| 575 |
+
const img = document.createElement('img');
|
| 576 |
+
img.src = gemData.src;
|
| 577 |
+
img.alt = `[已選擇的小寶石 ${gemData.id}]`;
|
| 578 |
+
img.className = 'h-12 w-12 object-contain cursor-pointer';
|
| 579 |
+
img.onclick = () => removeGemFromSelection(gemId);
|
| 580 |
+
answerSlots.appendChild(img);
|
| 581 |
+
}
|
| 582 |
+
}
|
| 583 |
+
});
|
| 584 |
+
}
|
| 585 |
+
|
| 586 |
+
function resetPuzzle() {
|
| 587 |
+
playerSelection = {};
|
| 588 |
+
answerSlots.innerHTML = '';
|
| 589 |
+
puzzleFeedback.textContent = '';
|
| 590 |
+
}
|
| 591 |
+
|
| 592 |
+
function checkPuzzleAnswer() {
|
| 593 |
+
const level = puzzleLevels[currentStage];
|
| 594 |
+
if (!level) return;
|
| 595 |
+
const answer = level.answer;
|
| 596 |
+
let correct = true;
|
| 597 |
+
|
| 598 |
+
if (Object.keys(playerSelection).length !== Object.keys(answer).length) {
|
| 599 |
+
correct = false;
|
| 600 |
+
} else {
|
| 601 |
+
for (const gemId in answer) {
|
| 602 |
+
if (playerSelection[gemId] !== answer[gemId]) {
|
| 603 |
+
correct = false;
|
| 604 |
+
break;
|
| 605 |
+
}
|
| 606 |
+
}
|
| 607 |
+
}
|
| 608 |
+
|
| 609 |
+
if (correct) {
|
| 610 |
+
puzzleFeedback.textContent = '組合正確!你獲得了大寶石!';
|
| 611 |
+
puzzleFeedback.className = 'text-center text-xl font-bold min-h-[32px] mb-4 text-green-400';
|
| 612 |
+
setTimeout(() => {
|
| 613 |
+
const nextStage = currentStage + 1;
|
| 614 |
+
if (nextStage < puzzleLevels.length) {
|
| 615 |
+
showScreen('intermission-screen');
|
| 616 |
+
} else {
|
| 617 |
+
showScreen('final-win-screen');
|
| 618 |
+
}
|
| 619 |
+
}, 1500);
|
| 620 |
+
} else {
|
| 621 |
+
puzzleFeedback.textContent = '組合不對喔,再試一次!';
|
| 622 |
+
puzzleFeedback.className = 'text-center text-xl font-bold min-h-[32px] mb-4 text-red-500';
|
| 623 |
+
}
|
| 624 |
+
}
|
| 625 |
+
|
| 626 |
+
// --- 事件監聽 ---
|
| 627 |
+
startButton.addEventListener('click', () => {
|
| 628 |
+
showScreen('game-screen');
|
| 629 |
+
resizeCanvas();
|
| 630 |
+
instructionsScreen.classList.remove('hidden');
|
| 631 |
+
});
|
| 632 |
+
|
| 633 |
+
playGameButton.addEventListener('click', () => startGame(0));
|
| 634 |
+
restartButton.addEventListener('click', () => startGame(currentStage));
|
| 635 |
+
continueButton.addEventListener('click', showPuzzleScreen);
|
| 636 |
+
startNextStageButton.addEventListener('click', () => {
|
| 637 |
+
showScreen('game-screen');
|
| 638 |
+
startGame(currentStage + 1);
|
| 639 |
+
});
|
| 640 |
+
resetPuzzleButton.addEventListener('click', resetPuzzle);
|
| 641 |
+
checkPuzzleButton.addEventListener('click', checkPuzzleAnswer);
|
| 642 |
+
unlockSecretButton.addEventListener('click', () => {
|
| 643 |
+
container.style.display = 'none';
|
| 644 |
+
secretScreen.style.display = 'flex';
|
| 645 |
+
});
|
| 646 |
+
|
| 647 |
+
secretChoiceButtons.forEach(button => {
|
| 648 |
+
button.addEventListener('click', () => {
|
| 649 |
+
const choice = button.dataset.choice;
|
| 650 |
+
secretFeedback.classList.remove('text-yellow-400', 'text-green-400');
|
| 651 |
+
if (choice === 'C') {
|
| 652 |
+
secretFeedback.textContent = '';
|
| 653 |
+
secretQuestion.style.display = 'none';
|
| 654 |
+
secretReveal.style.display = 'block';
|
| 655 |
+
} else if (choice === 'A') {
|
| 656 |
+
secretFeedback.textContent = '駕駛技術固然重要,但那只是過程喔!';
|
| 657 |
+
secretFeedback.classList.add('text-yellow-400');
|
| 658 |
+
} else {
|
| 659 |
+
secretFeedback.textContent = '閃避只是手段,不是目的呀!';
|
| 660 |
+
secretFeedback.classList.add('text-yellow-400');
|
| 661 |
+
}
|
| 662 |
+
});
|
| 663 |
+
});
|
| 664 |
+
|
| 665 |
+
window.addEventListener('resize', resizeCanvas);
|
| 666 |
+
|
| 667 |
+
window.addEventListener('keydown', (e) => {
|
| 668 |
+
if (e.key === 'ArrowLeft') handleMove('left');
|
| 669 |
+
else if (e.key === 'ArrowRight') handleMove('right');
|
| 670 |
+
|
| 671 |
+
if (gameInterval) {
|
| 672 |
+
keySequence.push(e.key);
|
| 673 |
+
keySequence = keySequence.slice(-cheatCode.length);
|
| 674 |
+
if (keySequence.join('') === cheatCode) {
|
| 675 |
+
winGame();
|
| 676 |
+
}
|
| 677 |
+
}
|
| 678 |
+
});
|
| 679 |
+
|
| 680 |
+
canvas.addEventListener('click', (e) => {
|
| 681 |
+
const rect = canvas.getBoundingClientRect();
|
| 682 |
+
const clickX = e.clientX - rect.left;
|
| 683 |
+
if (clickX < canvas.width / 2) handleMove('left');
|
| 684 |
+
else handleMove('right');
|
| 685 |
+
});
|
| 686 |
+
moveLeftButton.addEventListener('click', () => handleMove('left'));
|
| 687 |
+
moveRightButton.addEventListener('click', () => handleMove('right'));
|
| 688 |
+
|
| 689 |
+
// --- 初始啟動 ---
|
| 690 |
+
showScreen('start-screen');
|
| 691 |
+
});
|
| 692 |
+
</script>
|
| 693 |
+
</body>
|
| 694 |
+
</html>
|
example/harbor.html
ADDED
|
@@ -0,0 +1,697 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="zh-Hant">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>數學探險島 - 海風港灣</title>
|
| 7 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 8 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 9 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 10 |
+
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@400;500;700&display=swap" rel="stylesheet">
|
| 11 |
+
<!-- MathJax for rendering formulas -->
|
| 12 |
+
<script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script>
|
| 13 |
+
<script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
|
| 14 |
+
<style>
|
| 15 |
+
body {
|
| 16 |
+
font-family: 'Noto Sans TC', sans-serif;
|
| 17 |
+
overflow: hidden;
|
| 18 |
+
background-image: url('https://i.meee.com.tw/TDC4EZE.png');
|
| 19 |
+
background-size: cover;
|
| 20 |
+
background-position: center;
|
| 21 |
+
}
|
| 22 |
+
#game-container-bg {
|
| 23 |
+
background-image: url('https://i.meee.com.tw/vT2QeZF.png');
|
| 24 |
+
background-size: cover;
|
| 25 |
+
background-position: center;
|
| 26 |
+
}
|
| 27 |
+
.seaweed {
|
| 28 |
+
position: absolute;
|
| 29 |
+
bottom: -10px;
|
| 30 |
+
pointer-events: none;
|
| 31 |
+
filter: brightness(0.9);
|
| 32 |
+
}
|
| 33 |
+
.fish {
|
| 34 |
+
position: absolute;
|
| 35 |
+
cursor: pointer;
|
| 36 |
+
transition: transform 0.2s ease, opacity 0.15s ease;
|
| 37 |
+
}
|
| 38 |
+
.fish:hover {
|
| 39 |
+
transform: scale(1.1);
|
| 40 |
+
}
|
| 41 |
+
.catch-feedback {
|
| 42 |
+
position: absolute;
|
| 43 |
+
font-size: 2rem;
|
| 44 |
+
font-weight: bold;
|
| 45 |
+
pointer-events: none;
|
| 46 |
+
animation: fadeUp 1s forwards;
|
| 47 |
+
text-shadow: 1px 1px 2px black;
|
| 48 |
+
}
|
| 49 |
+
@keyframes fadeUp {
|
| 50 |
+
from { opacity: 1; transform: translateY(0); }
|
| 51 |
+
to { opacity: 0; transform: translateY(-50px); }
|
| 52 |
+
}
|
| 53 |
+
.loader {
|
| 54 |
+
border: 8px solid #f3f3f3;
|
| 55 |
+
border-radius: 50%;
|
| 56 |
+
border-top: 8px solid #3498db;
|
| 57 |
+
width: 60px;
|
| 58 |
+
height: 60px;
|
| 59 |
+
animation: spin 1s linear infinite;
|
| 60 |
+
}
|
| 61 |
+
@keyframes spin {
|
| 62 |
+
0% { transform: rotate(0deg); }
|
| 63 |
+
100% { transform: rotate(360deg); }
|
| 64 |
+
}
|
| 65 |
+
.market-choice.selected {
|
| 66 |
+
border-color: #facc15; /* yellow-400 */
|
| 67 |
+
box-shadow: 0 0 15px #facc15;
|
| 68 |
+
}
|
| 69 |
+
.secret-bg {
|
| 70 |
+
background-image: url('https://i.meee.com.tw/EKZpYKI.png');
|
| 71 |
+
background-size: cover;
|
| 72 |
+
background-position: center;
|
| 73 |
+
}
|
| 74 |
+
</style>
|
| 75 |
+
</head>
|
| 76 |
+
<body class="flex items-center justify-center h-screen">
|
| 77 |
+
|
| 78 |
+
<div id="container" class="w-full max-w-4xl mx-auto text-white p-4 flex flex-col h-[90vh] max-h-[800px] relative">
|
| 79 |
+
|
| 80 |
+
<!-- 引導畫面 -->
|
| 81 |
+
<div id="start-screen" class="flex flex-col items-center justify-center text-center p-8 bg-black bg-opacity-70 rounded-lg my-auto">
|
| 82 |
+
<h1 class="text-5xl font-bold text-cyan-300 mb-4" style="text-shadow: 2px 2px 4px #000;">海風港灣</h1>
|
| 83 |
+
<p class="text-xl text-gray-200 mb-8 max-w-2xl">歡迎來到海風港灣!這次的任務是捕捉指定的魚種,並到市場賣出。有了捕捉的技術,真正要賺大錢還得靠數學呢!請你在捕捉完漁獲後,根據市場需求,將漁獲賣到最適合的市場!</p>
|
| 84 |
+
<button id="start-button" class="bg-blue-500 hover:bg-blue-600 text-white font-bold py-4 px-8 rounded-lg text-2xl">
|
| 85 |
+
開始抓魚
|
| 86 |
+
</button>
|
| 87 |
+
</div>
|
| 88 |
+
|
| 89 |
+
<!-- 載入畫面 -->
|
| 90 |
+
<div id="loading-screen" class="hidden flex-col items-center justify-center text-center p-8 bg-black bg-opacity-70 rounded-lg my-auto">
|
| 91 |
+
<div class="loader mb-6"></div>
|
| 92 |
+
<p id="loading-text" class="text-2xl text-gray-200">正在準備海底世界...</p>
|
| 93 |
+
</div>
|
| 94 |
+
|
| 95 |
+
<!-- 遊戲畫面 -->
|
| 96 |
+
<div id="game-screen" class="hidden flex-col h-full w-full">
|
| 97 |
+
<div class="flex justify-between items-center p-2 bg-black/50 rounded-t-lg">
|
| 98 |
+
<div>
|
| 99 |
+
<p class="text-lg">目標: <span id="goal-text" class="font-bold text-yellow-400"></span></p>
|
| 100 |
+
<p class="text-lg">已捕捉: <span id="score" class="font-bold text-xl">0</span> / <span id="goal-count"></span></p>
|
| 101 |
+
</div>
|
| 102 |
+
<p class="text-sm text-cyan-200">請適度捕撈,維持海洋永續</p>
|
| 103 |
+
<div>
|
| 104 |
+
<p class="text-lg">時間</p>
|
| 105 |
+
<p id="timer" class="font-bold text-3xl">20</p>
|
| 106 |
+
</div>
|
| 107 |
+
</div>
|
| 108 |
+
<div id="game-container-bg" class="relative flex-grow rounded-b-lg overflow-hidden border-4 border-black/50">
|
| 109 |
+
<div id="game-container" class="absolute inset-0">
|
| 110 |
+
<img src="https://i.meee.com.tw/XE9mkQQ.gif" class="seaweed" style="left: 0%; height: 45%; transform: scaleX(-1);" alt="[海草的Image]">
|
| 111 |
+
<img src="https://i.meee.com.tw/cTBZUgx.gif" class="seaweed" style="left: 20%; height: 35%;" alt="[海草的Image]">
|
| 112 |
+
<img src="https://i.meee.com.tw/KyN2GaF.gif" class="seaweed" style="left: 45%; height: 30%;" alt="[海草的Image]">
|
| 113 |
+
<img src="https://i.meee.com.tw/XE9mkQQ.gif" class="seaweed" style="right: 25%; height: 48%;" alt="[海草的Image]">
|
| 114 |
+
<img src="https://i.meee.com.tw/cTBZUgx.gif" class="seaweed" style="right: 10%; height: 55%; transform: scaleX(-1);" alt="[海草的Image]">
|
| 115 |
+
<img src="https://i.meee.com.tw/KyN2GaF.gif" class="seaweed" style="right: -2%; height: 38%;" alt="[海草的Image]">
|
| 116 |
+
</div>
|
| 117 |
+
</div>
|
| 118 |
+
</div>
|
| 119 |
+
|
| 120 |
+
<!-- 疊加畫面 (操作說明, 成功/失敗) -->
|
| 121 |
+
<div id="overlay-container" class="hidden absolute inset-0 flex-col items-center justify-center z-10">
|
| 122 |
+
<div id="instructions-screen" class="hidden bg-black bg-opacity-80 w-full h-full flex-col items-center justify-center text-center p-4">
|
| 123 |
+
<div id="instructions-content" class="bg-slate-800 p-8 rounded-lg">
|
| 124 |
+
<!-- JS will fill this -->
|
| 125 |
+
</div>
|
| 126 |
+
</div>
|
| 127 |
+
<div id="game-over-screen" class="hidden bg-black bg-opacity-70 w-full h-full flex-col items-center justify-center text-center p-4">
|
| 128 |
+
<h2 class="text-4xl font-bold text-red-500 mb-4">挑戰失敗!</h2>
|
| 129 |
+
<button id="restart-button" class="bg-amber-500 hover:bg-amber-600 text-gray-900 font-bold py-3 px-6 rounded-lg text-xl">
|
| 130 |
+
重新挑戰
|
| 131 |
+
</button>
|
| 132 |
+
</div>
|
| 133 |
+
<div id="win-screen" class="hidden bg-black bg-opacity-70 w-full h-full flex-col items-center justify-center text-center p-4">
|
| 134 |
+
<h2 class="text-4xl font-bold text-green-400 mb-4">成功!</h2>
|
| 135 |
+
<p class="text-xl mb-6">你抓到足夠的漁獲了!</p>
|
| 136 |
+
<button id="continue-button" class="bg-green-500 hover:bg-green-600 text-white font-bold py-3 px-6 rounded-lg text-xl">
|
| 137 |
+
繼續前進
|
| 138 |
+
</button>
|
| 139 |
+
</div>
|
| 140 |
+
</div>
|
| 141 |
+
|
| 142 |
+
<!-- 市場選擇畫面 -->
|
| 143 |
+
<div id="market-screen" class="hidden flex-col h-full w-full p-6 bg-black bg-opacity-70 rounded-lg">
|
| 144 |
+
<div id="market-instructions" class="text-center my-auto">
|
| 145 |
+
<h2 class="text-3xl font-bold text-amber-300 mb-6">任務說明:選擇市場</h2>
|
| 146 |
+
<p class="text-lg">每個地點對<span class="market-fish-name text-yellow-300 font-bold"></span>的需求都不同,需求<span class="text-yellow-300 font-bold">比例</span>越高的市場,就能賣到越好的價格!</p>
|
| 147 |
+
<p class="text-lg mt-2">仔細觀察下方的統計圖,找出最賺錢的市場吧!</p>
|
| 148 |
+
<button id="show-market-choices-button" class="bg-blue-500 hover:bg-blue-600 text-white font-bold py-3 px-8 rounded-lg text-xl mt-8">
|
| 149 |
+
了解,前往選擇
|
| 150 |
+
</button>
|
| 151 |
+
</div>
|
| 152 |
+
<div id="market-choices" class="hidden flex-col">
|
| 153 |
+
<h2 class="text-3xl font-bold text-amber-300 text-center mb-4">你要把<span class="market-fish-name"></span>賣到哪裡?</h2>
|
| 154 |
+
<div id="market-images-container" class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
| 155 |
+
<!-- Market images will be inserted here by JS -->
|
| 156 |
+
</div>
|
| 157 |
+
<p id="market-feedback" class="text-center text-xl font-bold min-h-[32px] mt-4"></p>
|
| 158 |
+
<button id="next-level-button" class="hidden bg-blue-500 hover:bg-blue-600 text-white font-bold py-3 px-6 rounded-lg text-xl w-full max-w-xs mx-auto mt-4">
|
| 159 |
+
<!-- Button text will be set by JS -->
|
| 160 |
+
</button>
|
| 161 |
+
</div>
|
| 162 |
+
</div>
|
| 163 |
+
|
| 164 |
+
<!-- 攻略畫面 -->
|
| 165 |
+
<div id="strategy-screen" class="hidden flex-col h-full w-full p-6 bg-black bg-opacity-80 rounded-lg text-left overflow-y-auto">
|
| 166 |
+
<h1 class="text-3xl font-bold text-amber-300 text-center mb-4">經營之神的攻略</h1>
|
| 167 |
+
<p class="text-lg mb-4">在真實世界中,不同地點的調查所收到的資料不一定會一樣多,總人數不一樣的時候,我們很難用肉眼就判斷出比例的高低,因此需要「相對次數」這樣的統計數據。</p>
|
| 168 |
+
<div class="text-center bg-gray-900 p-3 rounded-lg text-xl mb-4">
|
| 169 |
+
$$\text{相對次數} = \frac{\text{需求(人)}}{\text{總人數}} \times 100\%$$
|
| 170 |
+
</div>
|
| 171 |
+
<img src="https://i.meee.com.tw/i67LLqV.png" class="rounded-lg mx-auto my-4 w-full max-w-md" alt="[包含相對次數的統計圖表]">
|
| 172 |
+
<p class="text-lg mb-6 text-center">有了相對次數,會不會更好判斷呢?用下一關的挑戰來試試吧!</p>
|
| 173 |
+
<button id="start-final-stage-button" class="bg-green-500 hover:bg-green-600 text-white font-bold py-3 px-8 rounded-lg text-xl w-full max-w-xs mx-auto mt-4">
|
| 174 |
+
挑戰最終關卡
|
| 175 |
+
</button>
|
| 176 |
+
</div>
|
| 177 |
+
|
| 178 |
+
<!-- 最終秘密畫面 -->
|
| 179 |
+
<div id="final-secret-screen" class="hidden flex-col h-full w-full p-6 bg-black bg-opacity-80 rounded-lg text-left overflow-y-auto">
|
| 180 |
+
<h1 class="text-3xl font-bold text-amber-300 text-center mb-6">海風港灣的秘密</h1>
|
| 181 |
+
|
| 182 |
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
| 183 |
+
<img src="https://i.meee.com.tw/rmcY2Vc.png" class="rounded-lg border-4 border-amber-400" alt="[代數之丘的統計圖表]">
|
| 184 |
+
<img src="https://i.meee.com.tw/qV8QrUI.png" class="rounded-lg" alt="[哲學之塔的統計圖表]">
|
| 185 |
+
<img src="https://i.meee.com.tw/i67LLqV.png" class="rounded-lg" alt="[寶石洞窟的統計圖表]">
|
| 186 |
+
</div>
|
| 187 |
+
|
| 188 |
+
<p class="text-lg mb-4">恭喜你,完成了所有挑戰!你學會了捕魚,也學會了如何分析數據來做出最好的商業決策。</p>
|
| 189 |
+
<p class="text-2xl font-bold text-cyan-300 mb-4">這個港灣的秘密就是:<br>「數字」本身有時候會騙人,「比例」才能揭露真相。</p>
|
| 190 |
+
<p class="text-lg mb-4">單看數字,哲學之塔似乎是更好的市場。但當你把「總人數」也考慮進來,計算出「相對次數」(也就是需求比例),你才會發現代數之丘的需求比例,是超過哲學之塔的。</p>
|
| 191 |
+
<p class="text-lg">這個智慧,不只適用於賣魚。未來在你看新聞、分析報告,甚至做人生重大決定時,記得問自己:「這個數字背後的『分母』是什麼?」看透比例,你就能做出更聰明的選擇。</p>
|
| 192 |
+
<a href="index.html" id="back-to-map-button" class="inline-block text-center bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-3 px-8 rounded-lg text-xl w-full max-w-xs mx-auto mt-8">
|
| 193 |
+
回到探險島地圖
|
| 194 |
+
</a>
|
| 195 |
+
</div>
|
| 196 |
+
</div>
|
| 197 |
+
|
| 198 |
+
<!-- Image Zoom Modal -->
|
| 199 |
+
<div id="zoom-modal" class="hidden fixed inset-0 bg-black/80 z-50 flex items-center justify-center p-4 cursor-pointer">
|
| 200 |
+
<img id="zoomed-image" src="" alt="放大的統計圖表" class="max-w-full max-h-full rounded-lg shadow-2xl">
|
| 201 |
+
</div>
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
<script>
|
| 205 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 206 |
+
// --- 畫面元素 ---
|
| 207 |
+
const startScreen = document.getElementById('start-screen');
|
| 208 |
+
const loadingScreen = document.getElementById('loading-screen');
|
| 209 |
+
const loadingText = document.getElementById('loading-text');
|
| 210 |
+
const gameScreen = document.getElementById('game-screen');
|
| 211 |
+
const startButton = document.getElementById('start-button');
|
| 212 |
+
const gameContainer = document.getElementById('game-container');
|
| 213 |
+
const scoreDisplay = document.getElementById('score');
|
| 214 |
+
const timerDisplay = document.getElementById('timer');
|
| 215 |
+
const instructionsScreen = document.getElementById('instructions-screen');
|
| 216 |
+
const instructionsContent = document.getElementById('instructions-content');
|
| 217 |
+
const goalText = document.getElementById('goal-text');
|
| 218 |
+
const goalCount = document.getElementById('goal-count');
|
| 219 |
+
const overlayContainer = document.getElementById('overlay-container');
|
| 220 |
+
const gameOverScreen = document.getElementById('game-over-screen');
|
| 221 |
+
const winScreen = document.getElementById('win-screen');
|
| 222 |
+
const restartButton = document.getElementById('restart-button');
|
| 223 |
+
const continueButton = document.getElementById('continue-button');
|
| 224 |
+
const marketScreen = document.getElementById('market-screen');
|
| 225 |
+
const marketInstructions = document.getElementById('market-instructions');
|
| 226 |
+
const marketChoices = document.getElementById('market-choices');
|
| 227 |
+
const showMarketChoicesButton = document.getElementById('show-market-choices-button');
|
| 228 |
+
const marketImagesContainer = document.getElementById('market-images-container');
|
| 229 |
+
const marketFeedback = document.getElementById('market-feedback');
|
| 230 |
+
const nextLevelButton = document.getElementById('next-level-button');
|
| 231 |
+
const startFinalStageButton = document.getElementById('start-final-stage-button');
|
| 232 |
+
const finalSecretScreen = document.getElementById('final-secret-screen');
|
| 233 |
+
const zoomModal = document.getElementById('zoom-modal');
|
| 234 |
+
const zoomedImage = document.getElementById('zoomed-image');
|
| 235 |
+
|
| 236 |
+
// --- 遊戲設定 ---
|
| 237 |
+
const MAX_FISH_ON_SCREEN = 8;
|
| 238 |
+
let score = 0, timeLeft = 0;
|
| 239 |
+
let gameInterval, timerInterval, fishSpawnInterval;
|
| 240 |
+
let currentLevel = 0;
|
| 241 |
+
let currentLevelSettings;
|
| 242 |
+
|
| 243 |
+
const levels = [
|
| 244 |
+
{ goal: 10, time: 20, targetFish: '小丑魚', speedMultiplier: 1.0, spawnRate: 1200, fishSet: ['小丑魚', '螃蟹'] },
|
| 245 |
+
{ goal: 10, time: 20, targetFish: '水母', speedMultiplier: 1.5, spawnRate: 1000, fishSet: ['水母', '章魚'] },
|
| 246 |
+
{ goal: 10, time: 20, targetFish: '水母', speedMultiplier: 1.8, spawnRate: 800, fishSet: ['水母', '鯊魚'] }
|
| 247 |
+
];
|
| 248 |
+
|
| 249 |
+
const fishTypes = [
|
| 250 |
+
{ name: '小丑魚', src: 'https://i.meee.com.tw/7tfldTT.gif', width: 160, height: 120 },
|
| 251 |
+
{ name: '鯊魚', src: 'https://i.meee.com.tw/KCQk7C6.gif', width: 270, height: 270 },
|
| 252 |
+
{ name: '章魚', src: 'https://i.meee.com.tw/lBWJmMy.gif', width: 160, height: 130 },
|
| 253 |
+
{ name: '海馬', src: 'https://i.meee.com.tw/sMehyty.gif', width: 150, height: 150 },
|
| 254 |
+
{ name: '螃蟹', src: 'https://i.meee.com.tw/zx9Vg0B.gif', width: 150, height: 110 },
|
| 255 |
+
{ name: '水母', src: 'https://i.meee.com.tw/81gncdG.gif', width: 160, height: 160 },
|
| 256 |
+
];
|
| 257 |
+
|
| 258 |
+
const marketLevels = [
|
| 259 |
+
{
|
| 260 |
+
images: { A: 'https://i.meee.com.tw/jQ3A90u.png', B: 'https://i.meee.com.tw/HEx1GWE.png', C: 'https://i.meee.com.tw/199bHzA.png' },
|
| 261 |
+
correctAnswer: 'B',
|
| 262 |
+
feedback: { correct: "答對了!哲學之塔的需求比例最高,要賺大錢還是得靠數學,不能蠻幹阿!", wrong: "每個地點的調查人數都一樣多,需求數量越多,即表示需求比例越高!" }
|
| 263 |
+
},
|
| 264 |
+
{
|
| 265 |
+
// A: 代數之丘(左), B: 哲學之塔(中), C: 寶石洞窟(右)
|
| 266 |
+
images: { A: 'https://i.meee.com.tw/lTXgvxv.png', B: 'https://i.meee.com.tw/FKPNhKh.png', C: 'https://i.meee.com.tw/zdv5MtZ.png' },
|
| 267 |
+
correctAnswer: 'A', // 正確答案是代數之丘
|
| 268 |
+
feedback: {
|
| 269 |
+
firstClick: {
|
| 270 |
+
C: "你確定嗎?其他兩個地方的需求人數比較多喔!若是確定請再點選一次",
|
| 271 |
+
B: "你確定嗎?雖然哲學之塔的需求人數最多,但他的總人數也最多喔!你確定他是比例最高的嗎?",
|
| 272 |
+
A: "你確定嗎?哲學之塔的需求人數比較多喔!"
|
| 273 |
+
},
|
| 274 |
+
correct: "你做出了正確的決定!但到底是運氣好,還是數學好呢,如果是運氣好的話,可沒有辦法長久經營阿~",
|
| 275 |
+
wrong: "商品買賣,不是越多人買越賺錢,不是喔!因為你長期的決策錯誤,導致買賣虧損,最後造成店家倒閉..."
|
| 276 |
+
}
|
| 277 |
+
},
|
| 278 |
+
{
|
| 279 |
+
// A: 代數之丘, B: 哲學之塔, C: 寶石洞窟
|
| 280 |
+
images: { A: 'https://i.meee.com.tw/rmcY2Vc.png', B: 'https://i.meee.com.tw/qV8QrUI.png', C: 'https://i.meee.com.tw/i67LLqV.png' },
|
| 281 |
+
correctAnswer: 'A', // 正確答案是代數之丘
|
| 282 |
+
feedback: { correct: "有了相對次數這項統計數據,是不是就更容易做選擇了呢~", wrong: "再仔細看看,哪個市場的需求比例最高呢?" }
|
| 283 |
+
}
|
| 284 |
+
];
|
| 285 |
+
let firstChoice = null;
|
| 286 |
+
const fishesOnScreen = [];
|
| 287 |
+
|
| 288 |
+
// --- 畫面管理 ---
|
| 289 |
+
function showScreen(screenId) {
|
| 290 |
+
['start-screen', 'game-screen', 'market-screen', 'loading-screen', 'strategy-screen', 'final-secret-screen'].forEach(id => {
|
| 291 |
+
document.getElementById(id).style.display = (id === screenId) ? 'flex' : 'none';
|
| 292 |
+
});
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
function showOverlay(overlayId) {
|
| 296 |
+
overlayContainer.style.display = 'flex';
|
| 297 |
+
['instructions-screen', 'game-over-screen', 'win-screen'].forEach(id => {
|
| 298 |
+
document.getElementById(id).style.display = (id === overlayId) ? 'flex' : 'none';
|
| 299 |
+
});
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
function hideOverlay() {
|
| 303 |
+
overlayContainer.style.display = 'none';
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
// --- 預載入函式 ---
|
| 307 |
+
function preloadImages(urls, onProgress) {
|
| 308 |
+
return new Promise((resolve) => {
|
| 309 |
+
let loadedCount = 0;
|
| 310 |
+
const totalImages = urls.length;
|
| 311 |
+
if (totalImages === 0) resolve();
|
| 312 |
+
urls.forEach(url => {
|
| 313 |
+
const img = new Image();
|
| 314 |
+
img.src = url;
|
| 315 |
+
const onFinish = () => {
|
| 316 |
+
loadedCount++;
|
| 317 |
+
onProgress(loadedCount, totalImages);
|
| 318 |
+
if (loadedCount === totalImages) resolve();
|
| 319 |
+
};
|
| 320 |
+
img.onload = onFinish;
|
| 321 |
+
img.onerror = () => { console.error(`圖片載入失敗: ${url}`); onFinish(); };
|
| 322 |
+
});
|
| 323 |
+
});
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
function startGame(levelIndex) {
|
| 327 |
+
currentLevel = levelIndex;
|
| 328 |
+
currentLevelSettings = levels[currentLevel];
|
| 329 |
+
score = 0;
|
| 330 |
+
timeLeft = currentLevelSettings.time;
|
| 331 |
+
updateUI();
|
| 332 |
+
|
| 333 |
+
fishesOnScreen.forEach(fish => fish.element.remove());
|
| 334 |
+
fishesOnScreen.length = 0;
|
| 335 |
+
|
| 336 |
+
fishSpawnInterval = setInterval(spawnFish, currentLevelSettings.spawnRate);
|
| 337 |
+
|
| 338 |
+
timerInterval = setInterval(() => {
|
| 339 |
+
timeLeft--;
|
| 340 |
+
updateUI();
|
| 341 |
+
if (timeLeft <= 0) endGame(false);
|
| 342 |
+
}, 1000);
|
| 343 |
+
|
| 344 |
+
gameInterval = setInterval(updateGame, 1000 / 60);
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
function spawnFish() {
|
| 348 |
+
if (fishesOnScreen.length >= MAX_FISH_ON_SCREEN) return;
|
| 349 |
+
|
| 350 |
+
let fishData;
|
| 351 |
+
const fishSet = currentLevelSettings.fishSet.map(name => fishTypes.find(f => f.name === name));
|
| 352 |
+
const targetFish = fishSet.find(f => f.name === currentLevelSettings.targetFish);
|
| 353 |
+
const otherFish = fishSet.filter(f => f.name !== currentLevelSettings.targetFish);
|
| 354 |
+
|
| 355 |
+
const activeDistractor = otherFish.length > 0 ? otherFish[0] : targetFish;
|
| 356 |
+
|
| 357 |
+
if (Math.random() < 0.75) {
|
| 358 |
+
fishData = targetFish;
|
| 359 |
+
} else {
|
| 360 |
+
fishData = activeDistractor;
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
const fishElement = document.createElement('img');
|
| 364 |
+
fishElement.src = fishData.src;
|
| 365 |
+
fishElement.className = 'fish';
|
| 366 |
+
|
| 367 |
+
const direction = Math.random() < 0.5 ? 'left' : 'right';
|
| 368 |
+
const speed = (Math.random() * 2 + 1.5) * currentLevelSettings.speedMultiplier;
|
| 369 |
+
const startY = Math.random() * (gameContainer.clientHeight - fishData.height);
|
| 370 |
+
|
| 371 |
+
fishElement.style.width = `${fishData.width}px`;
|
| 372 |
+
fishElement.style.height = `${fishData.height}px`;
|
| 373 |
+
fishElement.style.top = `${startY}px`;
|
| 374 |
+
|
| 375 |
+
if (direction === 'left') {
|
| 376 |
+
fishElement.style.left = `${gameContainer.clientWidth}px`;
|
| 377 |
+
fishElement.style.transform = 'scaleX(-1)';
|
| 378 |
+
} else {
|
| 379 |
+
fishElement.style.left = `-${fishData.width}px`;
|
| 380 |
+
}
|
| 381 |
+
|
| 382 |
+
const fishObject = { element: fishElement, data: fishData, speed, direction, vy: (Math.random() - 0.5) * 2, lastVyChange: Date.now() };
|
| 383 |
+
|
| 384 |
+
if (currentLevel === 2 && fishData.name === '水母') {
|
| 385 |
+
fishObject.health = 2;
|
| 386 |
+
} else {
|
| 387 |
+
fishObject.health = 1;
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
fishElement.onclick = () => catchFish(fishObject);
|
| 391 |
+
fishesOnScreen.push(fishObject);
|
| 392 |
+
gameContainer.appendChild(fishObject.element);
|
| 393 |
+
}
|
| 394 |
+
|
| 395 |
+
function updateGame() {
|
| 396 |
+
for (let i = fishesOnScreen.length - 1; i >= 0; i--) {
|
| 397 |
+
const fish = fishesOnScreen[i];
|
| 398 |
+
let currentX = parseFloat(fish.element.style.left);
|
| 399 |
+
let currentY = parseFloat(fish.element.style.top);
|
| 400 |
+
|
| 401 |
+
if (fish.direction === 'left') {
|
| 402 |
+
currentX -= fish.speed;
|
| 403 |
+
if (currentX < -fish.data.width) {
|
| 404 |
+
fish.element.remove();
|
| 405 |
+
fishesOnScreen.splice(i, 1);
|
| 406 |
+
continue;
|
| 407 |
+
}
|
| 408 |
+
} else {
|
| 409 |
+
currentX += fish.speed;
|
| 410 |
+
if (currentX > gameContainer.clientWidth) {
|
| 411 |
+
fish.element.remove();
|
| 412 |
+
fishesOnScreen.splice(i, 1);
|
| 413 |
+
continue;
|
| 414 |
+
}
|
| 415 |
+
}
|
| 416 |
+
fish.element.style.left = `${currentX}px`;
|
| 417 |
+
|
| 418 |
+
if (currentLevel > 0) {
|
| 419 |
+
if (Date.now() - fish.lastVyChange > 1000) {
|
| 420 |
+
fish.vy = (Math.random() - 0.5) * 6;
|
| 421 |
+
fish.lastVyChange = Date.now();
|
| 422 |
+
}
|
| 423 |
+
currentY += fish.vy;
|
| 424 |
+
if (currentY < 0 || currentY > gameContainer.clientHeight - fish.data.height) {
|
| 425 |
+
fish.vy *= -1;
|
| 426 |
+
}
|
| 427 |
+
fish.element.style.top = `${currentY}px`;
|
| 428 |
+
}
|
| 429 |
+
}
|
| 430 |
+
}
|
| 431 |
+
|
| 432 |
+
function catchFish(fishObject) {
|
| 433 |
+
fishObject.health--;
|
| 434 |
+
|
| 435 |
+
if (fishObject.health > 0) {
|
| 436 |
+
showCatchFeedback('!', fishObject.element, false);
|
| 437 |
+
fishObject.element.style.opacity = '0.5';
|
| 438 |
+
setTimeout(() => {
|
| 439 |
+
if (fishObject.element) {
|
| 440 |
+
fishObject.element.style.opacity = '1';
|
| 441 |
+
}
|
| 442 |
+
}, 150);
|
| 443 |
+
return;
|
| 444 |
+
}
|
| 445 |
+
|
| 446 |
+
if (fishObject.data.name === currentLevelSettings.targetFish) {
|
| 447 |
+
score++;
|
| 448 |
+
showCatchFeedback('+1', fishObject.element, true);
|
| 449 |
+
} else {
|
| 450 |
+
score = Math.max(0, score - 1);
|
| 451 |
+
showCatchFeedback('-1', fishObject.element, false);
|
| 452 |
+
}
|
| 453 |
+
|
| 454 |
+
fishObject.element.remove();
|
| 455 |
+
const index = fishesOnScreen.indexOf(fishObject);
|
| 456 |
+
if (index > -1) fishesOnScreen.splice(index, 1);
|
| 457 |
+
updateUI();
|
| 458 |
+
if (score >= currentLevelSettings.goal) endGame(true);
|
| 459 |
+
}
|
| 460 |
+
|
| 461 |
+
function showCatchFeedback(text, fishElement, isCorrect) {
|
| 462 |
+
const feedback = document.createElement('div');
|
| 463 |
+
feedback.className = 'catch-feedback';
|
| 464 |
+
feedback.textContent = text;
|
| 465 |
+
feedback.style.color = isCorrect ? '#4ade80' : '#f87171';
|
| 466 |
+
const rect = fishElement.getBoundingClientRect();
|
| 467 |
+
const containerRect = gameContainer.getBoundingClientRect();
|
| 468 |
+
feedback.style.left = `${rect.left - containerRect.left + rect.width / 2}px`;
|
| 469 |
+
feedback.style.top = `${rect.top - containerRect.top + rect.height / 2}px`;
|
| 470 |
+
gameContainer.appendChild(feedback);
|
| 471 |
+
setTimeout(() => feedback.remove(), 1000);
|
| 472 |
+
}
|
| 473 |
+
|
| 474 |
+
function updateUI() {
|
| 475 |
+
scoreDisplay.textContent = score;
|
| 476 |
+
timerDisplay.textContent = timeLeft;
|
| 477 |
+
}
|
| 478 |
+
|
| 479 |
+
function endGame(isWin) {
|
| 480 |
+
clearInterval(gameInterval);
|
| 481 |
+
clearInterval(timerInterval);
|
| 482 |
+
clearInterval(fishSpawnInterval);
|
| 483 |
+
gameInterval = timerInterval = fishSpawnInterval = null;
|
| 484 |
+
|
| 485 |
+
if (isWin) {
|
| 486 |
+
showOverlay('win-screen');
|
| 487 |
+
} else {
|
| 488 |
+
showOverlay('game-over-screen');
|
| 489 |
+
}
|
| 490 |
+
}
|
| 491 |
+
|
| 492 |
+
function setupMarketScreen() {
|
| 493 |
+
const marketLevel = marketLevels[currentLevel];
|
| 494 |
+
document.querySelectorAll('.market-fish-name').forEach(el => el.textContent = levels[currentLevel].targetFish);
|
| 495 |
+
marketImagesContainer.innerHTML = '';
|
| 496 |
+
|
| 497 |
+
const marketOrder = ['A', 'B', 'C'];
|
| 498 |
+
marketOrder.forEach(key => {
|
| 499 |
+
const src = marketLevel.images[key];
|
| 500 |
+
|
| 501 |
+
const choiceContainer = document.createElement('div');
|
| 502 |
+
choiceContainer.className = 'market-choice relative p-2 rounded-lg border-4 border-transparent cursor-pointer transition-all duration-200';
|
| 503 |
+
choiceContainer.dataset.market = key;
|
| 504 |
+
|
| 505 |
+
const img = document.createElement('img');
|
| 506 |
+
img.src = src;
|
| 507 |
+
img.alt = `[統計圖表]`;
|
| 508 |
+
img.className = 'w-full h-auto rounded-lg shadow-lg';
|
| 509 |
+
|
| 510 |
+
const zoomIcon = document.createElement('button');
|
| 511 |
+
zoomIcon.className = 'absolute top-2 right-2 bg-black/50 p-2 rounded-full text-white hover:bg-black/80 transition-colors z-10';
|
| 512 |
+
zoomIcon.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8zm8-3a1 1 0 011 1v2h2a1 1 0 110 2h-2v2a1 1 0 11-2 0v-2H5a1 1 0 110-2h2V6a1 1 0 011-1z" clip-rule="evenodd" /></svg>`;
|
| 513 |
+
zoomIcon.onclick = (e) => {
|
| 514 |
+
e.stopPropagation();
|
| 515 |
+
showEnlargedImage(src);
|
| 516 |
+
};
|
| 517 |
+
|
| 518 |
+
choiceContainer.appendChild(img);
|
| 519 |
+
choiceContainer.appendChild(zoomIcon);
|
| 520 |
+
|
| 521 |
+
choiceContainer.addEventListener('click', handleMarketChoice);
|
| 522 |
+
marketImagesContainer.appendChild(choiceContainer);
|
| 523 |
+
});
|
| 524 |
+
|
| 525 |
+
marketFeedback.textContent = '';
|
| 526 |
+
nextLevelButton.classList.add('hidden');
|
| 527 |
+
firstChoice = null;
|
| 528 |
+
}
|
| 529 |
+
|
| 530 |
+
function handleMarketChoice(e) {
|
| 531 |
+
const choiceContainer = e.target.closest('.market-choice');
|
| 532 |
+
if (!choiceContainer) return;
|
| 533 |
+
const choice = choiceContainer.dataset.market;
|
| 534 |
+
const marketLevel = marketLevels[currentLevel];
|
| 535 |
+
|
| 536 |
+
document.querySelectorAll('.market-choice').forEach(div => div.classList.remove('selected'));
|
| 537 |
+
|
| 538 |
+
if (currentLevel === 0) {
|
| 539 |
+
if (choice === marketLevel.correctAnswer) {
|
| 540 |
+
choiceContainer.classList.add('selected');
|
| 541 |
+
marketFeedback.textContent = marketLevel.feedback.correct;
|
| 542 |
+
marketFeedback.className = 'text-center text-xl font-bold min-h-[32px] mt-4 text-green-400';
|
| 543 |
+
nextLevelButton.textContent = "前往第二關";
|
| 544 |
+
nextLevelButton.classList.remove('hidden');
|
| 545 |
+
} else {
|
| 546 |
+
marketFeedback.textContent = marketLevel.feedback.wrong;
|
| 547 |
+
marketFeedback.className = 'text-center text-xl font-bold min-h-[32px] mt-4 text-yellow-400';
|
| 548 |
+
}
|
| 549 |
+
} else if (currentLevel === 1) {
|
| 550 |
+
if (firstChoice === choice) {
|
| 551 |
+
if (choice === marketLevel.correctAnswer) {
|
| 552 |
+
choiceContainer.classList.add('selected');
|
| 553 |
+
marketFeedback.textContent = marketLevel.feedback.correct;
|
| 554 |
+
marketFeedback.className = 'text-center text-xl font-bold min-h-[32px] mt-4 text-green-400';
|
| 555 |
+
nextLevelButton.textContent = "經營之神的攻略";
|
| 556 |
+
nextLevelButton.classList.remove('hidden');
|
| 557 |
+
} else {
|
| 558 |
+
marketFeedback.textContent = marketLevel.feedback.wrong;
|
| 559 |
+
marketFeedback.className = 'text-center text-xl font-bold min-h-[32px] mt-4 text-red-500';
|
| 560 |
+
}
|
| 561 |
+
firstChoice = null;
|
| 562 |
+
} else {
|
| 563 |
+
firstChoice = choice;
|
| 564 |
+
choiceContainer.classList.add('selected');
|
| 565 |
+
marketFeedback.textContent = marketLevel.feedback.firstClick[choice];
|
| 566 |
+
marketFeedback.className = 'text-center text-xl font-bold min-h-[32px] mt-4 text-cyan-300';
|
| 567 |
+
}
|
| 568 |
+
} else if (currentLevel === 2) {
|
| 569 |
+
if (choice === marketLevel.correctAnswer) {
|
| 570 |
+
choiceContainer.classList.add('selected');
|
| 571 |
+
marketFeedback.textContent = marketLevel.feedback.correct;
|
| 572 |
+
marketFeedback.className = 'text-center text-xl font-bold min-h-[32px] mt-4 text-green-400';
|
| 573 |
+
nextLevelButton.textContent = "查看海風港灣的秘密";
|
| 574 |
+
nextLevelButton.classList.remove('hidden');
|
| 575 |
+
} else {
|
| 576 |
+
marketFeedback.textContent = marketLevel.feedback.wrong;
|
| 577 |
+
marketFeedback.className = 'text-center text-xl font-bold min-h-[32px] mt-4 text-red-500';
|
| 578 |
+
}
|
| 579 |
+
}
|
| 580 |
+
}
|
| 581 |
+
|
| 582 |
+
function showEnlargedImage(src) {
|
| 583 |
+
zoomedImage.src = src;
|
| 584 |
+
zoomModal.classList.remove('hidden');
|
| 585 |
+
}
|
| 586 |
+
|
| 587 |
+
function hideEnlargedImage() {
|
| 588 |
+
zoomModal.classList.add('hidden');
|
| 589 |
+
zoomedImage.src = '';
|
| 590 |
+
}
|
| 591 |
+
|
| 592 |
+
|
| 593 |
+
// --- 事件監聽 ---
|
| 594 |
+
startButton.addEventListener('click', async () => {
|
| 595 |
+
showScreen('loading-screen');
|
| 596 |
+
const imageUrls = [
|
| 597 |
+
...fishTypes.map(fish => fish.src),
|
| 598 |
+
...Object.values(marketLevels[0].images),
|
| 599 |
+
...Object.values(marketLevels[1].images),
|
| 600 |
+
...Object.values(marketLevels[2].images),
|
| 601 |
+
'https://i.meee.com.tw/i67LLqV.png'
|
| 602 |
+
];
|
| 603 |
+
await preloadImages(imageUrls, (loaded, total) => {
|
| 604 |
+
loadingText.textContent = `正在準備海底世界... (${loaded}/${total})`;
|
| 605 |
+
});
|
| 606 |
+
|
| 607 |
+
showScreen('game-screen');
|
| 608 |
+
instructionsContent.innerHTML = `
|
| 609 |
+
<h2 class="text-3xl font-bold text-amber-300 mb-6">第一關任務</h2>
|
| 610 |
+
<div class="space-y-4 text-lg">
|
| 611 |
+
<p>在 <span class="text-yellow-400 font-bold">${levels[0].time}</span> 秒內,捕捉 <span class="text-yellow-400 font-bold">${levels[0].goal}</span> 隻${levels[0].targetFish}!</p>
|
| 612 |
+
<p>點擊<span class="text-green-400 font-bold">正確的魚</span>來加分。</p>
|
| 613 |
+
<p>注意!抓到<span class="text-red-400 font-bold">錯誤的魚</span>會扣分喔!</p>
|
| 614 |
+
</div>
|
| 615 |
+
<button id="play-game-button" class="bg-green-500 hover:bg-green-600 text-white font-bold py-3 px-8 rounded-lg text-xl mt-8">
|
| 616 |
+
了解!
|
| 617 |
+
</button>`;
|
| 618 |
+
document.getElementById('play-game-button').onclick = () => { hideOverlay(); startGame(0); };
|
| 619 |
+
showOverlay('instructions-screen');
|
| 620 |
+
goalText.textContent = `抓 ${levels[0].goal} 隻${levels[0].targetFish}`;
|
| 621 |
+
goalCount.textContent = levels[0].goal;
|
| 622 |
+
});
|
| 623 |
+
|
| 624 |
+
restartButton.addEventListener('click', () => {
|
| 625 |
+
hideOverlay();
|
| 626 |
+
startGame(currentLevel);
|
| 627 |
+
});
|
| 628 |
+
|
| 629 |
+
continueButton.addEventListener('click', () => {
|
| 630 |
+
hideOverlay();
|
| 631 |
+
showScreen('market-screen');
|
| 632 |
+
// FIX: Update the fish name immediately when showing the market screen instructions.
|
| 633 |
+
document.querySelectorAll('.market-fish-name').forEach(el => el.textContent = levels[currentLevel].targetFish);
|
| 634 |
+
marketInstructions.style.display = 'block';
|
| 635 |
+
marketChoices.style.display = 'none';
|
| 636 |
+
});
|
| 637 |
+
|
| 638 |
+
showMarketChoicesButton.addEventListener('click', () => {
|
| 639 |
+
marketInstructions.style.display = 'none';
|
| 640 |
+
marketChoices.style.display = 'flex';
|
| 641 |
+
setupMarketScreen();
|
| 642 |
+
});
|
| 643 |
+
|
| 644 |
+
nextLevelButton.addEventListener('click', () => {
|
| 645 |
+
if (currentLevel === 0) {
|
| 646 |
+
const nextLevel = currentLevel + 1;
|
| 647 |
+
const levelSettings = levels[nextLevel];
|
| 648 |
+
showScreen('game-screen');
|
| 649 |
+
instructionsContent.innerHTML = `
|
| 650 |
+
<h2 class="text-3xl font-bold text-amber-300 mb-6">第 ${nextLevel + 1} 關任務</h2>
|
| 651 |
+
<div class="space-y-4 text-lg">
|
| 652 |
+
<p>在 <span class="text-yellow-400 font-bold">${levelSettings.time}</span> 秒內,捕捉 <span class="text-yellow-400 font-bold">${levelSettings.goal}</span> 隻${levelSettings.targetFish}!</p>
|
| 653 |
+
<p>海洋生物的移動方式不一樣了,小心!</p>
|
| 654 |
+
</div>
|
| 655 |
+
<button id="play-next-level-button" class="bg-green-500 hover:bg-green-600 text-white font-bold py-3 px-8 rounded-lg text-xl mt-8">
|
| 656 |
+
挑戰!
|
| 657 |
+
</button>`;
|
| 658 |
+
document.getElementById('play-next-level-button').onclick = () => { hideOverlay(); startGame(nextLevel); };
|
| 659 |
+
showOverlay('instructions-screen');
|
| 660 |
+
goalText.textContent = `抓 ${levelSettings.goal} 隻${levelSettings.targetFish}`;
|
| 661 |
+
goalCount.textContent = levelSettings.goal;
|
| 662 |
+
} else if (currentLevel === 1) {
|
| 663 |
+
showScreen('strategy-screen');
|
| 664 |
+
} else if (currentLevel === 2) {
|
| 665 |
+
showScreen('final-secret-screen');
|
| 666 |
+
}
|
| 667 |
+
});
|
| 668 |
+
|
| 669 |
+
startFinalStageButton.addEventListener('click', () => {
|
| 670 |
+
const nextLevel = currentLevel + 1;
|
| 671 |
+
const levelSettings = levels[nextLevel];
|
| 672 |
+
showScreen('game-screen');
|
| 673 |
+
instructionsContent.innerHTML = `
|
| 674 |
+
<h2 class="text-3xl font-bold text-amber-300 mb-6">最終挑戰!</h2>
|
| 675 |
+
<div class="space-y-4 text-lg">
|
| 676 |
+
<p>在 <span class="text-yellow-400 font-bold">${levelSettings.time}</span> 秒內,捕捉 <span class="text-yellow-400 font-bold">${levelSettings.goal}</span> 隻${levelSettings.targetFish}!</p>
|
| 677 |
+
<p class="text-cyan-300">注意!這次的${levelSettings.targetFish}比較頑強,需要點擊兩下才能捕捉!</p>
|
| 678 |
+
</div>
|
| 679 |
+
<button id="play-final-level-button" class="bg-green-500 hover:bg-green-600 text-white font-bold py-3 px-8 rounded-lg text-xl mt-8">
|
| 680 |
+
挑戰!
|
| 681 |
+
</button>`;
|
| 682 |
+
document.getElementById('play-final-level-button').onclick = () => { hideOverlay(); startGame(nextLevel); };
|
| 683 |
+
showOverlay('instructions-screen');
|
| 684 |
+
goalText.textContent = `抓 ${levelSettings.goal} 隻${levelSettings.targetFish}`;
|
| 685 |
+
goalCount.textContent = levelSettings.goal;
|
| 686 |
+
});
|
| 687 |
+
|
| 688 |
+
zoomModal.addEventListener('click', () => {
|
| 689 |
+
hideEnlargedImage();
|
| 690 |
+
});
|
| 691 |
+
|
| 692 |
+
// --- 初始啟動 ---
|
| 693 |
+
showScreen('start-screen');
|
| 694 |
+
});
|
| 695 |
+
</script>
|
| 696 |
+
</body>
|
| 697 |
+
</html>
|
example/index.html
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="zh-Hant">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>數學探險島</title>
|
| 8 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 9 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 10 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 11 |
+
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@400;500;700&display=swap" rel="stylesheet">
|
| 12 |
+
<style>
|
| 13 |
+
body {
|
| 14 |
+
font-family: 'Noto Sans TC', sans-serif;
|
| 15 |
+
background-color: #333;
|
| 16 |
+
overflow: hidden;
|
| 17 |
+
/* 隱藏滾動條 */
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
#map-container {
|
| 21 |
+
position: absolute;
|
| 22 |
+
top: 0;
|
| 23 |
+
left: 0;
|
| 24 |
+
width: 100vw;
|
| 25 |
+
height: 100vh;
|
| 26 |
+
background-image: url('https://i.meee.com.tw/emt8qds.png');
|
| 27 |
+
background-size: cover;
|
| 28 |
+
/* 填滿整個容器 */
|
| 29 |
+
background-repeat: no-repeat;
|
| 30 |
+
background-position: center;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
.main-title {
|
| 34 |
+
position: relative;
|
| 35 |
+
/* 確保標題在最上層 */
|
| 36 |
+
z-index: 20;
|
| 37 |
+
animation: fadeOutTitle 5s forwards;
|
| 38 |
+
animation-delay: 2s;
|
| 39 |
+
/* 2秒後開始淡出 */
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
@keyframes fadeOutTitle {
|
| 43 |
+
0% {
|
| 44 |
+
opacity: 1;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
100% {
|
| 48 |
+
opacity: 0;
|
| 49 |
+
pointer-events: none;
|
| 50 |
+
}
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
.location-link {
|
| 54 |
+
position: absolute;
|
| 55 |
+
border-radius: 50%;
|
| 56 |
+
transform: translate(-50%, -50%);
|
| 57 |
+
display: flex;
|
| 58 |
+
align-items: center;
|
| 59 |
+
justify-content: center;
|
| 60 |
+
text-decoration: none;
|
| 61 |
+
cursor: pointer;
|
| 62 |
+
z-index: 10;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
.location-link::before {
|
| 66 |
+
content: '';
|
| 67 |
+
position: absolute;
|
| 68 |
+
width: 80px;
|
| 69 |
+
/* 縮小光圈的尺寸 */
|
| 70 |
+
height: 80px;
|
| 71 |
+
border-radius: 50%;
|
| 72 |
+
transition: all 0.3s ease;
|
| 73 |
+
transform: scale(0);
|
| 74 |
+
/* 預設隱藏 */
|
| 75 |
+
opacity: 0;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
.location-link:hover::before {
|
| 79 |
+
transform: scale(1);
|
| 80 |
+
/* 滑鼠移入時顯示 */
|
| 81 |
+
opacity: 1;
|
| 82 |
+
background-color: rgba(255, 255, 255, 0.2);
|
| 83 |
+
box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.5), 0 0 20px 7px rgba(255, 235, 59, 0.6);
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
.location-link .label {
|
| 87 |
+
opacity: 0;
|
| 88 |
+
color: white;
|
| 89 |
+
font-size: 1.5rem;
|
| 90 |
+
font-weight: bold;
|
| 91 |
+
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
|
| 92 |
+
transition: opacity 0.3s ease;
|
| 93 |
+
background-color: rgba(0, 0, 0, 0.5);
|
| 94 |
+
padding: 0.5rem 1rem;
|
| 95 |
+
border-radius: 999px;
|
| 96 |
+
position: relative;
|
| 97 |
+
/* 確保文字在光圈之上 */
|
| 98 |
+
z-index: 5;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
.location-link:hover .label {
|
| 102 |
+
opacity: 1;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
/* 最終調整後的地點位置 */
|
| 106 |
+
#algebra-hill {
|
| 107 |
+
top: 28%;
|
| 108 |
+
left: 88%;
|
| 109 |
+
width: 25%;
|
| 110 |
+
height: 35%;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
#philosophy-tower {
|
| 114 |
+
top: 35%;
|
| 115 |
+
left: 37%;
|
| 116 |
+
width: 22%;
|
| 117 |
+
height: 40%;
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
#gemstone-cave {
|
| 121 |
+
top: 58%;
|
| 122 |
+
left: 88%;
|
| 123 |
+
width: 20%;
|
| 124 |
+
height: 25%;
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
#harbor-bay {
|
| 128 |
+
top: 75%;
|
| 129 |
+
left: 25%;
|
| 130 |
+
width: 35%;
|
| 131 |
+
height: 35%;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
/* 風的痕跡 SVG 樣式 */
|
| 135 |
+
#footprints-svg {
|
| 136 |
+
position: absolute;
|
| 137 |
+
top: 0;
|
| 138 |
+
left: 0;
|
| 139 |
+
width: 100%;
|
| 140 |
+
height: 100%;
|
| 141 |
+
pointer-events: none;
|
| 142 |
+
/* 讓 SVG 不會擋到滑鼠點擊 */
|
| 143 |
+
z-index: 5;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
.footprint-path {
|
| 147 |
+
stroke: rgba(255, 255, 255, 0.4);
|
| 148 |
+
stroke-width: 2.5px;
|
| 149 |
+
fill: none;
|
| 150 |
+
stroke-linecap: round;
|
| 151 |
+
stroke-dasharray: 150 400;
|
| 152 |
+
/* 150px長的痕跡, 400px的間隔 */
|
| 153 |
+
stroke-dashoffset: 550;
|
| 154 |
+
animation: flow 15s linear infinite;
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
/* 讓每個路徑的動畫錯開,看起來更自然 */
|
| 158 |
+
.footprint-path:nth-child(2) {
|
| 159 |
+
animation-delay: -3s;
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
.footprint-path:nth-child(3) {
|
| 163 |
+
animation-delay: -7s;
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
.footprint-path:nth-child(4) {
|
| 167 |
+
animation-delay: -10s;
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
@keyframes flow {
|
| 171 |
+
from {
|
| 172 |
+
stroke-dashoffset: 550;
|
| 173 |
+
/* 從路徑外開始 */
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
to {
|
| 177 |
+
stroke-dashoffset: -550;
|
| 178 |
+
/* 移動到路徑的另一端外 */
|
| 179 |
+
}
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
/* 載入畫面樣式 */
|
| 183 |
+
#loading-screen {
|
| 184 |
+
position: absolute;
|
| 185 |
+
inset: 0;
|
| 186 |
+
z-index: 99;
|
| 187 |
+
background-color: #1a202c;
|
| 188 |
+
transition: opacity 0.5s ease-out;
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
.loader {
|
| 192 |
+
border: 8px solid #f3f3f3;
|
| 193 |
+
border-top: 8px solid #3498db;
|
| 194 |
+
border-radius: 50%;
|
| 195 |
+
width: 60px;
|
| 196 |
+
height: 60px;
|
| 197 |
+
animation: spin 1s linear infinite;
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
@keyframes spin {
|
| 201 |
+
0% {
|
| 202 |
+
transform: rotate(0deg);
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
100% {
|
| 206 |
+
transform: rotate(360deg);
|
| 207 |
+
}
|
| 208 |
+
}
|
| 209 |
+
</style>
|
| 210 |
+
</head>
|
| 211 |
+
|
| 212 |
+
<body class="bg-gray-900 flex items-center justify-center min-h-screen p-4">
|
| 213 |
+
|
| 214 |
+
<!-- 載入畫面 -->
|
| 215 |
+
<div id="loading-screen" class="w-full h-full flex flex-col items-center justify-center">
|
| 216 |
+
<div class="loader"></div>
|
| 217 |
+
<p class="text-white text-xl mt-4">地圖載入中...</p>
|
| 218 |
+
</div>
|
| 219 |
+
|
| 220 |
+
<!-- 主內容 (預設隱藏) -->
|
| 221 |
+
<div id="main-content" class="opacity-0 transition-opacity duration-500">
|
| 222 |
+
<div id="map-container">
|
| 223 |
+
<!-- 動態痕跡 SVG -->
|
| 224 |
+
<svg id="footprints-svg" viewBox="0 0 1200 900" preserveAspectRatio="xMidYMid meet">
|
| 225 |
+
<!-- 重新設計的、更不規則的曲線路徑 -->
|
| 226 |
+
<path class="footprint-path" d="M600 900 C 500 880, 350 800, 300 675" />
|
| 227 |
+
<path class="footprint-path" d="M600 900 C 600 700, 380 550, 444 315" />
|
| 228 |
+
<path class="footprint-path" d="M600 900 C 750 850, 950 700, 1056 522" />
|
| 229 |
+
<path class="footprint-path" d="M600 900 C 700 750, 980 500, 1056 252" />
|
| 230 |
+
</svg>
|
| 231 |
+
|
| 232 |
+
<!-- 地點連結 -->
|
| 233 |
+
<a id="algebra-hill" href="algebra.html" class="location-link">
|
| 234 |
+
<span class="label">代數之丘</span>
|
| 235 |
+
</a>
|
| 236 |
+
<a id="philosophy-tower" href="philosophy.html" class="location-link">
|
| 237 |
+
<span class="label">哲學之塔</span>
|
| 238 |
+
</a>
|
| 239 |
+
<a id="gemstone-cave" href="gemstone.html" class="location-link">
|
| 240 |
+
<span class="label">寶石洞窟</span>
|
| 241 |
+
</a>
|
| 242 |
+
<a id="harbor-bay" href="harbor.html" class="location-link">
|
| 243 |
+
<span class="label">海風港灣</span>
|
| 244 |
+
</a>
|
| 245 |
+
</div>
|
| 246 |
+
|
| 247 |
+
<h1 class="main-title text-4xl md:text-5xl font-bold text-white text-center"
|
| 248 |
+
style="text-shadow: 3px 3px 5px rgba(0,0,0,0.5);">歡迎來到數學探險島</h1>
|
| 249 |
+
|
| 250 |
+
<!-- 設計者資訊 -->
|
| 251 |
+
<div class="absolute bottom-4 right-4 text-right text-white text-sm opacity-80 z-10"
|
| 252 |
+
style="text-shadow: 1px 1px 3px rgba(0,0,0,0.7);">
|
| 253 |
+
<p>遊戲設計者:新竹縣精華國中藍星宇</p>
|
| 254 |
+
<p>FB教育社群:<a href="https://www.facebook.com/groups/1554372228718393" target="_blank"
|
| 255 |
+
class="underline hover:text-yellow-300 transition-colors">萬物皆數</a></p>
|
| 256 |
+
</div>
|
| 257 |
+
</div>
|
| 258 |
+
|
| 259 |
+
<script>
|
| 260 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 261 |
+
// ... existing image load code ...
|
| 262 |
+
});
|
| 263 |
+
|
| 264 |
+
// Load Function Game Score for Gemstone Cave (寶石洞窟)
|
| 265 |
+
// Assuming Gemstone Cave is the Energy Core / Function game
|
| 266 |
+
const scoreFunc = localStorage.getItem('math_city_score_function');
|
| 267 |
+
if (scoreFunc) {
|
| 268 |
+
const gemstoneLabel = document.querySelector('#gemstone-cave .label');
|
| 269 |
+
if (gemstoneLabel) {
|
| 270 |
+
// Add score display
|
| 271 |
+
const scoreDiv = document.createElement('div');
|
| 272 |
+
scoreDiv.className = 'text-xs text-amber-300 text-center mt-1 font-mono tracking-widest';
|
| 273 |
+
scoreDiv.innerText = `BEST: ${scoreFunc}`;
|
| 274 |
+
gemstoneLabel.appendChild(scoreDiv);
|
| 275 |
+
|
| 276 |
+
// Adjust layout to handle multiline
|
| 277 |
+
gemstoneLabel.style.flexDirection = 'column';
|
| 278 |
+
gemstoneLabel.style.display = 'flex';
|
| 279 |
+
gemstoneLabel.style.alignItems = 'center';
|
| 280 |
+
}
|
| 281 |
+
}
|
| 282 |
+
</script>
|
| 283 |
+
|
| 284 |
+
<script>
|
| 285 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 286 |
+
const loadingScreen = document.getElementById('loading-screen');
|
| 287 |
+
const mainContent = document.getElementById('main-content');
|
| 288 |
+
const imageUrl = 'https://i.meee.com.tw/emt8qds.png';
|
| 289 |
+
|
| 290 |
+
const image = new Image();
|
| 291 |
+
image.src = imageUrl;
|
| 292 |
+
|
| 293 |
+
image.onload = () => {
|
| 294 |
+
// 圖片載入完成後,淡出載入畫面
|
| 295 |
+
loadingScreen.style.opacity = '0';
|
| 296 |
+
|
| 297 |
+
// 淡入主內容
|
| 298 |
+
mainContent.style.opacity = '1';
|
| 299 |
+
|
| 300 |
+
// 在淡出動畫結束後,徹底隱藏載入畫面
|
| 301 |
+
setTimeout(() => {
|
| 302 |
+
loadingScreen.style.display = 'none';
|
| 303 |
+
}, 500); // 需與 transition duration 匹配
|
| 304 |
+
};
|
| 305 |
+
|
| 306 |
+
image.onerror = () => {
|
| 307 |
+
// 如果圖片載入失敗,也顯示主內容,避免卡住
|
| 308 |
+
console.error('背景圖片載入失敗!');
|
| 309 |
+
loadingScreen.style.opacity = '0';
|
| 310 |
+
mainContent.style.opacity = '1';
|
| 311 |
+
setTimeout(() => {
|
| 312 |
+
loadingScreen.style.display = 'none';
|
| 313 |
+
}, 500);
|
| 314 |
+
}
|
| 315 |
+
});
|
| 316 |
+
</script>
|
| 317 |
+
</body>
|
| 318 |
+
|
| 319 |
+
</html>
|
example/philosophy.html
ADDED
|
@@ -0,0 +1,431 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="zh-Hant">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>數學探險島 - 哲學之塔</title>
|
| 7 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 8 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 9 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 10 |
+
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@400;500;700&display=swap" rel="stylesheet">
|
| 11 |
+
<style>
|
| 12 |
+
body {
|
| 13 |
+
font-family: 'Noto Sans TC', sans-serif;
|
| 14 |
+
background-image: url('https://i.meee.com.tw/wCWCOGx.png');
|
| 15 |
+
background-size: cover;
|
| 16 |
+
background-position: center;
|
| 17 |
+
background-attachment: fixed;
|
| 18 |
+
}
|
| 19 |
+
.tower-bg {
|
| 20 |
+
background-image: url('https://www.transparenttextures.com/patterns/stone-wall.png');
|
| 21 |
+
background-color: #e2e8f0;
|
| 22 |
+
}
|
| 23 |
+
/* 自訂拉桿樣式 */
|
| 24 |
+
input[type=range] {
|
| 25 |
+
-webkit-appearance: none;
|
| 26 |
+
width: 100%;
|
| 27 |
+
background: transparent;
|
| 28 |
+
}
|
| 29 |
+
input[type=range]:focus {
|
| 30 |
+
outline: none;
|
| 31 |
+
}
|
| 32 |
+
input[type=range]::-webkit-slider-runnable-track {
|
| 33 |
+
width: 100%;
|
| 34 |
+
height: 12px;
|
| 35 |
+
cursor: pointer;
|
| 36 |
+
background: #93c5fd;
|
| 37 |
+
border-radius: 5px;
|
| 38 |
+
border: 1px solid #60a5fa;
|
| 39 |
+
}
|
| 40 |
+
input[type=range]::-webkit-slider-thumb {
|
| 41 |
+
border: 2px solid #3b82f6;
|
| 42 |
+
height: 30px;
|
| 43 |
+
width: 30px;
|
| 44 |
+
border-radius: 50%;
|
| 45 |
+
background: #eff6ff;
|
| 46 |
+
cursor: pointer;
|
| 47 |
+
-webkit-appearance: none;
|
| 48 |
+
margin-top: -10px;
|
| 49 |
+
}
|
| 50 |
+
/* 成功時的閃爍動畫 */
|
| 51 |
+
@keyframes flash {
|
| 52 |
+
0%, 100% { box-shadow: 0 0 20px 5px rgba(34, 197, 94, 0.7); }
|
| 53 |
+
50% { box-shadow: 0 0 5px 0px rgba(34, 197, 94, 0.2); }
|
| 54 |
+
}
|
| 55 |
+
.success-flash {
|
| 56 |
+
animation: flash 1.5s ease-in-out;
|
| 57 |
+
}
|
| 58 |
+
/* 測驗選項樣式 */
|
| 59 |
+
.quiz-option {
|
| 60 |
+
display: block;
|
| 61 |
+
padding: 0.75rem 1rem;
|
| 62 |
+
border: 2px solid #e5e7eb;
|
| 63 |
+
border-radius: 0.5rem;
|
| 64 |
+
margin-bottom: 0.5rem;
|
| 65 |
+
cursor: pointer;
|
| 66 |
+
transition: all 0.2s;
|
| 67 |
+
}
|
| 68 |
+
.quiz-option:hover {
|
| 69 |
+
background-color: #f3f4f6;
|
| 70 |
+
}
|
| 71 |
+
input[type="radio"]:checked + .quiz-option {
|
| 72 |
+
background-color: #dbeafe;
|
| 73 |
+
border-color: #60a5fa;
|
| 74 |
+
}
|
| 75 |
+
.question-container.incorrect {
|
| 76 |
+
border: 2px solid #ef4444;
|
| 77 |
+
border-radius: 0.75rem;
|
| 78 |
+
padding: 1rem;
|
| 79 |
+
background-color: #fee2e2;
|
| 80 |
+
}
|
| 81 |
+
</style>
|
| 82 |
+
</head>
|
| 83 |
+
<body class="flex items-center justify-center min-h-screen p-4">
|
| 84 |
+
|
| 85 |
+
<div id="main-container" class="container mx-auto max-w-5xl">
|
| 86 |
+
|
| 87 |
+
<!-- 故事引導畫面 -->
|
| 88 |
+
<div id="story-view" class="bg-white/80 backdrop-blur-md rounded-xl shadow-2xl p-8 text-center">
|
| 89 |
+
<h1 class="text-3xl md:text-4xl font-bold text-gray-800 text-center mb-6">前情提要:西帕索斯的悲劇</h1>
|
| 90 |
+
<div class="border rounded-lg shadow-inner bg-gray-100 p-12 flex flex-col items-center justify-center" style="height: 50vh;">
|
| 91 |
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-24 w-24 text-indigo-300 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
| 92 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v11.494m-9-5.747h18" />
|
| 93 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v11.494m-9-5.747h18" />
|
| 94 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 18h16" />
|
| 95 |
+
</svg>
|
| 96 |
+
<p class="text-xl text-gray-600">故事即將展開...</p>
|
| 97 |
+
<p class="mt-4 text-lg text-indigo-600 font-semibold">點擊下方按鈕,在新分頁閱讀故事前情提要!</p>
|
| 98 |
+
</div>
|
| 99 |
+
<p class="mt-8 text-gray-600 font-semibold">閱讀完故事後,請回來按下開始挑戰</p>
|
| 100 |
+
<a href="https://g.co/gemini/share/d181055c5aa2" target="_blank" id="story-link-button" class="mt-2 inline-block w-full md:w-auto bg-blue-500 text-white font-bold py-3 px-8 rounded-lg hover:bg-blue-600 transition-colors shadow-lg text-lg">
|
| 101 |
+
閱讀故事
|
| 102 |
+
</a>
|
| 103 |
+
<button id="start-quiz-button" class="mt-4 w-full md:w-auto bg-indigo-600 text-white font-bold py-3 px-8 rounded-lg hover:bg-indigo-700 transition-colors shadow-lg text-lg">
|
| 104 |
+
開始挑戰!
|
| 105 |
+
</button>
|
| 106 |
+
</div>
|
| 107 |
+
|
| 108 |
+
<!-- 閱讀測驗畫面 (新增) -->
|
| 109 |
+
<div id="quiz-view" class="hidden bg-white/80 backdrop-blur-md rounded-xl shadow-2xl p-8">
|
| 110 |
+
<h1 class="text-3xl font-bold text-gray-800 text-center mb-6">閱讀測驗:西帕索斯的考驗</h1>
|
| 111 |
+
<div class="space-y-6 text-left">
|
| 112 |
+
<!-- 問題 1 -->
|
| 113 |
+
<div id="question-container-0" class="question-container">
|
| 114 |
+
<p class="font-semibold mb-2">1. 根據故事內容,西帕索斯為什麼被畢達哥拉斯囚禁在哲學之塔?</p>
|
| 115 |
+
<div class="space-y-2">
|
| 116 |
+
<label><input type="radio" name="q0" value="A" class="hidden"> <span class="quiz-option">(A) 因為他試圖偷取畢達哥拉斯的數學理論。</span></label>
|
| 117 |
+
<label><input type="radio" name="q0" value="B" class="hidden"> <span class="quiz-option">(B) 因為他在公開場合侮辱了畢達哥拉斯本人。</span></label>
|
| 118 |
+
<label><input type="radio" name="q0" value="C" class="hidden"> <span class="quiz-option">(C) 因為他發現了一個無法用分數或小數表示的數字,這與畢達哥拉斯學派的信念相衝突。</span></label>
|
| 119 |
+
<label><input type="radio" name="q0" value="D" class="hidden"> <span class="quiz-option">(D) 因為他沒能成功計算出一個正方形的面積。</span></label>
|
| 120 |
+
</div>
|
| 121 |
+
</div>
|
| 122 |
+
<!-- 問題 2 -->
|
| 123 |
+
<div id="question-container-1" class="question-container">
|
| 124 |
+
<p class="font-semibold mb-2">2. 故事中,那個動搖了畢達哥拉斯學派信念的「神秘數字」,最初是源自於什麼?</p>
|
| 125 |
+
<div class="space-y-2">
|
| 126 |
+
<label><input type="radio" name="q1" value="A" class="hidden"> <span class="quiz-option">(A) 一個面積為3的圓形半徑。</span></label>
|
| 127 |
+
<label><input type="radio" name="q1" value="B" class="hidden"> <span class="quiz-option">(B) 一個面積為2的正方形邊長。</span></label>
|
| 128 |
+
<label><input type="radio" name="q1" value="C" class="hidden"> <span class="quiz-option">(C) 一個從未有人見過的全新立體圖形。</span></label>
|
| 129 |
+
<label><input type="radio" name="q1" value="D" class="hidden"> <span class="quiz-option">(D) 一個畢達哥拉斯自己提出的數學謎題。</span></label>
|
| 130 |
+
</div>
|
| 131 |
+
</div>
|
| 132 |
+
<!-- 問題 3 -->
|
| 133 |
+
<div id="question-container-2" class="question-container">
|
| 134 |
+
<p class="font-semibold mb-2">3. 在故事的結尾,你(讀者)被賦予的主要任務是什麼?</p>
|
| 135 |
+
<div class="space-y-2">
|
| 136 |
+
<label><input type="radio" name="q2" value="A" class="hidden"> <span class="quiz-option">(A) 找到一把萬能鑰匙,趁半夜把西帕索斯從監獄裡救出來。</span></label>
|
| 137 |
+
<label><input type="radio" name="q2" value="B" class="hidden"> <span class="quiz-option">(B) 回到未來,尋找歷史文獻來證明西帕索斯是對的。</span></label>
|
| 138 |
+
<label><input type="radio" name="q2" value="C" class="hidden"> <span class="quiz-option">(C) 成為畢達哥拉斯的學徒,從內部瓦解他的學派。</span></label>
|
| 139 |
+
<label><input type="radio" name="q2" value="D" class="hidden"> <span class="quiz-option">(D) 找出幾個「神秘數字」的近似值,用具體的證據去說服畢達哥拉斯。</span></label>
|
| 140 |
+
</div>
|
| 141 |
+
</div>
|
| 142 |
+
</div>
|
| 143 |
+
<div id="quiz-feedback" class="text-center font-semibold mt-6 min-h-[24px]"></div>
|
| 144 |
+
<div class="flex flex-col md:flex-row gap-4 mt-6">
|
| 145 |
+
<button id="reread-story-button" class="w-full md:w-1/2 bg-gray-500 text-white font-bold py-3 px-6 rounded-lg hover:bg-gray-600 transition-colors">再看一次故事</button>
|
| 146 |
+
<button id="submit-quiz-button" class="w-full md:w-1/2 bg-green-500 text-white font-bold py-3 px-6 rounded-lg hover:bg-green-600 transition-colors">提交答案</button>
|
| 147 |
+
</div>
|
| 148 |
+
</div>
|
| 149 |
+
|
| 150 |
+
<!-- 遊戲畫面 (預設隱藏) -->
|
| 151 |
+
<div id="game-view" class="hidden bg-white/80 backdrop-blur-md rounded-xl shadow-2xl p-8 tower-bg">
|
| 152 |
+
<h1 class="text-3xl md:text-4xl font-bold text-gray-800 text-center mb-2">哲學之塔</h1>
|
| 153 |
+
<p class="text-center text-gray-600 mb-6">幫助被囚禁的西帕索斯,找出正方形的神秘邊長!</p>
|
| 154 |
+
|
| 155 |
+
<div class="grid md:grid-cols-2 gap-8 items-center">
|
| 156 |
+
<!-- 左側:正方形展示區 -->
|
| 157 |
+
<div class="flex flex-col items-center justify-center bg-white/70 p-6 rounded-lg shadow-inner">
|
| 158 |
+
<div id="square-display" class="w-48 h-48 bg-blue-300 border-4 border-blue-500 flex items-center justify-center relative transition-transform duration-500">
|
| 159 |
+
<p class="text-2xl font-bold text-white">面積 = <span id="target-area-text">2</span></p>
|
| 160 |
+
</div>
|
| 161 |
+
<p class="mt-4 text-2xl font-mono text-gray-700">邊長 = <span id="target-sqrt-text">√2</span></p>
|
| 162 |
+
</div>
|
| 163 |
+
|
| 164 |
+
<!-- 右側:互動操作區 -->
|
| 165 |
+
<div class="flex flex-col space-y-6">
|
| 166 |
+
<div class="bg-white/80 p-4 rounded-lg text-center">
|
| 167 |
+
<p class="text-lg">你的猜測邊長 <span id="guess-sqrt-text" class="font-mono">√2</span> ≒ <span id="current-value" class="font-bold text-2xl text-indigo-600">1.50</span></p>
|
| 168 |
+
<p class="text-lg">邊長的平方(正方形面積):<span id="current-squared-value" class="font-bold text-2xl text-red-500">2.25</span></p>
|
| 169 |
+
</div>
|
| 170 |
+
|
| 171 |
+
<input type="range" id="value-slider" min="1" max="2" value="1.5" step="0.01" class="w-full">
|
| 172 |
+
|
| 173 |
+
<button id="check-button" class="w-full bg-green-500 text-white font-bold py-3 px-4 rounded-lg hover:bg-green-600 transition-colors shadow-md text-xl">
|
| 174 |
+
確定答案
|
| 175 |
+
</button>
|
| 176 |
+
|
| 177 |
+
<div id="feedback-box" class="text-center text-xl font-semibold min-h-[32px]"></div>
|
| 178 |
+
<button id="next-level-button" class="w-full bg-indigo-500 text-white font-bold py-3 px-4 rounded-lg hover:bg-indigo-600 transition-colors shadow-md text-xl hidden">
|
| 179 |
+
挑戰下一關
|
| 180 |
+
</button>
|
| 181 |
+
</div>
|
| 182 |
+
</div>
|
| 183 |
+
</div>
|
| 184 |
+
|
| 185 |
+
<!-- 生活應用畫面 (預設隱藏) -->
|
| 186 |
+
<div id="application-view" class="hidden bg-white/80 backdrop-blur-md rounded-xl shadow-2xl p-8">
|
| 187 |
+
<h1 class="text-3xl md:text-4xl font-bold text-green-600 mb-4 text-center">恭喜你,成功解救了西帕索斯!</h1>
|
| 188 |
+
<p class="text-lg text-gray-700 mb-8 text-center">你們玩遊戲的時候,有沒有想過,為什麼角色撞到怪物就會扣血?為什麼子彈會正好打到你?這不是靠神奇的第六感,是靠數學──而且關鍵數學招式就是平方根!</p>
|
| 189 |
+
|
| 190 |
+
<div class="bg-blue-50 p-6 rounded-lg text-left">
|
| 191 |
+
<h2 class="text-2xl font-bold text-indigo-600 mb-4 text-center">🎮 遊戲物理引擎裡的平方根魔法</h2>
|
| 192 |
+
<div class="space-y-6">
|
| 193 |
+
<div>
|
| 194 |
+
<h3 class="font-semibold text-xl mb-2">判斷「有沒有撞到」</h3>
|
| 195 |
+
<p>遊戲世界是由座標組成的(就像地圖上的 X、Y 點)。當角色和怪物在不同位置,遊戲必須算兩者的距離,來判斷是不是近到可以碰撞。</p>
|
| 196 |
+
<p class="mt-2 p-3 bg-gray-200 rounded-md text-center font-mono text-lg">距離 = √((x₁ - x₂)² + (y₁ - y₂)²)</p>
|
| 197 |
+
<p class="mt-2">最後那個開根號,就是讓距離變回「真實長度」。沒有平方根,遊戲根本不知道你到底是不是碰到牆、怪物、寶箱。</p>
|
| 198 |
+
</div>
|
| 199 |
+
<div>
|
| 200 |
+
<h3 class="font-semibold text-xl mb-2">算出移動的真速度</h3>
|
| 201 |
+
<p>在 3D 遊戲中,速度不只有一個方向。例如你同時向前(X 軸)+ 向右(Y 軸)移動,總速度不是單純相加,而是:</p>
|
| 202 |
+
<p class="mt-2 p-3 bg-gray-200 rounded-md text-center font-mono text-lg">總速度 = √(水平速度² + 垂直速度²)</p>
|
| 203 |
+
<p class="mt-2">這樣遊戲才能正確決定動畫快慢、物理反應強度。</p>
|
| 204 |
+
</div>
|
| 205 |
+
<div>
|
| 206 |
+
<h3 class="font-semibold text-xl mb-2">模擬「真實的碰撞反應」</h3>
|
| 207 |
+
<p>角色被怪物推一下,會往斜方向飛。遊戲會算推力向量的大小(也就是「合力長度」)來決定你飛多遠,這裡也要平方根。</p>
|
| 208 |
+
</div>
|
| 209 |
+
</div>
|
| 210 |
+
<div class="mt-6 pt-6 border-t">
|
| 211 |
+
<p class="text-xl text-center"><span class="font-bold">💡 簡單比喻:</span>「平方根在遊戲引擎裡,就像一把『距離測量尺』,沒有它,遊戲世界就不知道東西有多近、有多快,甚至不會知道你到底撞到沒。」</p>
|
| 212 |
+
</div>
|
| 213 |
+
</div>
|
| 214 |
+
|
| 215 |
+
<div class="mt-12 text-center">
|
| 216 |
+
<a href="index.html" class="inline-block w-full md:w-1/2 bg-indigo-600 text-white font-bold py-3 md:py-4 px-6 rounded-lg hover:bg-indigo-700 transition-colors shadow-lg text-lg md:text-xl">
|
| 217 |
+
回到探險島地圖
|
| 218 |
+
</a>
|
| 219 |
+
</div>
|
| 220 |
+
</div>
|
| 221 |
+
|
| 222 |
+
</div>
|
| 223 |
+
|
| 224 |
+
<script>
|
| 225 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 226 |
+
// --- 關���設定 ---
|
| 227 |
+
const levels = [
|
| 228 |
+
{ target: 2, min: 1, max: 2, tolerance: 0.05, type: 'manual' },
|
| 229 |
+
{ target: 3, min: 1, max: 2, tolerance: 0.05, type: 'auto', speed: 0.007 },
|
| 230 |
+
{ target: 5, min: 2, max: 3, tolerance: 0.05, type: 'auto', speed: 0.011 }
|
| 231 |
+
];
|
| 232 |
+
let currentLevel = 0;
|
| 233 |
+
|
| 234 |
+
const quizQuestions = [
|
| 235 |
+
{ answer: 'C' },
|
| 236 |
+
{ answer: 'B' },
|
| 237 |
+
{ answer: 'D' }
|
| 238 |
+
];
|
| 239 |
+
|
| 240 |
+
// --- DOM 元素 ---
|
| 241 |
+
const storyView = document.getElementById('story-view');
|
| 242 |
+
const startQuizButton = document.getElementById('start-quiz-button');
|
| 243 |
+
const quizView = document.getElementById('quiz-view');
|
| 244 |
+
const submitQuizButton = document.getElementById('submit-quiz-button');
|
| 245 |
+
const rereadStoryButton = document.getElementById('reread-story-button');
|
| 246 |
+
const quizFeedback = document.getElementById('quiz-feedback');
|
| 247 |
+
const gameView = document.getElementById('game-view');
|
| 248 |
+
const applicationView = document.getElementById('application-view');
|
| 249 |
+
const squareDisplay = document.getElementById('square-display');
|
| 250 |
+
const targetAreaText = document.getElementById('target-area-text');
|
| 251 |
+
const targetSqrtText = document.getElementById('target-sqrt-text');
|
| 252 |
+
const guessSqrtText = document.getElementById('guess-sqrt-text');
|
| 253 |
+
const currentValue = document.getElementById('current-value');
|
| 254 |
+
const currentSquaredValue = document.getElementById('current-squared-value');
|
| 255 |
+
const valueSlider = document.getElementById('value-slider');
|
| 256 |
+
const checkButton = document.getElementById('check-button');
|
| 257 |
+
const feedbackBox = document.getElementById('feedback-box');
|
| 258 |
+
const nextLevelButton = document.getElementById('next-level-button');
|
| 259 |
+
|
| 260 |
+
// --- 遊戲狀態 ---
|
| 261 |
+
let sliderAnimationId = null;
|
| 262 |
+
let sliderDirection = 1;
|
| 263 |
+
|
| 264 |
+
// --- 核心函式 ---
|
| 265 |
+
function initLevel(levelIndex) {
|
| 266 |
+
const level = levels[levelIndex];
|
| 267 |
+
|
| 268 |
+
if (sliderAnimationId) {
|
| 269 |
+
cancelAnimationFrame(sliderAnimationId);
|
| 270 |
+
sliderAnimationId = null;
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
targetAreaText.textContent = level.target;
|
| 274 |
+
targetSqrtText.innerHTML = `√${level.target}`;
|
| 275 |
+
guessSqrtText.innerHTML = `√${level.target}`;
|
| 276 |
+
|
| 277 |
+
valueSlider.min = level.min;
|
| 278 |
+
valueSlider.max = level.max;
|
| 279 |
+
valueSlider.value = (level.min + level.max) / 2;
|
| 280 |
+
valueSlider.step = 0.01;
|
| 281 |
+
|
| 282 |
+
updateSliderDisplay();
|
| 283 |
+
feedbackBox.textContent = '';
|
| 284 |
+
feedbackBox.className = 'text-center text-xl font-semibold min-h-[32px]';
|
| 285 |
+
checkButton.disabled = false;
|
| 286 |
+
nextLevelButton.classList.add('hidden');
|
| 287 |
+
squareDisplay.classList.remove('success-flash');
|
| 288 |
+
|
| 289 |
+
if (level.type === 'manual') {
|
| 290 |
+
valueSlider.style.pointerEvents = 'auto';
|
| 291 |
+
checkButton.textContent = '確定答案';
|
| 292 |
+
} else {
|
| 293 |
+
valueSlider.style.pointerEvents = 'none';
|
| 294 |
+
checkButton.textContent = '按下鎖定!';
|
| 295 |
+
sliderDirection = 1;
|
| 296 |
+
startSliderAnimation();
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
if (levelIndex === levels.length - 1) {
|
| 300 |
+
nextLevelButton.textContent = '查看生活應用';
|
| 301 |
+
} else {
|
| 302 |
+
nextLevelButton.textContent = '挑戰下一關';
|
| 303 |
+
}
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
function startSliderAnimation() {
|
| 307 |
+
const level = levels[currentLevel];
|
| 308 |
+
let val = parseFloat(valueSlider.value);
|
| 309 |
+
val += sliderDirection * level.speed;
|
| 310 |
+
if (val >= level.max || val <= level.min) {
|
| 311 |
+
sliderDirection *= -1;
|
| 312 |
+
val = Math.max(level.min, Math.min(level.max, val));
|
| 313 |
+
}
|
| 314 |
+
valueSlider.value = val;
|
| 315 |
+
updateSliderDisplay();
|
| 316 |
+
sliderAnimationId = requestAnimationFrame(startSliderAnimation);
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
function updateSliderDisplay() {
|
| 320 |
+
const val = parseFloat(valueSlider.value);
|
| 321 |
+
const squaredVal = val * val;
|
| 322 |
+
currentValue.textContent = val.toFixed(2);
|
| 323 |
+
currentSquaredValue.textContent = squaredVal.toFixed(2);
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
function checkAnswer() {
|
| 327 |
+
const level = levels[currentLevel];
|
| 328 |
+
|
| 329 |
+
if (level.type === 'auto' && sliderAnimationId) {
|
| 330 |
+
cancelAnimationFrame(sliderAnimationId);
|
| 331 |
+
sliderAnimationId = null;
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
checkButton.disabled = true;
|
| 335 |
+
|
| 336 |
+
const val = parseFloat(valueSlider.value);
|
| 337 |
+
const squaredVal = val * val;
|
| 338 |
+
const difference = Math.abs(squaredVal - level.target);
|
| 339 |
+
feedbackBox.textContent = '';
|
| 340 |
+
|
| 341 |
+
if (difference <= level.tolerance) {
|
| 342 |
+
feedbackBox.textContent = '太棒了!你找到了!';
|
| 343 |
+
feedbackBox.className = 'text-center text-xl font-semibold text-green-600';
|
| 344 |
+
nextLevelButton.classList.remove('hidden');
|
| 345 |
+
squareDisplay.classList.add('success-flash');
|
| 346 |
+
} else {
|
| 347 |
+
const hint = squaredVal < level.target ? '太小了' : '太大了';
|
| 348 |
+
feedbackBox.textContent = `喔喔!${hint},再試一次!`;
|
| 349 |
+
feedbackBox.className = 'text-center text-xl font-semibold text-red-600';
|
| 350 |
+
|
| 351 |
+
if (level.type === 'auto') {
|
| 352 |
+
setTimeout(() => {
|
| 353 |
+
initLevel(currentLevel);
|
| 354 |
+
}, 2000);
|
| 355 |
+
} else {
|
| 356 |
+
checkButton.disabled = false;
|
| 357 |
+
}
|
| 358 |
+
}
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
function loadNext() {
|
| 362 |
+
currentLevel++;
|
| 363 |
+
if (currentLevel < levels.length) {
|
| 364 |
+
initLevel(currentLevel);
|
| 365 |
+
} else {
|
| 366 |
+
gameView.classList.add('hidden');
|
| 367 |
+
applicationView.classList.remove('hidden');
|
| 368 |
+
}
|
| 369 |
+
}
|
| 370 |
+
|
| 371 |
+
function checkQuiz() {
|
| 372 |
+
let allCorrect = true;
|
| 373 |
+
quizFeedback.textContent = '';
|
| 374 |
+
quizFeedback.classList.remove('text-red-500', 'text-green-600');
|
| 375 |
+
|
| 376 |
+
quizQuestions.forEach((question, index) => {
|
| 377 |
+
const container = document.getElementById(`question-container-${index}`);
|
| 378 |
+
const selected = document.querySelector(`input[name="q${index}"]:checked`);
|
| 379 |
+
|
| 380 |
+
container.classList.remove('incorrect');
|
| 381 |
+
|
| 382 |
+
if (!selected || selected.value !== question.answer) {
|
| 383 |
+
allCorrect = false;
|
| 384 |
+
container.classList.add('incorrect');
|
| 385 |
+
}
|
| 386 |
+
});
|
| 387 |
+
|
| 388 |
+
if (allCorrect) {
|
| 389 |
+
quizFeedback.textContent = '太棒了!你很用心在看故事喔!';
|
| 390 |
+
quizFeedback.classList.add('text-green-600');
|
| 391 |
+
submitQuizButton.disabled = true;
|
| 392 |
+
rereadStoryButton.disabled = true;
|
| 393 |
+
setTimeout(() => {
|
| 394 |
+
quizView.classList.add('hidden');
|
| 395 |
+
gameView.classList.remove('hidden');
|
| 396 |
+
}, 2000);
|
| 397 |
+
} else {
|
| 398 |
+
quizFeedback.textContent = '有題目答錯了,再檢查看看或重讀一次故事吧!';
|
| 399 |
+
quizFeedback.classList.add('text-red-500');
|
| 400 |
+
}
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
// --- 事件監聽 ---
|
| 404 |
+
startQuizButton.addEventListener('click', () => {
|
| 405 |
+
storyView.classList.add('hidden');
|
| 406 |
+
quizView.classList.remove('hidden');
|
| 407 |
+
submitQuizButton.disabled = false;
|
| 408 |
+
rereadStoryButton.disabled = false;
|
| 409 |
+
quizFeedback.textContent = '';
|
| 410 |
+
quizQuestions.forEach((_, index) => {
|
| 411 |
+
document.getElementById(`question-container-${index}`).classList.remove('incorrect');
|
| 412 |
+
});
|
| 413 |
+
});
|
| 414 |
+
|
| 415 |
+
rereadStoryButton.addEventListener('click', () => {
|
| 416 |
+
quizView.classList.add('hidden');
|
| 417 |
+
storyView.classList.remove('hidden');
|
| 418 |
+
});
|
| 419 |
+
|
| 420 |
+
submitQuizButton.addEventListener('click', checkQuiz);
|
| 421 |
+
valueSlider.addEventListener('input', updateSliderDisplay);
|
| 422 |
+
checkButton.addEventListener('click', checkAnswer);
|
| 423 |
+
nextLevelButton.addEventListener('click', loadNext);
|
| 424 |
+
|
| 425 |
+
// --- 初始啟動 ---
|
| 426 |
+
initLevel(currentLevel);
|
| 427 |
+
});
|
| 428 |
+
</script>
|
| 429 |
+
</body>
|
| 430 |
+
</html>
|
| 431 |
+
|
function.html
CHANGED
|
@@ -17,9 +17,16 @@
|
|
| 17 |
<script src="https://cdn.tailwindcss.com"></script>
|
| 18 |
|
| 19 |
<style>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
body {
|
| 21 |
font-family: 'Noto Sans TC', sans-serif;
|
| 22 |
-
background-color: #
|
| 23 |
/* Slate 900 */
|
| 24 |
color: white;
|
| 25 |
overflow: hidden;
|
|
@@ -42,7 +49,7 @@
|
|
| 42 |
width: 100vw;
|
| 43 |
height: 100vh;
|
| 44 |
height: 100dvh;
|
| 45 |
-
background: rgba(
|
| 46 |
/* Semi-transparent Slate 900 */
|
| 47 |
z-index: -1;
|
| 48 |
}
|
|
@@ -67,11 +74,12 @@
|
|
| 67 |
|
| 68 |
/* Tech UI Styling */
|
| 69 |
.glass-panel {
|
| 70 |
-
background:
|
| 71 |
backdrop-filter: blur(12px);
|
| 72 |
-webkit-backdrop-filter: blur(12px);
|
| 73 |
-
border: 1px solid
|
| 74 |
-
|
|
|
|
| 75 |
}
|
| 76 |
|
| 77 |
.font-tech {
|
|
@@ -1642,7 +1650,7 @@
|
|
| 1642 |
const time = Date.now() / 1000;
|
| 1643 |
|
| 1644 |
// Clear with dark blue tint
|
| 1645 |
-
ctx.fillStyle = '#
|
| 1646 |
ctx.fillRect(0, 0, width, height);
|
| 1647 |
|
| 1648 |
// Rotating Grid
|
|
@@ -1959,7 +1967,7 @@
|
|
| 1959 |
const gy = isDesktop ? height * 0.2 : height * 0.1;
|
| 1960 |
|
| 1961 |
// BG - Darker background for graph area to make it pop against the global image
|
| 1962 |
-
ctx.fillStyle = 'rgba(
|
| 1963 |
ctx.fillRect(gx, gy, gw, gh);
|
| 1964 |
ctx.strokeStyle = '#475569';
|
| 1965 |
ctx.strokeRect(gx, gy, gw, gh);
|
|
|
|
| 17 |
<script src="https://cdn.tailwindcss.com"></script>
|
| 18 |
|
| 19 |
<style>
|
| 20 |
+
:root {
|
| 21 |
+
--glass-bg: rgba(8, 51, 68, 0.85);
|
| 22 |
+
/* Dark Cyan */
|
| 23 |
+
--glass-border: rgba(6, 182, 212, 0.3);
|
| 24 |
+
/* Cyan Border */
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
body {
|
| 28 |
font-family: 'Noto Sans TC', sans-serif;
|
| 29 |
+
background-color: #051015;
|
| 30 |
/* Slate 900 */
|
| 31 |
color: white;
|
| 32 |
overflow: hidden;
|
|
|
|
| 49 |
width: 100vw;
|
| 50 |
height: 100vh;
|
| 51 |
height: 100dvh;
|
| 52 |
+
background: rgba(5, 16, 21, 0.6);
|
| 53 |
/* Semi-transparent Slate 900 */
|
| 54 |
z-index: -1;
|
| 55 |
}
|
|
|
|
| 74 |
|
| 75 |
/* Tech UI Styling */
|
| 76 |
.glass-panel {
|
| 77 |
+
background: var(--glass-bg);
|
| 78 |
backdrop-filter: blur(12px);
|
| 79 |
-webkit-backdrop-filter: blur(12px);
|
| 80 |
+
border: 1px solid var(--glass-border);
|
| 81 |
+
border-radius: 16px;
|
| 82 |
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.5), 0 2px 4px -1px rgba(0, 0, 0, 0.5);
|
| 83 |
}
|
| 84 |
|
| 85 |
.font-tech {
|
|
|
|
| 1650 |
const time = Date.now() / 1000;
|
| 1651 |
|
| 1652 |
// Clear with dark blue tint
|
| 1653 |
+
ctx.fillStyle = '#051015';
|
| 1654 |
ctx.fillRect(0, 0, width, height);
|
| 1655 |
|
| 1656 |
// Rotating Grid
|
|
|
|
| 1967 |
const gy = isDesktop ? height * 0.2 : height * 0.1;
|
| 1968 |
|
| 1969 |
// BG - Darker background for graph area to make it pop against the global image
|
| 1970 |
+
ctx.fillStyle = 'rgba(5, 16, 21, 0.85)';
|
| 1971 |
ctx.fillRect(gx, gy, gw, gh);
|
| 1972 |
ctx.strokeStyle = '#475569';
|
| 1973 |
ctx.strokeRect(gx, gy, gw, gh);
|
index.html
CHANGED
|
@@ -268,7 +268,8 @@
|
|
| 268 |
</div>
|
| 269 |
|
| 270 |
<!-- Pin 3: Congruence District (Bottom-Right) -->
|
| 271 |
-
<div id="pin-congruence" class="map-pin group
|
|
|
|
| 272 |
<div class="pin-marker border-fuchsia-500 group-hover:scale-110"></div>
|
| 273 |
<div class="pin-label border-fuchsia-500 text-fuchsia-400">
|
| 274 |
<div class="font-bold text-xl">全等重案組</div>
|
|
@@ -277,11 +278,11 @@
|
|
| 277 |
</div>
|
| 278 |
|
| 279 |
<!-- Pin 4: Skyscraper (Top-Right-ish) -->
|
| 280 |
-
<div id="pin-parallel" class="map-pin group
|
| 281 |
<div class="pin-marker border-green-500 group-hover:scale-110"></div>
|
| 282 |
<div class="pin-label border-green-500 text-green-400">
|
| 283 |
-
<div class="font-bold text-xl">
|
| 284 |
-
<div class="text-xs text-white opacity-80">
|
| 285 |
</div>
|
| 286 |
</div>
|
| 287 |
|
|
@@ -379,6 +380,8 @@
|
|
| 379 |
|
| 380 |
updatePin('#pin-function', 'math_city_score_function');
|
| 381 |
updatePin('#pin-sequence', 'math_city_score_sequence');
|
|
|
|
|
|
|
| 382 |
}
|
| 383 |
loadScores();
|
| 384 |
|
|
@@ -387,6 +390,8 @@
|
|
| 387 |
if (confirm('確定要重置所有分數與獎盃嗎?\nAre you sure you want to reset all scores?')) {
|
| 388 |
localStorage.removeItem('math_city_score_function');
|
| 389 |
localStorage.removeItem('math_city_score_sequence');
|
|
|
|
|
|
|
| 390 |
location.reload();
|
| 391 |
}
|
| 392 |
};
|
|
|
|
| 268 |
</div>
|
| 269 |
|
| 270 |
<!-- Pin 3: Congruence District (Bottom-Right) -->
|
| 271 |
+
<div id="pin-congruence" class="map-pin group"
|
| 272 |
+
onclick="triggerBeamNavigation(this, 'congruence_detective.html')">
|
| 273 |
<div class="pin-marker border-fuchsia-500 group-hover:scale-110"></div>
|
| 274 |
<div class="pin-label border-fuchsia-500 text-fuchsia-400">
|
| 275 |
<div class="font-bold text-xl">全等重案組</div>
|
|
|
|
| 278 |
</div>
|
| 279 |
|
| 280 |
<!-- Pin 4: Skyscraper (Top-Right-ish) -->
|
| 281 |
+
<div id="pin-parallel" class="map-pin group" onclick="window.location.href='skyscraper.html'">
|
| 282 |
<div class="pin-marker border-green-500 group-hover:scale-110"></div>
|
| 283 |
<div class="pin-label border-green-500 text-green-400">
|
| 284 |
+
<div class="font-bold text-xl">鋼鐵輸送帶</div>
|
| 285 |
+
<div class="text-xs text-white opacity-80">Steel Conveyor</div>
|
| 286 |
</div>
|
| 287 |
</div>
|
| 288 |
|
|
|
|
| 380 |
|
| 381 |
updatePin('#pin-function', 'math_city_score_function');
|
| 382 |
updatePin('#pin-sequence', 'math_city_score_sequence');
|
| 383 |
+
updatePin('#pin-congruence', 'math_city_score_congruence');
|
| 384 |
+
updatePin('#pin-parallel', 'math_city_score_parallel');
|
| 385 |
}
|
| 386 |
loadScores();
|
| 387 |
|
|
|
|
| 390 |
if (confirm('確定要重置所有分數與獎盃嗎?\nAre you sure you want to reset all scores?')) {
|
| 391 |
localStorage.removeItem('math_city_score_function');
|
| 392 |
localStorage.removeItem('math_city_score_sequence');
|
| 393 |
+
localStorage.removeItem('math_city_score_congruence');
|
| 394 |
+
localStorage.removeItem('math_city_score_parallel');
|
| 395 |
location.reload();
|
| 396 |
}
|
| 397 |
};
|
sequence.html
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
skills.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Skills & Best Practices - Math City Project
|
| 2 |
+
|
| 3 |
+
此文件記錄本專案開發過程中的技術規範、設計模式、教學設計哲學與除錯經驗,供後續開發參考。
|
| 4 |
+
|
| 5 |
+
## 1. 技術架構與規範 (Tech Stack & Guidelines)
|
| 6 |
+
|
| 7 |
+
### 核心技術
|
| 8 |
+
* **核心**: HTML5 Canvas + Vanilla JS (ES6+)。
|
| 9 |
+
* **樣式**: Tailwind CSS (CDN read-only 模式) + Custom CSS (for animations/glassmorphism)。
|
| 10 |
+
* **字體**: 'Orbitron' (數字符號/標題), 'Noto Sans TC' (中文內文)。
|
| 11 |
+
* **建置**: 無須 Build Tool (No Webpack/Vite),保持單檔可執行 (`.html`),方便老師分享與部署。
|
| 12 |
+
|
| 13 |
+
### 程式碼規範
|
| 14 |
+
* **語言**: 註解與文件說明嚴格使用**繁體中文**,變數與函數名稱使用**英文**。
|
| 15 |
+
* **檔案結構**: 保持扁平,html 檔包含 CSS/JS 為主 (方便單檔攜帶),通用素材放 `Assets/`。
|
| 16 |
+
* **狀態管理**: 使用整數常數定義狀態機 (`const STATE = { INIT: 0, PLAYING: 1 ... }`),避免字串混淆。
|
| 17 |
+
|
| 18 |
+
## 2. 視覺與互動設計 (UI/UX Patterns)
|
| 19 |
+
|
| 20 |
+
### 視覺風格:Glassmorphism (玻璃擬態)
|
| 21 |
+
* 背景使用 `backdrop-filter: blur(12px)` 搭配半透明深色背景 (`bg-slate-900/90`)。
|
| 22 |
+
* 邊框使用亮色且低透明度 (`border-cyan-500/30`) 營造科技質感。
|
| 23 |
+
* 善用 `box-shadow` 與 `drop-shadow` 營造霓虹發光效果。
|
| 24 |
+
|
| 25 |
+
### Canvas 渲染優化
|
| 26 |
+
* **High-DPI 支援**:
|
| 27 |
+
* 在 Retina 螢幕上,務必處理 `devicePixelRatio`。
|
| 28 |
+
* Canvas 屬性寬高 = CSS 寬高 * dpr。
|
| 29 |
+
* Context 縮放: `ctx.scale(dpr, dpr)`。
|
| 30 |
+
```javascript
|
| 31 |
+
const dpr = window.devicePixelRatio || 1;
|
| 32 |
+
canvas.width = width * dpr;
|
| 33 |
+
canvas.height = height * dpr;
|
| 34 |
+
canvas.style.width = width + 'px';
|
| 35 |
+
canvas.style.height = height + 'px';
|
| 36 |
+
ctx.scale(dpr, dpr);
|
| 37 |
+
```
|
| 38 |
+
|
| 39 |
+
### 行動裝置適配 (Mobile Adaptation)
|
| 40 |
+
* **禁止捲動/縮放**:
|
| 41 |
+
* CSS: `touch-action: none; user-select: none; -webkit-user-select: none;`
|
| 42 |
+
* JS: 監聽 `touchstart` / `touchmove` 並執行 `e.preventDefault()` (需加 `{ passive: false }`)。
|
| 43 |
+
* **虛擬鍵盤 (Virtual Keypad)**:
|
| 44 |
+
* iOS/Android 預設軟鍵盤會推擠版面,破壞全螢幕體驗。
|
| 45 |
+
* **解決方案**: 自製 DOM 虛擬鍵盤,支援拖曳 (Draggable) 與自動定位。
|
| 46 |
+
* **點擊穿透處理**: 鍵盤點擊事件需 `e.stopPropagation()`。
|
| 47 |
+
|
| 48 |
+
## 3. 遊戲邏輯與物理 (Game Mechanics)
|
| 49 |
+
|
| 50 |
+
### 物理引擎 (Physics)
|
| 51 |
+
* **Coyote Time (土狼時間)**:
|
| 52 |
+
* 在玩家離開平台的一小段時間內 (例如 0.13秒) 仍允許跳躍,增加操作手感容錯率。
|
| 53 |
+
* 實作: 設置 `coyoteTimer`,只要在地上就重置,離地後遞減,大於 0 時仍視為可跳躍。
|
| 54 |
+
* **時間步長 (Time Step)**:
|
| 55 |
+
* 目前使用簡易的 `requestAnimationFrame` 迴圈。若需更精準物理,考慮導入 Delta Time (`dt`)。
|
| 56 |
+
|
| 57 |
+
### 遊戲反饋 (Juice & Feedback)
|
| 58 |
+
* **動畫選擇**:
|
| 59 |
+
* 錯誤提示避免使用滑稽的 `bounce`,改用嚴肅且有質感的 `pulse` (呼吸燈) + 紅色邊框。
|
| 60 |
+
* **音效合成 (Web Audio API)**:
|
| 61 |
+
* 不依賴外部 mp3 檔案,使用 `OscillatorNode` 即時合成音效 (Sine, Square, Sawtooth)。
|
| 62 |
+
* 優點:無載入時間、檔案極小、可動態改變頻率 (Pitch Bend)。
|
| 63 |
+
* **注意**: 必須使用者互動 (Click/Touch) 後才能 `resume()` AudioContext。
|
| 64 |
+
|
| 65 |
+
## 4. 數學教學設計 (Pedagogy Design)
|
| 66 |
+
|
| 67 |
+
### 鷹架理論 (Scaffolding)
|
| 68 |
+
* **不僅僅是給答案**: 錯誤訊息應轉化為引導問題。
|
| 69 |
+
* *Bad*: "答錯了!"
|
| 70 |
+
* *Good*: "如果 x=2, y=11, 但公式是 y=2x+b, 所以 b 是多少?"
|
| 71 |
+
* **變數分離**:
|
| 72 |
+
* 在解 $y=ax+b$ 時,先令 $x=0$ 解 $b$,再令 $x=1$ 解 $a$,避免同時處理雙變數的認知負荷。
|
| 73 |
+
|
| 74 |
+
### 視覺化連結 (Visual-Algebra Connection)
|
| 75 |
+
* **多重表徵連結**: 當計算出代數答案 (例如 y=35) 時,程式應同步強調圖表上的對應幾何點 (10, 35),強化「數形合一」的概念。
|
| 76 |
+
|
| 77 |
+
### 正向回饋 (Positive Feedback)
|
| 78 |
+
* 避免挫折感,將艱澀的計算任務後的獎勵極大化 (例如:華麗的煙火、Phase 6 的節奏遊戲),讓大腦獲得多巴胺釋放。
|
| 79 |
+
|
| 80 |
+
## 5. Agent 開發工作流 (Agent Workflow)
|
| 81 |
+
|
| 82 |
+
### 任務邊界 (Task Boundary)
|
| 83 |
+
* 使用 `task_boundary` 明確區分 **PLANNING** (規劃)、**EXECUTION** (執行) 與 **VERIFICATION** (驗證) 階段。
|
| 84 |
+
* **Context Management**: 保持對話專注,一個 Task 解決一個具體問題。
|
| 85 |
+
|
| 86 |
+
### Artifacts 使用
|
| 87 |
+
* **Implementation Plan**: 修改複雜邏輯前,先撰寫 `implementation_plan.md` 讓使用者確認設計思路。
|
| 88 |
+
* **Walkthrough**: 完成功能後,建立 `walkthrough.md` 展示成果與測試截圖。
|
| 89 |
+
|
| 90 |
+
## 6. 已知問題與解決 (Troubleshooting Log)
|
| 91 |
+
|
| 92 |
+
* **問題**: 平台消失後的透明牆導致玩家卡住。
|
| 93 |
+
* **解法**: 物理碰撞檢測時,需過濾掉 `active: false` 的平台物件。
|
| 94 |
+
* **問���**: Home 鍵在全螢幕模式下難以點擊。
|
| 95 |
+
* **解法**: 確保 UI 層級 (`z-index`) 正確,並添加 `pointer-events-auto` 到按鈕本身,而容器保持 `pointer-events-none`。
|
| 96 |
+
* **問題**: 數列峽谷中,只有前兩項有提示,後續太難。
|
| 97 |
+
* **解法**: 設計意圖就是強制計算。但為了公平,統一了所有平台的視覺 (碎石紋理),避免玩家嘗試「辨色」而非「計算」。
|
| 98 |
+
|
| 99 |
+
---
|
| 100 |
+
*Created by Antigravity Team for Project 11402*
|
skyscraper.html
ADDED
|
@@ -0,0 +1,1005 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="zh-TW">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
| 7 |
+
<title>鋼鐵輸送帶 - Steel Conveyor</title>
|
| 8 |
+
<link
|
| 9 |
+
href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Noto+Sans+TC:wght@400;700;900&family=JetBrains+Mono:wght@400;700&display=swap"
|
| 10 |
+
rel="stylesheet">
|
| 11 |
+
<style>
|
| 12 |
+
:root {
|
| 13 |
+
--main: #22c55e;
|
| 14 |
+
--accent: #f97316;
|
| 15 |
+
--cyan: #06b6d4;
|
| 16 |
+
--amber: #fbbf24;
|
| 17 |
+
--fail: #ef4444;
|
| 18 |
+
--bg: #071a0e;
|
| 19 |
+
--glass: rgba(7, 26, 14, .88);
|
| 20 |
+
--glass-b: rgba(34, 197, 94, .2)
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
* {
|
| 24 |
+
margin: 0;
|
| 25 |
+
padding: 0;
|
| 26 |
+
box-sizing: border-box
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
.hidden {
|
| 30 |
+
display: none !important
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
body {
|
| 34 |
+
font-family: 'Noto Sans TC', sans-serif;
|
| 35 |
+
background: var(--bg);
|
| 36 |
+
color: #fff;
|
| 37 |
+
overflow: hidden;
|
| 38 |
+
touch-action: none;
|
| 39 |
+
user-select: none;
|
| 40 |
+
-webkit-user-select: none;
|
| 41 |
+
width: 100vw;
|
| 42 |
+
height: 100dvh
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
canvas {
|
| 46 |
+
display: block;
|
| 47 |
+
width: 100%;
|
| 48 |
+
height: 100%;
|
| 49 |
+
cursor: crosshair
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
.glass {
|
| 53 |
+
background: var(--glass);
|
| 54 |
+
backdrop-filter: blur(16px);
|
| 55 |
+
-webkit-backdrop-filter: blur(16px);
|
| 56 |
+
border: 1px solid var(--glass-b);
|
| 57 |
+
box-shadow: 0 8px 32px rgba(0, 0, 0, .5)
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
.ft {
|
| 61 |
+
font-family: 'Orbitron', sans-serif
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
.fm {
|
| 65 |
+
font-family: 'JetBrains Mono', monospace
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
.glow {
|
| 69 |
+
text-shadow: 0 0 8px rgba(34, 197, 94, .6)
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
.lbl {
|
| 73 |
+
font-size: 11px;
|
| 74 |
+
color: var(--cyan);
|
| 75 |
+
text-transform: uppercase;
|
| 76 |
+
letter-spacing: 1.5px;
|
| 77 |
+
font-family: 'JetBrains Mono', monospace
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
#hud {
|
| 81 |
+
position: absolute;
|
| 82 |
+
top: 0;
|
| 83 |
+
left: 0;
|
| 84 |
+
width: 100%;
|
| 85 |
+
padding: 12px 16px;
|
| 86 |
+
display: flex;
|
| 87 |
+
justify-content: space-between;
|
| 88 |
+
align-items: flex-start;
|
| 89 |
+
pointer-events: none;
|
| 90 |
+
z-index: 20
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
#hud>* {
|
| 94 |
+
pointer-events: auto
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
.hud-box {
|
| 98 |
+
padding: 10px 14px;
|
| 99 |
+
border-radius: 12px;
|
| 100 |
+
min-width: 100px
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
#bottom-panel {
|
| 104 |
+
position: absolute;
|
| 105 |
+
bottom: 16px;
|
| 106 |
+
left: 50%;
|
| 107 |
+
transform: translateX(-50%);
|
| 108 |
+
width: 92%;
|
| 109 |
+
max-width: 520px;
|
| 110 |
+
z-index: 30
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
.abtn {
|
| 114 |
+
width: 100%;
|
| 115 |
+
padding: 14px 24px;
|
| 116 |
+
border: none;
|
| 117 |
+
border-radius: 14px;
|
| 118 |
+
font-family: 'Orbitron';
|
| 119 |
+
font-size: 16px;
|
| 120 |
+
font-weight: 700;
|
| 121 |
+
letter-spacing: 2px;
|
| 122 |
+
cursor: pointer;
|
| 123 |
+
transition: all .2s;
|
| 124 |
+
color: #fff
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
.abtn:active {
|
| 128 |
+
transform: scale(.96)
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
.abtn:disabled {
|
| 132 |
+
opacity: .4;
|
| 133 |
+
cursor: not-allowed
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
.btn-go {
|
| 137 |
+
background: linear-gradient(135deg, #22c55e, #16a34a);
|
| 138 |
+
box-shadow: 0 4px 20px rgba(34, 197, 94, .35)
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
.btn-go:hover:not(:disabled) {
|
| 142 |
+
box-shadow: 0 6px 30px rgba(34, 197, 94, .5);
|
| 143 |
+
transform: translateY(-1px)
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
.btn-p,
|
| 147 |
+
.btn-np {
|
| 148 |
+
flex: 1;
|
| 149 |
+
padding: 14px 16px;
|
| 150 |
+
border: none;
|
| 151 |
+
border-radius: 14px;
|
| 152 |
+
font-family: 'Orbitron';
|
| 153 |
+
font-size: 14px;
|
| 154 |
+
font-weight: 700;
|
| 155 |
+
letter-spacing: 1px;
|
| 156 |
+
cursor: pointer;
|
| 157 |
+
transition: all .2s;
|
| 158 |
+
color: #fff
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
.btn-p:active,
|
| 162 |
+
.btn-np:active {
|
| 163 |
+
transform: scale(.96)
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
.btn-p {
|
| 167 |
+
background: linear-gradient(135deg, #22c55e, #16a34a);
|
| 168 |
+
box-shadow: 0 4px 16px rgba(34, 197, 94, .3)
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
.btn-np {
|
| 172 |
+
background: linear-gradient(135deg, #ef4444, #dc2626);
|
| 173 |
+
box-shadow: 0 4px 16px rgba(239, 68, 68, .3)
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
.toast {
|
| 177 |
+
position: fixed;
|
| 178 |
+
top: 50%;
|
| 179 |
+
left: 50%;
|
| 180 |
+
transform: translate(-50%, -50%);
|
| 181 |
+
padding: 16px 32px;
|
| 182 |
+
border-radius: 16px;
|
| 183 |
+
font-size: 18px;
|
| 184 |
+
font-weight: 700;
|
| 185 |
+
pointer-events: none;
|
| 186 |
+
z-index: 100;
|
| 187 |
+
animation: ti .3s ease, to .4s 1.2s ease forwards
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
@keyframes ti {
|
| 191 |
+
from {
|
| 192 |
+
opacity: 0;
|
| 193 |
+
transform: translate(-50%, -50%) scale(.7)
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
to {
|
| 197 |
+
opacity: 1;
|
| 198 |
+
transform: translate(-50%, -50%) scale(1)
|
| 199 |
+
}
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
@keyframes to {
|
| 203 |
+
to {
|
| 204 |
+
opacity: 0;
|
| 205 |
+
transform: translate(-50%, -60%) scale(.9)
|
| 206 |
+
}
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
.tok {
|
| 210 |
+
background: rgba(34, 197, 94, .2);
|
| 211 |
+
border: 2px solid #22c55e;
|
| 212 |
+
color: #86efac;
|
| 213 |
+
text-shadow: 0 0 12px rgba(34, 197, 94, .5)
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
.tng {
|
| 217 |
+
background: rgba(239, 68, 68, .2);
|
| 218 |
+
border: 2px solid #ef4444;
|
| 219 |
+
color: #fca5a5;
|
| 220 |
+
text-shadow: 0 0 12px rgba(239, 68, 68, .5)
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
.ov {
|
| 224 |
+
position: absolute;
|
| 225 |
+
inset: 0;
|
| 226 |
+
display: flex;
|
| 227 |
+
flex-direction: column;
|
| 228 |
+
align-items: center;
|
| 229 |
+
justify-content: center;
|
| 230 |
+
z-index: 50;
|
| 231 |
+
padding: 24px;
|
| 232 |
+
text-align: center
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
#start-screen {
|
| 236 |
+
background: radial-gradient(ellipse at center, rgba(7, 26, 14, .94), #071a0e)
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
#brief-screen {
|
| 240 |
+
background: radial-gradient(ellipse at center, rgba(7, 26, 14, .94), #071a0e)
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
#gameover-screen {
|
| 244 |
+
background: rgba(0, 0, 0, .92)
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
#summary-screen {
|
| 248 |
+
background: rgba(0, 0, 0, .92);
|
| 249 |
+
overflow-y: auto
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
#hint-panel {
|
| 253 |
+
position: absolute;
|
| 254 |
+
top: 80px;
|
| 255 |
+
left: 50%;
|
| 256 |
+
transform: translateX(-50%);
|
| 257 |
+
max-width: 440px;
|
| 258 |
+
width: 90%;
|
| 259 |
+
z-index: 25;
|
| 260 |
+
pointer-events: none;
|
| 261 |
+
transition: opacity .4s
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
/* Tutorial/Phase modal — centered with flex on parent */
|
| 265 |
+
#tut-modal {
|
| 266 |
+
position: fixed;
|
| 267 |
+
inset: 0;
|
| 268 |
+
display: flex;
|
| 269 |
+
align-items: center;
|
| 270 |
+
justify-content: center;
|
| 271 |
+
z-index: 80;
|
| 272 |
+
background: rgba(0, 0, 0, .65)
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
#tut-modal .tut-box {
|
| 276 |
+
background: linear-gradient(160deg, #0c1e2e, #0a1628);
|
| 277 |
+
border: 1.5px solid rgba(99, 179, 237, .35);
|
| 278 |
+
border-radius: 20px;
|
| 279 |
+
padding: 32px 28px;
|
| 280 |
+
max-width: 420px;
|
| 281 |
+
width: 90%;
|
| 282 |
+
text-align: center;
|
| 283 |
+
box-shadow: 0 0 50px rgba(99, 179, 237, .12), 0 20px 40px rgba(0, 0, 0, .5);
|
| 284 |
+
animation: modalIn .35s ease
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
@keyframes modalIn {
|
| 288 |
+
from {
|
| 289 |
+
opacity: 0;
|
| 290 |
+
transform: scale(.85) translateY(20px)
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
to {
|
| 294 |
+
opacity: 1;
|
| 295 |
+
transform: scale(1) translateY(0)
|
| 296 |
+
}
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
/* Timer bar */
|
| 300 |
+
#timer-bar {
|
| 301 |
+
position: absolute;
|
| 302 |
+
top: 64px;
|
| 303 |
+
left: 50%;
|
| 304 |
+
transform: translateX(-50%);
|
| 305 |
+
width: 70%;
|
| 306 |
+
max-width: 500px;
|
| 307 |
+
height: 8px;
|
| 308 |
+
background: rgba(255, 255, 255, .08);
|
| 309 |
+
border-radius: 4px;
|
| 310 |
+
z-index: 22;
|
| 311 |
+
overflow: hidden
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
#timer-fill {
|
| 315 |
+
height: 100%;
|
| 316 |
+
background: linear-gradient(90deg, #22c55e, #4ade80);
|
| 317 |
+
border-radius: 4px;
|
| 318 |
+
transition: width .15s linear
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
@media(max-width:640px) {
|
| 322 |
+
.hud-box {
|
| 323 |
+
padding: 8px 10px;
|
| 324 |
+
min-width: 80px
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
.hud-box .ft {
|
| 328 |
+
font-size: 18px !important
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
.abtn {
|
| 332 |
+
font-size: 14px;
|
| 333 |
+
padding: 12px 16px
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
#hint-panel {
|
| 337 |
+
top: 64px
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
#summary-screen div[style*="grid-template-columns"] {
|
| 341 |
+
grid-template-columns: 1fr !important;
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
#summary-screen div[style*="grid-column:span 2"] {
|
| 345 |
+
grid-column: span 1 !important;
|
| 346 |
+
}
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
.back-btn {
|
| 350 |
+
position: absolute;
|
| 351 |
+
top: 12px;
|
| 352 |
+
right: 12px;
|
| 353 |
+
z-index: 60;
|
| 354 |
+
background: rgba(255, 255, 255, .08);
|
| 355 |
+
border: 1px solid rgba(255, 255, 255, .15);
|
| 356 |
+
color: #94a3b8;
|
| 357 |
+
padding: 8px 14px;
|
| 358 |
+
border-radius: 8px;
|
| 359 |
+
font-size: 13px;
|
| 360 |
+
cursor: pointer;
|
| 361 |
+
transition: all .2s
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
.back-btn:hover {
|
| 365 |
+
background: rgba(255, 255, 255, .14);
|
| 366 |
+
color: #fff
|
| 367 |
+
}
|
| 368 |
+
</style>
|
| 369 |
+
</head>
|
| 370 |
+
|
| 371 |
+
<body>
|
| 372 |
+
<canvas id="gameCanvas"></canvas>
|
| 373 |
+
<div id="hud" class="hidden">
|
| 374 |
+
<div class="glass hud-box">
|
| 375 |
+
<div class="lbl" id="hud-lbl">鋼材運送</div>
|
| 376 |
+
<div><span id="score-d" class="ft glow" style="font-size:22px;color:var(--main)">0</span><span
|
| 377 |
+
id="score-unit" style="font-size:12px;color:#64748b"> 噸</span></div>
|
| 378 |
+
</div>
|
| 379 |
+
<div class="glass hud-box" style="text-align:center">
|
| 380 |
+
<div class="lbl" id="phase-lbl">教學</div><span id="level-d" class="ft glow"
|
| 381 |
+
style="font-size:18px;color:var(--cyan)">1/2</span>
|
| 382 |
+
</div>
|
| 383 |
+
<div class="glass hud-box" id="combo-box">
|
| 384 |
+
<div class="lbl">連續</div><span id="combo-d" class="ft glow"
|
| 385 |
+
style="font-size:22px;color:var(--amber)">0</span>
|
| 386 |
+
</div>
|
| 387 |
+
</div>
|
| 388 |
+
<div id="timer-bar" class="hidden">
|
| 389 |
+
<div id="timer-fill" style="width:100%"></div>
|
| 390 |
+
</div>
|
| 391 |
+
<div id="hint-panel" class="hidden">
|
| 392 |
+
<div class="glass" style="padding:12px 18px;border-radius:14px;border-left:3px solid var(--cyan)">
|
| 393 |
+
<div
|
| 394 |
+
style="font-size:12px;color:var(--cyan);font-weight:700;font-family:'Orbitron';letter-spacing:1px;margin-bottom:4px">
|
| 395 |
+
MISSION HINT</div>
|
| 396 |
+
<p id="hint-text" style="font-size:15px;color:#e2e8f0;line-height:1.5"></p>
|
| 397 |
+
</div>
|
| 398 |
+
</div>
|
| 399 |
+
<div id="bottom-panel" class="hidden">
|
| 400 |
+
<div id="draw-ctl">
|
| 401 |
+
<div class="glass" style="padding:16px;border-radius:16px;margin-bottom:8px">
|
| 402 |
+
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px">
|
| 403 |
+
<span class="lbl">指定垂直距離</span>
|
| 404 |
+
<span id="tgt-d" class="fm" style="font-size:20px;font-weight:700;color:var(--accent)">d = 60</span>
|
| 405 |
+
</div>
|
| 406 |
+
<div style="font-size:13px;color:#94a3b8">調整邊 B 的角度,讓右邊的垂直距離也等於 d</div>
|
| 407 |
+
</div>
|
| 408 |
+
<button id="btn-confirm" class="abtn btn-go" onclick="game.checkAnswer()">⚡ 確認輸送帶 CONFIRM</button>
|
| 409 |
+
</div>
|
| 410 |
+
<div id="judge-ctl" class="hidden">
|
| 411 |
+
<div class="glass" style="padding:16px;border-radius:16px;margin-bottom:8px">
|
| 412 |
+
<div style="font-size:14px;color:#e2e8f0;line-height:1.6;text-align:center">觀察各測量點的<span
|
| 413 |
+
style="color:var(--cyan);font-weight:700">垂直距離</span>,<br>判斷這條輸送帶的兩邊是否<span
|
| 414 |
+
style="color:var(--main);font-weight:700">平行</span>?</div>
|
| 415 |
+
</div>
|
| 416 |
+
<div style="display:flex;gap:10px">
|
| 417 |
+
<button class="btn-p" onclick="game.judgeAnswer(true)">✓ 平行</button>
|
| 418 |
+
<button class="btn-np" onclick="game.judgeAnswer(false)">✗ 不平行</button>
|
| 419 |
+
</div>
|
| 420 |
+
</div>
|
| 421 |
+
</div>
|
| 422 |
+
<div id="start-screen" class="ov">
|
| 423 |
+
<div style="margin-bottom:32px">
|
| 424 |
+
<div style="font-size:96px;margin-bottom:12px">🏗️</div>
|
| 425 |
+
<h1 style="font-size:clamp(32px,7vw,52px);font-weight:900;color:#fff;line-height:1.2;margin-bottom:6px">
|
| 426 |
+
鋼鐵輸送帶</h1>
|
| 427 |
+
<p class="ft" style="font-size:clamp(14px,3vw,20px);color:var(--cyan);letter-spacing:4px;margin-top:4px">
|
| 428 |
+
STEEL CONVEYOR</p>
|
| 429 |
+
</div>
|
| 430 |
+
<button class="abtn btn-go" onclick="game.showBrief()"
|
| 431 |
+
style="max-width:280px;font-size:18px;padding:16px">查看任務簡報 →</button>
|
| 432 |
+
</div>
|
| 433 |
+
<div id="brief-screen" class="ov hidden">
|
| 434 |
+
<div class="glass"
|
| 435 |
+
style="padding:32px;border-radius:18px;max-width:520px;width:100%;text-align:left;margin-bottom:28px;border-color:rgba(6,182,212,.2)">
|
| 436 |
+
<h3
|
| 437 |
+
style="color:var(--cyan);font-weight:700;font-size:18px;letter-spacing:2px;border-bottom:1px solid rgba(255,255,255,.08);padding-bottom:10px;margin-bottom:18px;font-family:'Orbitron'">
|
| 438 |
+
● 任務簡報 MISSION BRIEF</h3>
|
| 439 |
+
<p style="color:#cbd5e1;font-size:20px;line-height:1.9;margin-bottom:20px">工程師,兩棟摩天大樓之間需要搭建<span
|
| 440 |
+
style="color:var(--accent);font-weight:700">輸送帶</span>來運送 H 型鋼材。輸送帶的兩條邊必須<span
|
| 441 |
+
style="color:#fff;font-weight:700">確定平行</span>,鋼材才能安全通過。</p>
|
| 442 |
+
<ul style="color:#cbd5e1;font-size:18px;line-height:2.2;list-style:none;padding:0">
|
| 443 |
+
<li style="display:flex;align-items:flex-start;gap:10px"><span
|
| 444 |
+
style="color:var(--main)">➤</span><span>利用「<span
|
| 445 |
+
style="color:#fff;font-weight:700">平行線間垂直距離處處相等</span>」的原理來搭建輸送帶</span></li>
|
| 446 |
+
<li style="display:flex;align-items:flex-start;gap:10px;margin-top:8px"><span
|
| 447 |
+
style="color:var(--accent)">➤</span><span>有時需要<span
|
| 448 |
+
style="color:var(--main)">繪製</span>第二條邊,有時需要<span
|
| 449 |
+
style="color:var(--amber)">判斷</span>是否平行,必須確定平行才能讓鋼材通過!</span></li>
|
| 450 |
+
</ul>
|
| 451 |
+
</div>
|
| 452 |
+
<button class="abtn btn-go" onclick="game.start()" style="max-width:280px;font-size:18px;padding:16px">開始搭建
|
| 453 |
+
START</button>
|
| 454 |
+
</div>
|
| 455 |
+
<!-- Tutorial / Phase complete modal -->
|
| 456 |
+
<div id="tut-modal" class="hidden">
|
| 457 |
+
<div class="tut-box">
|
| 458 |
+
<div id="tut-icon" style="font-size:40px;margin-bottom:10px">✅</div>
|
| 459 |
+
<h3 id="tut-title" style="font-size:22px;font-weight:900;color:#93c5fd;margin-bottom:10px">教學完成!</h3>
|
| 460 |
+
<p id="tut-msg"
|
| 461 |
+
style="font-size:17px;color:#cbd5e1;line-height:1.7;margin-bottom:24px;white-space:pre-line">垂直距離處處相等 =
|
| 462 |
+
平行</p>
|
| 463 |
+
<button id="tut-btn" class="abtn"
|
| 464 |
+
style="background:linear-gradient(135deg,#3b82f6,#2563eb);box-shadow:0 4px 20px rgba(59,130,246,.3);max-width:260px;margin:0 auto"
|
| 465 |
+
onclick="game.tutNext()">確認,繼續 →</button>
|
| 466 |
+
</div>
|
| 467 |
+
</div>
|
| 468 |
+
<!-- Summary screen after challenge -->
|
| 469 |
+
<div id="summary-screen" class="ov hidden"
|
| 470 |
+
style="justify-content:flex-start;padding-top:32px;padding-bottom:40px;align-items:stretch">
|
| 471 |
+
<!-- Header -->
|
| 472 |
+
<div style="text-align:center;margin-bottom:20px;padding:0 24px">
|
| 473 |
+
<div style="font-size:48px;margin-bottom:6px">🏆</div>
|
| 474 |
+
<h2
|
| 475 |
+
style="font-size:clamp(22px,5vw,32px);font-weight:900;margin-bottom:2px;background:linear-gradient(135deg,#fbbf24,#f97316);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text">
|
| 476 |
+
任務總結 REPORT</h2>
|
| 477 |
+
<p style="color:#94a3b8;font-size:12px;letter-spacing:3px;font-family:'Orbitron'">MISSION SUMMARY</p>
|
| 478 |
+
</div>
|
| 479 |
+
|
| 480 |
+
<!-- 60s Challenge + Best Record — side by side, compact -->
|
| 481 |
+
<div
|
| 482 |
+
style="display:flex;gap:16px;justify-content:center;align-items:stretch;margin-bottom:20px;padding:0 24px;flex-wrap:wrap;max-width:900px;width:100%;margin-left:auto;margin-right:auto">
|
| 483 |
+
<div class="glass"
|
| 484 |
+
style="padding:16px 24px;border-radius:14px;flex:1;min-width:180px;max-width:300px;text-align:center">
|
| 485 |
+
<div style="display:flex;align-items:center;justify-content:center;gap:8px;margin-bottom:6px">
|
| 486 |
+
<span style="font-size:20px">⏱️</span>
|
| 487 |
+
<span style="color:#94a3b8;font-size:14px;font-weight:700">60 秒挑戰</span>
|
| 488 |
+
</div>
|
| 489 |
+
<div class="ft" style="font-size:36px;font-weight:900;color:var(--amber)">
|
| 490 |
+
<span id="sum-challenge">—</span>
|
| 491 |
+
</div>
|
| 492 |
+
<div style="font-size:11px;color:#64748b;margin-top:2px">60 秒內通過的關卡數</div>
|
| 493 |
+
</div>
|
| 494 |
+
<div class="glass"
|
| 495 |
+
style="padding:16px 24px;border-radius:14px;flex:1;min-width:180px;max-width:300px;text-align:center">
|
| 496 |
+
<div style="display:flex;align-items:center;justify-content:center;gap:8px;margin-bottom:6px">
|
| 497 |
+
<span style="font-size:20px">🏅</span>
|
| 498 |
+
<span class="lbl" style="font-size:14px">最高紀錄</span>
|
| 499 |
+
</div>
|
| 500 |
+
<div class="ft" style="font-size:36px;font-weight:900;color:var(--amber)">
|
| 501 |
+
<span id="sum-best">0</span><span style="font-size:14px;color:#64748b;margin-left:4px">關</span>
|
| 502 |
+
</div>
|
| 503 |
+
</div>
|
| 504 |
+
</div>
|
| 505 |
+
|
| 506 |
+
<!-- 你學到了什麼 — WIDE container -->
|
| 507 |
+
<div class="glass"
|
| 508 |
+
style="padding:28px 32px;border-radius:18px;margin-bottom:20px;max-width:900px;width:94%;text-align:left;border-color:rgba(34,197,94,.2);margin-left:auto;margin-right:auto">
|
| 509 |
+
<h3
|
| 510 |
+
style="color:var(--main);font-weight:700;font-size:20px;letter-spacing:1.5px;margin-bottom:16px;font-family:'Orbitron';display:flex;align-items:center;gap:8px">
|
| 511 |
+
<span style="font-size:24px">📐</span> 你學到了什麼?
|
| 512 |
+
</h3>
|
| 513 |
+
|
| 514 |
+
<!-- 核心定理 — 在應用上方 -->
|
| 515 |
+
<div
|
| 516 |
+
style="background:rgba(34,197,94,.08);border:1px solid rgba(34,197,94,.2);border-radius:12px;padding:18px 20px;margin-bottom:20px">
|
| 517 |
+
<p style="color:#4ade80;font-weight:700;font-size:18px;margin-bottom:8px">⭐ 核心定理</p>
|
| 518 |
+
<p style="color:#e2e8f0;font-size:17px;line-height:1.8">若兩直線的<span
|
| 519 |
+
style="color:var(--amber);font-weight:700">垂直距離</span>處處相等,則兩直線<span
|
| 520 |
+
style="color:var(--main);font-weight:700">互相平行</span>。<br>反之,平行線之間的垂直距離<span
|
| 521 |
+
style="color:var(--cyan);font-weight:700">永遠不變</span>。</p>
|
| 522 |
+
</div>
|
| 523 |
+
|
| 524 |
+
<!-- 生活應用 — 2 欄 Grid -->
|
| 525 |
+
<h4
|
| 526 |
+
style="color:var(--cyan);font-size:16px;font-weight:700;margin-bottom:14px;letter-spacing:1px;font-family:'Orbitron'">
|
| 527 |
+
🌍 平行在生活中的應用</h4>
|
| 528 |
+
<div
|
| 529 |
+
style="display:grid;grid-template-columns:1fr 1fr;gap:14px 24px;color:#cbd5e1;font-size:16px;line-height:1.9;margin-bottom:10px">
|
| 530 |
+
<div style="background:rgba(255,255,255,.03);border-radius:10px;padding:14px 16px">
|
| 531 |
+
<span style="font-size:22px">🚂</span> <span style="color:#fff;font-weight:700">鐵軌</span><br>
|
| 532 |
+
火車的兩條鐵軌必須保持平行且等距,否則火車會脫軌。工程師用的正是「垂直距離處處相等」的原理來鋪設鐵道!
|
| 533 |
+
</div>
|
| 534 |
+
<div style="background:rgba(255,255,255,.03);border-radius:10px;padding:14px 16px">
|
| 535 |
+
<span style="font-size:22px">🏗️</span> <span style="color:#fff;font-weight:700">建築結構</span><br>
|
| 536 |
+
大樓的樓板、天花板與地板、扶手欄杆,都要保持平行才能穩固安全。
|
| 537 |
+
</div>
|
| 538 |
+
<div style="background:rgba(255,255,255,.03);border-radius:10px;padding:14px 16px">
|
| 539 |
+
<span style="font-size:22px">🎵</span> <span style="color:#fff;font-weight:700">五線譜</span><br>
|
| 540 |
+
音樂的五線譜就是五條等距的平行線,音符放在不同的平行線上代表不同的音高。
|
| 541 |
+
</div>
|
| 542 |
+
<div style="background:rgba(255,255,255,.03);border-radius:10px;padding:14px 16px">
|
| 543 |
+
<span style="font-size:22px">️</span> <span style="color:#fff;font-weight:700">電扶梯與跑道</span><br>
|
| 544 |
+
自動扶梯的兩側扶手、操場跑道的直線段等,都是平行線應用的例子。
|
| 545 |
+
</div>
|
| 546 |
+
</div>
|
| 547 |
+
|
| 548 |
+
<!-- 結語 — 放在最下方 -->
|
| 549 |
+
<div
|
| 550 |
+
style="margin-top:16px;padding-top:14px;border-top:1px solid rgba(255,255,255,.08);color:#e2e8f0;font-weight:700;font-size:17px;line-height:1.8">
|
| 551 |
+
平行的概念看似簡單,卻是幾何學的<span style="color:var(--amber);font-weight:700">基石</span>之一。除了今天學到的「垂直距離處處相等 ⇔
|
| 552 |
+
平行」,之後還會學到更多有趣的平行性質,例如<span style="color:var(--cyan);font-weight:700">同位角</span>、<span
|
| 553 |
+
style="color:var(--cyan);font-weight:700">內錯角</span>和<span
|
| 554 |
+
style="color:var(--cyan);font-weight:700">同側內角</span>等,細節等上課的時候再慢慢說吧~ 😄</div>
|
| 555 |
+
</div>
|
| 556 |
+
|
| 557 |
+
<!-- Buttons -->
|
| 558 |
+
<div style="display:flex;gap:12px;justify-content:center;margin-top:8px;margin-bottom:24px;padding:0 24px">
|
| 559 |
+
<button class="abtn btn-go" onclick="game.restart()" style="max-width:200px">再次挑戰</button>
|
| 560 |
+
<button class="back-btn" onclick="window.location.href='index.html'" style="position:static">返回地圖</button>
|
| 561 |
+
</div>
|
| 562 |
+
</div>
|
| 563 |
+
<button class="back-btn hidden" id="back-btn" onclick="window.location.href='index.html'">← 返回</button>
|
| 564 |
+
|
| 565 |
+
<script>
|
| 566 |
+
const TUT_LEVELS = [
|
| 567 |
+
{ type: 'tutorial-draw', angle: 0, dist: 70, msg: '教學:左邊的垂直距離已固定為 d,請拖曳橘色端點調整邊 B 的角度,讓右邊也等於 d' },
|
| 568 |
+
{ type: 'tutorial-judge', angle: 0, dist: 70, parallel: true, msg: '教學:觀察各處的垂直距離是否都相等,相等就代表平行' }
|
| 569 |
+
];
|
| 570 |
+
const PRAC_LEVELS = [
|
| 571 |
+
{ type: 'draw', angle: 5, dist: 65 }, { type: 'draw', angle: -8, dist: 60 },
|
| 572 |
+
{ type: 'judge', angle: 6, dist: 55 }, { type: 'draw', angle: 12, dist: 55 },
|
| 573 |
+
{ type: 'judge', angle: -10, dist: 50 }, { type: 'draw', angle: 15, dist: 50 },
|
| 574 |
+
{ type: 'judge', angle: 18, dist: 45 }, { type: 'draw', angle: -20, dist: 45 }
|
| 575 |
+
];
|
| 576 |
+
// 60s challenge pool
|
| 577 |
+
const CHAL_POOL = [
|
| 578 |
+
{ type: 'draw', angle: 5, dist: 65 }, { type: 'draw', angle: -8, dist: 60 }, { type: 'draw', angle: 12, dist: 55 },
|
| 579 |
+
{ type: 'draw', angle: 15, dist: 50 }, { type: 'draw', angle: -20, dist: 45 }, { type: 'draw', angle: 22, dist: 40 },
|
| 580 |
+
{ type: 'judge', angle: 6, dist: 55 }, { type: 'judge', angle: -10, dist: 50 }, { type: 'judge', angle: 18, dist: 45 },
|
| 581 |
+
{ type: 'judge', angle: 14, dist: 40 }, { type: 'judge', angle: -15, dist: 60 }, { type: 'draw', angle: -12, dist: 48 }
|
| 582 |
+
];
|
| 583 |
+
|
| 584 |
+
class ConveyorGame {
|
| 585 |
+
constructor() {
|
| 586 |
+
this.cv = document.getElementById('gameCanvas'); this.ctx = this.cv.getContext('2d');
|
| 587 |
+
this.dom = {
|
| 588 |
+
hud: document.getElementById('hud'), score: document.getElementById('score-d'),
|
| 589 |
+
level: document.getElementById('level-d'), combo: document.getElementById('combo-d'),
|
| 590 |
+
bp: document.getElementById('bottom-panel'), dc: document.getElementById('draw-ctl'),
|
| 591 |
+
jc: document.getElementById('judge-ctl'), hp: document.getElementById('hint-panel'),
|
| 592 |
+
ht: document.getElementById('hint-text'), td: document.getElementById('tgt-d'),
|
| 593 |
+
ss: document.getElementById('start-screen'), bs: document.getElementById('brief-screen'),
|
| 594 |
+
tm: document.getElementById('tut-modal'), tt: document.getElementById('tut-title'),
|
| 595 |
+
tmsg: document.getElementById('tut-msg'), ticon: document.getElementById('tut-icon'),
|
| 596 |
+
tbtn: document.getElementById('tut-btn'),
|
| 597 |
+
sum: document.getElementById('summary-screen'), sumCh: document.getElementById('sum-challenge'),
|
| 598 |
+
sumBest: document.getElementById('sum-best'),
|
| 599 |
+
bb: document.getElementById('back-btn'), phaseLbl: document.getElementById('phase-lbl'),
|
| 600 |
+
hudLbl: document.getElementById('hud-lbl'), scoreUnit: document.getElementById('score-unit'),
|
| 601 |
+
comboBox: document.getElementById('combo-box'),
|
| 602 |
+
timerBar: document.getElementById('timer-bar'), timerFill: document.getElementById('timer-fill')
|
| 603 |
+
};
|
| 604 |
+
this.s = {
|
| 605 |
+
playing: false, phase: 'tutorial', lvIdx: 0, score: 0, combo: 0, maxCombo: 0,
|
| 606 |
+
chalScore: 0, bestScore: 0,
|
| 607 |
+
line1: null, pivot: null, dragEnd: null, angleOff: 0, type: 'draw',
|
| 608 |
+
targetDist: 60, isParallel: true, markerTs: [], dragging: false,
|
| 609 |
+
showHbeam: false, hbeamAnim: 0, frame: 0, stars: [], bL: {}, bR: {},
|
| 610 |
+
fixMode: false, lineB: null, line2: null, nx: 0, ny: 0,
|
| 611 |
+
timerStart: 0, timerDur: 60000, timerActive: false
|
| 612 |
+
};
|
| 613 |
+
this._genStars(); this._resize();
|
| 614 |
+
window.addEventListener('resize', () => this._resize());
|
| 615 |
+
this.cv.addEventListener('pointerdown', e => this._pd(e));
|
| 616 |
+
this.cv.addEventListener('pointermove', e => this._pm(e));
|
| 617 |
+
this.cv.addEventListener('pointerup', () => { this.s.dragging = false });
|
| 618 |
+
this.cv.addEventListener('pointercancel', () => { this.s.dragging = false });
|
| 619 |
+
this.s.bestScore = parseInt(localStorage.getItem('math_city_score_parallel')) || 0;
|
| 620 |
+
this._loop = this._loop.bind(this); requestAnimationFrame(this._loop);
|
| 621 |
+
}
|
| 622 |
+
_resize() {
|
| 623 |
+
const d = window.devicePixelRatio || 1; this.W = window.innerWidth; this.H = window.innerHeight;
|
| 624 |
+
this.cv.width = this.W * d; this.cv.height = this.H * d; this.ctx.setTransform(d, 0, 0, d, 0, 0);
|
| 625 |
+
const m = Math.max(40, this.W * .06), bw = Math.max(70, Math.min(140, this.W * .14));
|
| 626 |
+
this.s.bL = { x: m, y: this.H * .15, w: bw, h: this.H * .78 };
|
| 627 |
+
this.s.bR = { x: this.W - m - bw, y: this.H * .1, w: bw, h: this.H * .83 };
|
| 628 |
+
}
|
| 629 |
+
_genStars() { this.s.stars = []; for (let i = 0; i < 80; i++)this.s.stars.push({ x: Math.random(), y: Math.random() * .55, r: .5 + Math.random() * 1.5, tw: Math.random() * Math.PI * 2 }) }
|
| 630 |
+
showBrief() { this.dom.ss.classList.add('hidden'); this.dom.bs.classList.remove('hidden') }
|
| 631 |
+
start() {
|
| 632 |
+
this.s.playing = true; this.s.phase = 'tutorial'; this.s.lvIdx = 0;
|
| 633 |
+
this.s.score = 0; this.s.combo = 0; this.s.maxCombo = 0; this.s.chalScore = 0;
|
| 634 |
+
this.s.timerActive = false;
|
| 635 |
+
this.dom.ss.classList.add('hidden'); this.dom.bs.classList.add('hidden');
|
| 636 |
+
this.dom.sum.classList.add('hidden'); this.dom.tm.classList.add('hidden');
|
| 637 |
+
this.dom.hud.classList.remove('hidden'); this.dom.bb.classList.remove('hidden');
|
| 638 |
+
this.dom.timerBar.classList.add('hidden');
|
| 639 |
+
this._genLevel();
|
| 640 |
+
}
|
| 641 |
+
restart() { this.start() }
|
| 642 |
+
|
| 643 |
+
_perpFoot(px, py, ax, ay, bx, by) {
|
| 644 |
+
const dx = bx - ax, dy = by - ay, t = ((px - ax) * dx + (py - ay) * dy) / (dx * dx + dy * dy);
|
| 645 |
+
return { x: ax + t * dx, y: ay + t * dy, t };
|
| 646 |
+
}
|
| 647 |
+
_perpDist(px, py, lp1, lp2) {
|
| 648 |
+
const f = this._perpFoot(px, py, lp1.x, lp1.y, lp2.x, lp2.y);
|
| 649 |
+
return Math.sqrt((px - f.x) ** 2 + (py - f.y) ** 2);
|
| 650 |
+
}
|
| 651 |
+
|
| 652 |
+
_getLevels() {
|
| 653 |
+
if (this.s.phase === 'tutorial') return TUT_LEVELS;
|
| 654 |
+
if (this.s.phase === 'practice') return PRAC_LEVELS;
|
| 655 |
+
return null; // challenge mode uses random
|
| 656 |
+
}
|
| 657 |
+
|
| 658 |
+
_genLevel() {
|
| 659 |
+
this.s.fixMode = false; this._resize();
|
| 660 |
+
let lv;
|
| 661 |
+
if (this.s.phase === 'challenge') {
|
| 662 |
+
lv = { ...CHAL_POOL[Math.floor(Math.random() * CHAL_POOL.length)] };
|
| 663 |
+
// Randomize angle direction
|
| 664 |
+
lv.angle = lv.angle * (Math.random() > .5 ? 1 : -1);
|
| 665 |
+
if (lv.type === 'judge') lv.parallel = Math.random() < .5;
|
| 666 |
+
} else {
|
| 667 |
+
const levels = this._getLevels();
|
| 668 |
+
lv = levels[this.s.lvIdx];
|
| 669 |
+
if (!lv) {
|
| 670 |
+
if (this.s.phase === 'tutorial') {
|
| 671 |
+
this._showPhaseModal('🎓', '教學完成!', '你已學會如何判斷平行線。\n接下來進入練習關卡!', '開始練習 →', 'practice');
|
| 672 |
+
return;
|
| 673 |
+
}
|
| 674 |
+
if (this.s.phase === 'practice') {
|
| 675 |
+
this._showPhaseModal('🔧', '練習完成!', '你已經很熟練了!\n接下來是 60 秒限時挑戰,\n看你能通過幾關!', '開始挑戰 ⏱️', 'challenge');
|
| 676 |
+
return;
|
| 677 |
+
}
|
| 678 |
+
return;
|
| 679 |
+
}
|
| 680 |
+
}
|
| 681 |
+
this.s.type = lv.type;
|
| 682 |
+
const bL = this.s.bL, bR = this.s.bR, lx = bL.x + bL.w, rx = bR.x;
|
| 683 |
+
const yT = Math.max(bL.y, bR.y) + 60, yB = Math.min(bL.y + bL.h, bR.y + bR.h) - 60;
|
| 684 |
+
const yM = (yT + yB) / 2, aRad = lv.angle * Math.PI / 180;
|
| 685 |
+
const hL = (rx - lx) / 2, cy = yM + (Math.random() * 40 - 20);
|
| 686 |
+
const l1p1 = { x: lx, y: cy - Math.sin(aRad) * hL }, l1p2 = { x: rx, y: cy + Math.sin(aRad) * hL };
|
| 687 |
+
this.s.line1 = { p1: l1p1, p2: l1p2 };
|
| 688 |
+
const d = lv.dist; this.s.targetDist = d;
|
| 689 |
+
const ldx = l1p2.x - l1p1.x, ldy = l1p2.y - l1p1.y, len = Math.sqrt(ldx * ldx + ldy * ldy);
|
| 690 |
+
let nx = -ldy / len, ny = ldx / len; if (ny < 0) { nx = -nx; ny = -ny }
|
| 691 |
+
this.s.nx = nx; this.s.ny = ny;
|
| 692 |
+
const nMk = (this.s.phase === 'tutorial' || this.s.lvIdx < 2) ? 2 : 3;
|
| 693 |
+
const mkTs = []; for (let i = 0; i < nMk; i++)mkTs.push((i + 1) / (nMk + 1));
|
| 694 |
+
this.s.markerTs = mkTs;
|
| 695 |
+
const isDraw = lv.type.includes('draw');
|
| 696 |
+
if (isDraw) {
|
| 697 |
+
const tLeft = mkTs[0];
|
| 698 |
+
const pA = { x: l1p1.x + ldx * tLeft, y: l1p1.y + ldy * tLeft };
|
| 699 |
+
this.s.pivot = { x: pA.x + nx * d, y: pA.y + ny * d };
|
| 700 |
+
this.s.angleOff = (Math.random() > .5 ? 1 : -1) * (0.04 + Math.random() * 0.08);
|
| 701 |
+
this._updateDragEnd();
|
| 702 |
+
this.dom.dc.classList.remove('hidden'); this.dom.jc.classList.add('hidden');
|
| 703 |
+
this.dom.td.textContent = `d = ${d}`;
|
| 704 |
+
} else {
|
| 705 |
+
this.s.isParallel = lv.parallel != null ? lv.parallel : Math.random() < .5;
|
| 706 |
+
const cP1 = { x: l1p1.x + nx * d, y: l1p1.y + ny * d }, cP2 = { x: l1p2.x + nx * d, y: l1p2.y + ny * d };
|
| 707 |
+
if (this.s.isParallel) { this.s.line2 = { p1: { ...cP1 }, p2: { ...cP2 } } }
|
| 708 |
+
else {
|
| 709 |
+
const sk = (8 + Math.random() * 18) * (Math.random() < .5 ? 1 : -1);
|
| 710 |
+
this.s.line2 = { p1: { x: cP1.x + nx * sk * .4, y: cP1.y + ny * sk * .4 }, p2: { x: cP2.x - nx * sk * .4, y: cP2.y - ny * sk * .4 } }
|
| 711 |
+
}
|
| 712 |
+
this.dom.dc.classList.add('hidden'); this.dom.jc.classList.remove('hidden');
|
| 713 |
+
}
|
| 714 |
+
const hint = lv.msg || (isDraw ? '拖曳橘色端點調整邊 B 的角度,讓各處垂直距離都等於 d' : '觀察各測量點的垂直距離是否相等,判斷輸送帶是否平行');
|
| 715 |
+
this.dom.ht.textContent = hint;
|
| 716 |
+
this.s.showHbeam = false; this.s.hbeamAnim = 0;
|
| 717 |
+
this.dom.bp.classList.remove('hidden'); this.dom.hp.classList.remove('hidden'); this._upHUD();
|
| 718 |
+
}
|
| 719 |
+
|
| 720 |
+
_updateDragEnd() {
|
| 721 |
+
const l1 = this.s.line1, ldx = l1.p2.x - l1.p1.x, ldy = l1.p2.y - l1.p1.y;
|
| 722 |
+
const baseAngle = Math.atan2(ldy, ldx), angle = baseAngle + this.s.angleOff;
|
| 723 |
+
const lx = this.s.bL.x + this.s.bL.w, rx = this.s.bR.x, pivot = this.s.pivot;
|
| 724 |
+
const cos = Math.cos(angle), sin = Math.sin(angle);
|
| 725 |
+
const tL = cos !== 0 ? (lx - pivot.x) / cos : 0, tR = cos !== 0 ? (rx - pivot.x) / cos : 0;
|
| 726 |
+
this.s.lineB = { p1: { x: pivot.x + tL * cos, y: pivot.y + tL * sin }, p2: { x: pivot.x + tR * cos, y: pivot.y + tR * sin } };
|
| 727 |
+
this.s.dragEnd = this.s.lineB.p2;
|
| 728 |
+
}
|
| 729 |
+
|
| 730 |
+
_enterFixMode() {
|
| 731 |
+
this.s.fixMode = true; this.s.type = 'draw';
|
| 732 |
+
const l1 = this.s.line1, l2 = this.s.line2, d = this.s.targetDist;
|
| 733 |
+
const ldx = l1.p2.x - l1.p1.x, ldy = l1.p2.y - l1.p1.y;
|
| 734 |
+
const tLeft = this.s.markerTs[0];
|
| 735 |
+
const pA = { x: l1.p1.x + ldx * tLeft, y: l1.p1.y + ldy * tLeft };
|
| 736 |
+
this.s.pivot = { x: pA.x + this.s.nx * d, y: pA.y + this.s.ny * d };
|
| 737 |
+
const baseAngle = Math.atan2(ldy, ldx);
|
| 738 |
+
this.s.angleOff = Math.atan2(l2.p2.y - l2.p1.y, l2.p2.x - l2.p1.x) - baseAngle;
|
| 739 |
+
this._updateDragEnd();
|
| 740 |
+
this.dom.jc.classList.add('hidden'); this.dom.dc.classList.remove('hidden');
|
| 741 |
+
this.dom.td.textContent = `d = ${d}`;
|
| 742 |
+
this.dom.ht.textContent = '不平行!請調整邊 B,讓垂直距離都等於 d 才能通過';
|
| 743 |
+
}
|
| 744 |
+
|
| 745 |
+
_getL2() { return (this.s.type.includes('draw') || this.s.fixMode) ? this.s.lineB : this.s.line2 }
|
| 746 |
+
|
| 747 |
+
_showPhaseModal(icon, title, msg, btnText, nextPhase) {
|
| 748 |
+
this.dom.ticon.textContent = icon; this.dom.tt.textContent = title; this.dom.tmsg.textContent = msg;
|
| 749 |
+
this.dom.tbtn.textContent = btnText; this.dom.tbtn.onclick = () => {
|
| 750 |
+
this.dom.tm.classList.add('hidden');
|
| 751 |
+
this.s.phase = nextPhase; this.s.lvIdx = 0;
|
| 752 |
+
if (nextPhase === 'challenge') { this._startChallenge() }
|
| 753 |
+
else this._genLevel();
|
| 754 |
+
};
|
| 755 |
+
this.dom.tm.classList.remove('hidden');
|
| 756 |
+
}
|
| 757 |
+
|
| 758 |
+
_startChallenge() {
|
| 759 |
+
this.s.chalScore = 0; this.s.combo = 0; this.s.timerStart = Date.now(); this.s.timerActive = true;
|
| 760 |
+
this.dom.timerBar.classList.remove('hidden'); this._genLevel();
|
| 761 |
+
}
|
| 762 |
+
|
| 763 |
+
_checkTimer() {
|
| 764 |
+
if (!this.s.timerActive) return false;
|
| 765 |
+
const elapsed = Date.now() - this.s.timerStart;
|
| 766 |
+
const pct = Math.max(0, 1 - elapsed / this.s.timerDur);
|
| 767 |
+
this.dom.timerFill.style.width = `${pct * 100}%`;
|
| 768 |
+
if (pct <= .3) this.dom.timerFill.style.background = 'linear-gradient(90deg,#ef4444,#f97316)';
|
| 769 |
+
else if (pct <= .6) this.dom.timerFill.style.background = 'linear-gradient(90deg,#fbbf24,#22c55e)';
|
| 770 |
+
if (elapsed >= this.s.timerDur) { this.s.timerActive = false; this._endChallenge(); return true }
|
| 771 |
+
return false;
|
| 772 |
+
}
|
| 773 |
+
|
| 774 |
+
_endChallenge() {
|
| 775 |
+
this.s.playing = false; this.s.timerActive = false;
|
| 776 |
+
this.dom.hud.classList.add('hidden'); this.dom.bp.classList.add('hidden');
|
| 777 |
+
this.dom.hp.classList.add('hidden'); this.dom.bb.classList.add('hidden');
|
| 778 |
+
this.dom.timerBar.classList.add('hidden');
|
| 779 |
+
// Save best
|
| 780 |
+
if (this.s.chalScore > this.s.bestScore) {
|
| 781 |
+
this.s.bestScore = this.s.chalScore;
|
| 782 |
+
localStorage.setItem('math_city_score_parallel', String(this.s.bestScore));
|
| 783 |
+
}
|
| 784 |
+
this.dom.sumCh.textContent = `${this.s.chalScore} 關`;
|
| 785 |
+
this.dom.sumBest.textContent = this.s.bestScore;
|
| 786 |
+
this.dom.sum.classList.remove('hidden');
|
| 787 |
+
}
|
| 788 |
+
|
| 789 |
+
checkAnswer() {
|
| 790 |
+
if (!this.s.playing) return;
|
| 791 |
+
if (this.s.timerActive && this._checkTimer()) return;
|
| 792 |
+
const l2 = this._getL2(); if (!l2) return;
|
| 793 |
+
const d = this.s.targetDist, l1 = this.s.line1;
|
| 794 |
+
let totalErr = 0;
|
| 795 |
+
this.s.markerTs.forEach(t => {
|
| 796 |
+
const px = l1.p1.x + (l1.p2.x - l1.p1.x) * t, py = l1.p1.y + (l1.p2.y - l1.p1.y) * t;
|
| 797 |
+
totalErr += Math.abs(this._perpDist(px, py, l2.p1, l2.p2) - d);
|
| 798 |
+
});
|
| 799 |
+
const avg = totalErr / this.s.markerTs.length;
|
| 800 |
+
if (this.s.phase === 'tutorial') {
|
| 801 |
+
if (avg < 5) {
|
| 802 |
+
this._showPhaseModal('✅', '教學完成!', '垂直距離處處相等 = 平行!\n確認距離一致就代表兩條線平行', '確認,繼續 →', null);
|
| 803 |
+
this.dom.tbtn.onclick = () => { this.dom.tm.classList.add('hidden'); this.s.lvIdx++; this._genLevel() }
|
| 804 |
+
}
|
| 805 |
+
else this._showToast('還不夠精確,再調整一下!', false);
|
| 806 |
+
return;
|
| 807 |
+
}
|
| 808 |
+
if (this.s.fixMode) {
|
| 809 |
+
if (avg < 4) { this._onCorrect(80) }
|
| 810 |
+
else if (avg < 10) { this._onCorrect(Math.max(20, 60 - Math.floor(avg * 4))) }
|
| 811 |
+
else this._showToast('還不夠精確,再調整!', false);
|
| 812 |
+
return;
|
| 813 |
+
}
|
| 814 |
+
if (avg < 4) this._onCorrect(100); else if (avg < 10) this._onCorrect(Math.max(30, 80 - Math.floor(avg * 5)));
|
| 815 |
+
else this._showToast('垂直距離不一致,再調整!', false);
|
| 816 |
+
}
|
| 817 |
+
|
| 818 |
+
judgeAnswer(says) {
|
| 819 |
+
if (!this.s.playing) return;
|
| 820 |
+
if (this.s.timerActive && this._checkTimer()) return;
|
| 821 |
+
if (this.s.phase === 'tutorial') {
|
| 822 |
+
if (says === this.s.isParallel) {
|
| 823 |
+
this._showPhaseModal('✅', '教學完成!', '觀察垂直距離是否處處相等,\n相等就代表平行!', '確認,繼續 →', null);
|
| 824 |
+
this.dom.tbtn.onclick = () => { this.dom.tm.classList.add('hidden'); this.s.lvIdx++; this._genLevel() }
|
| 825 |
+
} else this._showToast(this.s.isParallel ? '看看各點垂直距離是否相同?' : '注意各點距離不同喔!', false);
|
| 826 |
+
return;
|
| 827 |
+
}
|
| 828 |
+
if (says === this.s.isParallel) {
|
| 829 |
+
if (this.s.isParallel) { this._onCorrect(100) }
|
| 830 |
+
else { this._showToast('正確!不平行。現在請調整讓它平行!', true); setTimeout(() => this._enterFixMode(), 1200) }
|
| 831 |
+
} else {
|
| 832 |
+
this.s.combo = 0;
|
| 833 |
+
this._showToast(this.s.isParallel ? '其實是平行的!垂直距離處處相等。' : '其實不平行!各點垂直距離不同。', false);
|
| 834 |
+
this._upHUD();
|
| 835 |
+
}
|
| 836 |
+
}
|
| 837 |
+
|
| 838 |
+
tutNext() { }
|
| 839 |
+
|
| 840 |
+
_onCorrect(pts) {
|
| 841 |
+
this.s.combo++; if (this.s.combo > this.s.maxCombo) this.s.maxCombo = this.s.combo;
|
| 842 |
+
if (this.s.phase === 'challenge') {
|
| 843 |
+
this.s.chalScore++;
|
| 844 |
+
this._showToast(`+1 關!共 ${this.s.chalScore} 關`, true);
|
| 845 |
+
} else {
|
| 846 |
+
const tot = pts + Math.min(this.s.combo * 10, 50); this.s.score += tot;
|
| 847 |
+
this._showToast(`+${tot} 噸 鋼材通過!`, true);
|
| 848 |
+
}
|
| 849 |
+
this.s.showHbeam = true; this.s.hbeamAnim = 0;
|
| 850 |
+
setTimeout(() => {
|
| 851 |
+
if (this.s.timerActive && this._checkTimer()) return;
|
| 852 |
+
this.s.fixMode = false; this.s.lvIdx++;
|
| 853 |
+
if (this.s.phase === 'challenge') { this._genLevel(); return }
|
| 854 |
+
const levels = this._getLevels();
|
| 855 |
+
if (!levels || this.s.lvIdx >= levels.length) { this._genLevel(); return } // will trigger phase transition
|
| 856 |
+
this._genLevel();
|
| 857 |
+
}, 2200);
|
| 858 |
+
}
|
| 859 |
+
|
| 860 |
+
_showToast(t, ok) { const e = document.createElement('div'); e.className = `toast ${ok ? 'tok' : 'tng'}`; e.textContent = t; document.body.appendChild(e); setTimeout(() => { try { e.remove() } catch (x) { } }, 1800) }
|
| 861 |
+
_upHUD() {
|
| 862 |
+
if (this.s.phase === 'challenge') {
|
| 863 |
+
this.dom.hudLbl.textContent = '通過關卡'; this.dom.score.textContent = this.s.chalScore; this.dom.scoreUnit.textContent = ' 關';
|
| 864 |
+
this.dom.phaseLbl.textContent = '挑戰'; this.dom.level.textContent = '⏱️ 60s';
|
| 865 |
+
} else if (this.s.phase === 'practice') {
|
| 866 |
+
this.dom.hudLbl.textContent = '鋼材運送'; this.dom.score.textContent = this.s.score; this.dom.scoreUnit.textContent = ' 噸';
|
| 867 |
+
this.dom.phaseLbl.textContent = '練習'; this.dom.level.textContent = `${this.s.lvIdx + 1}/${PRAC_LEVELS.length}`;
|
| 868 |
+
} else {
|
| 869 |
+
this.dom.hudLbl.textContent = '鋼材運送'; this.dom.score.textContent = this.s.score; this.dom.scoreUnit.textContent = ' 噸';
|
| 870 |
+
this.dom.phaseLbl.textContent = '教學'; this.dom.level.textContent = `${this.s.lvIdx + 1}/${TUT_LEVELS.length}`;
|
| 871 |
+
}
|
| 872 |
+
this.dom.combo.textContent = this.s.combo;
|
| 873 |
+
}
|
| 874 |
+
|
| 875 |
+
_gp(e) { const r = this.cv.getBoundingClientRect(); return { x: e.clientX - r.left, y: e.clientY - r.top } }
|
| 876 |
+
_pd(e) {
|
| 877 |
+
if (!(this.s.type.includes('draw') || this.s.fixMode) || !this.s.playing || !this.s.dragEnd) return;
|
| 878 |
+
const pos = this._gp(e), dx = pos.x - this.s.dragEnd.x, dy = pos.y - this.s.dragEnd.y;
|
| 879 |
+
if (dx * dx + dy * dy < 40 * 40) { this.s.dragging = true; this.cv.setPointerCapture(e.pointerId) }
|
| 880 |
+
}
|
| 881 |
+
_pm(e) {
|
| 882 |
+
if (!this.s.dragging) return;
|
| 883 |
+
const pos = this._gp(e), pivot = this.s.pivot;
|
| 884 |
+
const newAngle = Math.atan2(pos.y - pivot.y, pos.x - pivot.x);
|
| 885 |
+
const l1 = this.s.line1;
|
| 886 |
+
this.s.angleOff = Math.max(-0.5, Math.min(0.5, newAngle - Math.atan2(l1.p2.y - l1.p1.y, l1.p2.x - l1.p1.x)));
|
| 887 |
+
this._updateDragEnd();
|
| 888 |
+
}
|
| 889 |
+
|
| 890 |
+
_loop() {
|
| 891 |
+
this.s.frame++;
|
| 892 |
+
if (this.s.timerActive) this._checkTimer();
|
| 893 |
+
this._draw(); requestAnimationFrame(this._loop);
|
| 894 |
+
}
|
| 895 |
+
_draw() {
|
| 896 |
+
const ctx = this.ctx, W = this.W, H = this.H, f = this.s.frame;
|
| 897 |
+
const sg = ctx.createLinearGradient(0, 0, 0, H);
|
| 898 |
+
sg.addColorStop(0, '#030d06'); sg.addColorStop(.5, '#071a0e'); sg.addColorStop(1, '#0a2614');
|
| 899 |
+
ctx.fillStyle = sg; ctx.fillRect(0, 0, W, H);
|
| 900 |
+
this.s.stars.forEach(s => { const tw = .4 + .6 * Math.abs(Math.sin(f * .02 + s.tw)); ctx.beginPath(); ctx.arc(s.x * W, s.y * H, s.r, 0, Math.PI * 2); ctx.fillStyle = `rgba(255,255,255,${tw * .7})`; ctx.fill() });
|
| 901 |
+
ctx.fillStyle = '#051208'; const bt = H * .88;
|
| 902 |
+
[[.05, .3], [.1, .45], [.18, .25], [.25, .5], [.3, .35], [.38, .55], [.45, .28], [.52, .42], [.58, .32], [.65, .48], [.72, .38], [.78, .52], [.85, .3], [.92, .44]]
|
| 903 |
+
.forEach(([xr, hr]) => { ctx.fillRect(xr * W, bt - hr * H * .2, W * .055, hr * H * .2) });
|
| 904 |
+
ctx.fillStyle = '#030d06'; ctx.fillRect(0, bt, W, H - bt);
|
| 905 |
+
if (this.s.playing) { this._drawBld(ctx, this.s.bL, 'left', f); this._drawBld(ctx, this.s.bR, 'right', f); this._drawScene(ctx, f) }
|
| 906 |
+
}
|
| 907 |
+
_drawBld(ctx, b, side, f) {
|
| 908 |
+
const gr = ctx.createLinearGradient(b.x, b.y, b.x + b.w, b.y + b.h);
|
| 909 |
+
gr.addColorStop(0, '#0f2818'); gr.addColorStop(1, '#081a0e');
|
| 910 |
+
ctx.fillStyle = gr; ctx.beginPath(); const t = 3;
|
| 911 |
+
ctx.moveTo(b.x + t, b.y); ctx.lineTo(b.x + b.w - t, b.y); ctx.lineTo(b.x + b.w, b.y + b.h); ctx.lineTo(b.x, b.y + b.h); ctx.closePath(); ctx.fill();
|
| 912 |
+
ctx.strokeStyle = 'rgba(34,197,94,.12)'; ctx.lineWidth = 1; ctx.stroke();
|
| 913 |
+
ctx.fillStyle = 'rgba(6,182,212,.3)'; ctx.fillRect(b.x + t, b.y, b.w - t * 2, 3);
|
| 914 |
+
const wC = Math.floor(b.w / 20), wR = Math.floor(b.h / 24), ws = 8;
|
| 915 |
+
const gx = (b.w - wC * ws) / (wC + 1), gy = (b.h - wR * ws) / (wR + 1);
|
| 916 |
+
for (let r = 0; r < wR; r++)for (let c = 0; c < wC; c++) {
|
| 917 |
+
const wx = b.x + gx * (c + 1) + ws * c, wy = b.y + gy * (r + 1) + ws * r;
|
| 918 |
+
const lit = Math.sin(f * .005 + r * 3.7 + c * 7.1) > -.3;
|
| 919 |
+
ctx.fillStyle = lit ? `rgba(251,191,36,${(Math.sin(f * .01 + r + c * 2) * .3 + .7) * .35})` : 'rgba(30,41,59,.7)';
|
| 920 |
+
ctx.fillRect(wx, wy, ws, ws);
|
| 921 |
+
}
|
| 922 |
+
if (side === 'right') {
|
| 923 |
+
const ax = b.x + b.w / 2; ctx.strokeStyle = '#475569'; ctx.lineWidth = 2;
|
| 924 |
+
ctx.beginPath(); ctx.moveTo(ax, b.y); ctx.lineTo(ax, b.y - 25); ctx.stroke();
|
| 925 |
+
if (Math.sin(f * .06) > 0) { ctx.beginPath(); ctx.arc(ax, b.y - 25, 3, 0, Math.PI * 2); ctx.fillStyle = '#ef4444'; ctx.shadowBlur = 8; ctx.shadowColor = '#ef4444'; ctx.fill(); ctx.shadowBlur = 0 }
|
| 926 |
+
}
|
| 927 |
+
}
|
| 928 |
+
_drawScene(ctx, f) {
|
| 929 |
+
const l1 = this.s.line1; if (!l1) return;
|
| 930 |
+
const d = this.s.targetDist, isDraw = this.s.type.includes('draw') || this.s.fixMode, l2 = this._getL2();
|
| 931 |
+
ctx.beginPath(); ctx.moveTo(l1.p1.x, l1.p1.y); ctx.lineTo(l1.p2.x, l1.p2.y);
|
| 932 |
+
ctx.strokeStyle = '#22c55e'; ctx.lineWidth = 4; ctx.shadowBlur = 6; ctx.shadowColor = '#22c55e'; ctx.stroke(); ctx.shadowBlur = 0;
|
| 933 |
+
ctx.font = 'bold 13px "Noto Sans TC"'; ctx.fillStyle = '#4ade80'; ctx.fillText('邊 A', l1.p1.x - 40, l1.p1.y - 8);
|
| 934 |
+
if (l2) {
|
| 935 |
+
ctx.beginPath(); ctx.moveTo(l2.p1.x, l2.p1.y); ctx.lineTo(l2.p2.x, l2.p2.y);
|
| 936 |
+
ctx.strokeStyle = '#f97316'; ctx.lineWidth = 4; ctx.shadowBlur = 6; ctx.shadowColor = '#f97316'; ctx.stroke(); ctx.shadowBlur = 0;
|
| 937 |
+
ctx.fillStyle = '#fb923c'; ctx.fillText('邊 B', l2.p1.x - 40, l2.p1.y + 20)
|
| 938 |
+
}
|
| 939 |
+
if (isDraw && this.s.dragEnd) {
|
| 940 |
+
const p = this.s.dragEnd, pr = 16 + Math.sin(f * .08) * 3;
|
| 941 |
+
ctx.beginPath(); ctx.arc(p.x, p.y, pr, 0, Math.PI * 2); ctx.fillStyle = 'rgba(249,115,22,.12)'; ctx.fill();
|
| 942 |
+
ctx.beginPath(); ctx.arc(p.x, p.y, 10, 0, Math.PI * 2); ctx.fillStyle = '#f97316'; ctx.shadowBlur = 10; ctx.shadowColor = '#f97316'; ctx.fill(); ctx.shadowBlur = 0;
|
| 943 |
+
ctx.beginPath(); ctx.arc(p.x, p.y, 4, 0, Math.PI * 2); ctx.fillStyle = '#fff'; ctx.fill();
|
| 944 |
+
if (this.s.pivot) {
|
| 945 |
+
const pv = this.s.pivot;
|
| 946 |
+
ctx.beginPath(); ctx.arc(pv.x, pv.y, 6, 0, Math.PI * 2); ctx.fillStyle = '#06b6d4'; ctx.shadowBlur = 6; ctx.shadowColor = '#06b6d4'; ctx.fill(); ctx.shadowBlur = 0;
|
| 947 |
+
ctx.beginPath(); ctx.arc(pv.x, pv.y, 3, 0, Math.PI * 2); ctx.fillStyle = '#fff'; ctx.fill()
|
| 948 |
+
}
|
| 949 |
+
}
|
| 950 |
+
if (l2) {
|
| 951 |
+
this.s.markerTs.forEach((t, idx) => {
|
| 952 |
+
const pAx = l1.p1.x + (l1.p2.x - l1.p1.x) * t, pAy = l1.p1.y + (l1.p2.y - l1.p1.y) * t;
|
| 953 |
+
let fx, fy, pd;
|
| 954 |
+
if (isDraw && idx === 0 && this.s.pivot) { fx = this.s.pivot.x; fy = this.s.pivot.y; pd = Math.sqrt((pAx - fx) ** 2 + (pAy - fy) ** 2) }
|
| 955 |
+
else { const foot = this._perpFoot(pAx, pAy, l2.p1.x, l2.p1.y, l2.p2.x, l2.p2.y); fx = foot.x; fy = foot.y; pd = Math.sqrt((pAx - fx) ** 2 + (pAy - fy) ** 2) }
|
| 956 |
+
ctx.beginPath(); ctx.setLineDash([5, 4]); ctx.moveTo(pAx, pAy); ctx.lineTo(fx, fy);
|
| 957 |
+
ctx.strokeStyle = 'rgba(255,255,255,.55)'; ctx.lineWidth = 1.5; ctx.stroke(); ctx.setLineDash([]);
|
| 958 |
+
const mAngle = Math.atan2(fy - pAy, fx - pAx);
|
| 959 |
+
const tkW = 7, txD = Math.cos(mAngle + Math.PI / 2) * tkW, tyD = Math.sin(mAngle + Math.PI / 2) * tkW;
|
| 960 |
+
ctx.beginPath(); ctx.moveTo(pAx - txD, pAy - tyD); ctx.lineTo(pAx + txD, pAy + tyD);
|
| 961 |
+
ctx.moveTo(fx - txD, fy - tyD); ctx.lineTo(fx + txD, fy + tyD);
|
| 962 |
+
ctx.strokeStyle = 'rgba(255,255,255,.6)'; ctx.lineWidth = 2; ctx.stroke();
|
| 963 |
+
// Right angle at A
|
| 964 |
+
const ras = 10;
|
| 965 |
+
let rax = pAx + Math.cos(mAngle) * ras, ray = pAy + Math.sin(mAngle) * ras;
|
| 966 |
+
ctx.beginPath(); ctx.moveTo(rax - txD * .5, ray - tyD * .5); ctx.lineTo(rax, ray); ctx.lineTo(pAx - txD * .5, pAy - tyD * .5);
|
| 967 |
+
ctx.strokeStyle = 'rgba(255,255,255,.4)'; ctx.lineWidth = 1; ctx.stroke();
|
| 968 |
+
// Right angle at B
|
| 969 |
+
rax = fx - Math.cos(mAngle) * ras; ray = fy - Math.sin(mAngle) * ras;
|
| 970 |
+
ctx.beginPath(); ctx.moveTo(rax - txD * .5, ray - tyD * .5); ctx.lineTo(rax, ray); ctx.lineTo(fx - txD * .5, fy - tyD * .5);
|
| 971 |
+
ctx.strokeStyle = 'rgba(255,255,255,.4)'; ctx.lineWidth = 1; ctx.stroke();
|
| 972 |
+
const label = Math.round(pd); ctx.font = 'bold 14px "JetBrains Mono"'; ctx.textAlign = 'center';
|
| 973 |
+
let col = '#e2e8f0';
|
| 974 |
+
if (isDraw) { if (idx === 0) col = '#06b6d4'; else { const err = Math.abs(pd - d); col = err < 3 ? '#4ade80' : err < 10 ? '#fbbf24' : '#f87171' } }
|
| 975 |
+
ctx.fillStyle = col; ctx.fillText(`${label}`, (pAx + fx) / 2 + 20, (pAy + fy) / 2 + 5); ctx.textAlign = 'left';
|
| 976 |
+
ctx.beginPath(); ctx.arc(pAx, pAy, 4, 0, Math.PI * 2); ctx.fillStyle = '#4ade80'; ctx.fill();
|
| 977 |
+
ctx.beginPath(); ctx.arc(fx, fy, 4, 0, Math.PI * 2); ctx.fillStyle = '#fb923c'; ctx.fill();
|
| 978 |
+
});
|
| 979 |
+
}
|
| 980 |
+
if (l2 && !this.s.showHbeam) {
|
| 981 |
+
const ph = (f * .015) % 1;
|
| 982 |
+
[l1, l2].forEach((ln, i) => {
|
| 983 |
+
const c = i === 0 ? 'rgba(34,197,94,.25)' : 'rgba(249,115,22,.25)';
|
| 984 |
+
for (let j = 0; j < 12; j++) { const tt = ((j / 12) + ph) % 1; ctx.beginPath(); ctx.arc(ln.p1.x + (ln.p2.x - ln.p1.x) * tt, ln.p1.y + (ln.p2.y - ln.p1.y) * tt, 2, 0, Math.PI * 2); ctx.fillStyle = c; ctx.fill() }
|
| 985 |
+
})
|
| 986 |
+
}
|
| 987 |
+
if (this.s.showHbeam && l1 && l2) {
|
| 988 |
+
this.s.hbeamAnim += 0.012;
|
| 989 |
+
if (this.s.hbeamAnim <= 1.5) {
|
| 990 |
+
const tt = Math.min(1, this.s.hbeamAnim);
|
| 991 |
+
const hx = l1.p1.x + (l1.p2.x - l1.p1.x) * tt, hy1 = l1.p1.y + (l1.p2.y - l1.p1.y) * tt, hy2 = l2.p1.y + (l2.p2.y - l2.p1.y) * tt;
|
| 992 |
+
const hc = (hy1 + hy2) / 2, bH = Math.abs(hy2 - hy1) * .5, bW = 18;
|
| 993 |
+
ctx.save(); ctx.translate(hx, hc);
|
| 994 |
+
ctx.fillStyle = '#94a3b8'; ctx.fillRect(-bW / 2, -bH / 2, 4, bH); ctx.fillRect(bW / 2 - 4, -bH / 2, 4, bH); ctx.fillRect(-bW / 2, -2, bW, 4);
|
| 995 |
+
ctx.shadowBlur = 8; ctx.shadowColor = '#22c55e'; ctx.strokeStyle = '#22c55e'; ctx.lineWidth = 1; ctx.strokeRect(-bW / 2 - 1, -bH / 2 - 1, bW + 2, bH + 2); ctx.shadowBlur = 0;
|
| 996 |
+
ctx.restore()
|
| 997 |
+
}
|
| 998 |
+
}
|
| 999 |
+
}
|
| 1000 |
+
}
|
| 1001 |
+
let game; try { game = new ConveyorGame(); window.game = game } catch (e) { console.error('Init fail:', e) }
|
| 1002 |
+
</script>
|
| 1003 |
+
</body>
|
| 1004 |
+
|
| 1005 |
+
</html>
|