Lashtw commited on
Commit
dbd41db
·
verified ·
1 Parent(s): f3b5228

Upload 102 files

Browse files
.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

  • SHA256: dee665d586024e7288ca74e4ccafc728839786b40ef9782a2008cacc350752b9
  • Pointer size: 132 Bytes
  • Size of remote file: 9.4 MB
Assets/triangle/Case/case2.png ADDED

Git LFS Details

  • SHA256: f4824e7e03ff6553875d3e03d10e1257faf9cfb41ab660e3e78707758ca287b5
  • Pointer size: 132 Bytes
  • Size of remote file: 8.89 MB
Assets/triangle/Case/case3.png ADDED

Git LFS Details

  • SHA256: 426a2a86981f6821c24a388136dacf5a4e67fac5c54bde35a817864b34f7e71a
  • Pointer size: 132 Bytes
  • Size of remote file: 8.99 MB
Assets/triangle/detective.svg ADDED
Assets/triangle/嫌疑犯/16造型師特徵.png ADDED

Git LFS Details

  • SHA256: b6441afaf3be1649282e6c4cfcfd55c83a6fc91dc2da58b975844cdfb0b421e1
  • Pointer size: 131 Bytes
  • Size of remote file: 539 kB
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
- title: MathCity
3
- emoji: 🌖
4
- colorFrom: gray
5
- colorTo: pink
6
- sdk: static
7
- pinned: false
8
- ---
9
-
10
- # Math City: Cyber Chronicles (數學特區:未來都市)
11
- ## ⚠️ AI 行為準則 (System Instructions)
12
- 1. **語言要求:** 所有的對話、思考過程、Implementation Plan (實作計畫) 以及程式碼註解,請**嚴格使用「繁體中文 (Traditional Chinese)」,唯獨專有名詞可以用英文呈現**。
13
- 2. **例外:** 只有程式碼本身的變數名稱、函數名稱保留英文。
14
- ## 1. 專案願景 (Project Vision)
15
- 本專案為八年級下學期數學課程的遊戲化教學平台。透過「未來都市」的沉浸式包裝,讓學生以「特務/維護者」的身分,在解決城市危機的過程中學習數學概念。
16
-
17
- **核心體驗**:
18
- * **平台**:iPad 橫向全螢幕體驗 (Mobile Web App)
19
- * **風格**:Cyberpunk / Neon Noir (賽博龐克/霓虹暗黑)
20
- * **技術**:HTML5 Canvas + Tailwind CSS + Vanilla JS (無須建置工具)。
21
-
22
- ## 2. 檔案結構 (File Structure)
23
- ```
24
- /
25
- ├── index.html # [入口] 城市全息地圖 (Math City Map)
26
- ├── sequence.html # [數列] 數列峽谷 (The Glitch Canyon)
27
- ├── function.html # [函] 能源核心 (The Energy Core)
28
- ├── congruence.html # [全等] 全等重案組 (Congruence District) - {待開發}
29
- ├── parallel.html # [平行] 平行建構區 (Parallel Skyline) - {待開發}
30
- └── assets/ # (建議) 存放片與音效資源
31
- ```
32
-
33
- ## 3. 視覺設計系統 (Design System)
34
-
35
- ### 色彩計畫 (Neon Palette)
36
- 這座城市由四個區域組成,每個區域有其代表的主調:
37
- * **🟦 能源核心 (Cyan)**: `#06b6d4` (Cyan-500) - 代表科技、冷靜、函數邏輯。
38
- * **🟨 數列峽谷 (Amber)**: `#f59e0b` (Amber-500) - 代表古老遺跡警示規律
39
- * **🟪 全等重案組 (Magenta)**: `#d946ef` (Fuchsia-500) - 代表神秘霓虹街頭偵探氛圍
40
- * **🟩 平行建構區 (Green)**: `#22c55e` (Green-500) - 代表建設雷射光束結構
41
- * **🌑 背景基調**: `#050510` (Very Dark Blue) - 極致黑,襯托霓虹光。
42
-
43
- ### UI 規範
44
- * **字體**: 'Orbitron' (數字/英文標題), 'Noto Sans TC' (中文內文)。
45
- * **介面**: 玻璃擬態 (Glassmorphism),半透明深色背景 + 亮色邊框
46
- * **互動**: 針對觸控優化的大按鈕避免依賴 Hover 效果
47
-
48
- ## 4. 遊戲模組詳解 (Game Modules)
49
-
50
- ### A. 數列峽谷 (The Glitch Canyon) - `sequence.html` [已完成/需微調]
51
- * **學單元**:等差數與級數。
52
- * **劇情**:城市邊緣的古老程式碼層出現 Glitch,階梯消失
53
- * **玩法**:動作跳躍遊戲
54
- * **Phase 1 (觀念檢測)**:進入關卡前,先通過數列填空測驗,確認對首項與公差的理解
55
- * **Phase 2 (數列跑酷)**:動作遊戲階段
56
- * **視覺風格**:全區統一碎石 (Gravel) 紋理,位荒野主題
57
- * **規則**:僅有數列的「前兩項」會有琥珀色外框提示,後續特定平台外觀玩家需自行計算並跳上正確字平台
58
- * **裝飾**:仙人掌與骨骸等裝飾物生成於無安全平台不干擾作答
59
- * **Phase 3 (召喚儀式)**:終點的互動教學。利用「倒轉複製」概念,將階梯補成長方形,引導學生推導出差級求和公式 $S_n = \frac{(a_1+a_n) \times n}{2}$
60
- * **Phase 4 (核心回顧)**:(Gold Standard) 遊戲結束後顯示重摘要,包含「等差數列定義」、「公差公式」、「級數和公式」,對應學習單內容
61
-
62
- ### B. 能源核心 (The Energy Core) - `function.html` [已完成]
63
- * **數學單元**:線型函數 ($y = ax + b$)
64
- * **劇情**:中央發電廠核心不穩,需要輸入正確的晶石量以維持能量平衡
65
- * **玩法**:數據分析與邏輯推導
66
- * **Phase 1 (蒐集數據)**:輸入 $x$ (晶石),觀察 $y$ (電力),利用描點法找出函關係
67
- * **Phase 2 (規律分析)**:觀察圖表上的點辨識出「直線」規律,並利用規律預測未來
68
- * **Phase 3 (過載危機)**:系統給目標電力 $y=203$玩家需根據 $y=2x+3$ 反求投入量 $x$
69
- * **Phase 4 (進階推導)**:(Advanced Deduction) 核心重置公式改變 ($y=ax+b$)
70
- * **Step 1 (找截距 b)**:投入 $x=0$,觀察 $y$ 值解出 $b$。
71
- * **Step 2 (找斜率 a)**:投入 $x=1$,觀察變化量,解出 $a$。
72
- * **Step 3 (驗算)**:利用新公式 $y=3x+5$ 驗算 $x=10$ 的結果並用圖表上的點 ($10,35$) 進行再確認
73
- * **Phase 5 (核心解鎖)**:輸入正確的 $a$ $b$ 完成任務
74
- * **Phase 6 (脈衝同步)**:節奏遊戲在音樂節拍下進行點擊,穩定核心能量,強化回饋感
75
-
76
- ### C. 全等重案組 (Congruence District) - `congruence.html` [規劃中]
77
- * **數學單元**���全等三角形判別 (SAS, ASA, SSS, RHS)。
78
- * **劇情**:追捕偽裝大師「三角魔人」
79
- * **玩法**:偵探解謎
80
- * **Phase 1 (蒐證)**:在案發現場使用放大鏡蒐集「邊長」與「角度」線索
81
- * **Phase 2 (指認)**:在警局指認牆上,利用蒐集的條件篩選嫌疑犯
82
- * **Phase 3 (審判/回顧)**:整理案情總結全等性質(SAS, ASA...),並解釋為何 SSA (搖擺分身) 無法全等
83
-
84
- ### D. 平行建構區 (Parallel Skyline) - `parallel.html` [規劃中]
85
- * **數學單元**:平行線截角性質、特殊四邊形。
86
- * **劇情**:建設通往雲端的摩天大樓
87
- * **玩法**:建築工程
88
- * **平行光束**:調整雷射發射器角度,利用「內錯角相等」或「同側內角互補」原理接通橋樑
89
- * **四邊形物流**:輸送帶上出現各種四邊形建材需根據對線性質 (是否長/垂直/平分) 快速分類
90
- * **Phase 3 (完工驗收)**:展示築藍圖總結平行線截角性質與特殊四邊形的判別家族樹
91
-
92
- ## 5. 開發路徑 (Roadmap)
93
- - [x] **Phase 1: 基礎建設**
94
- - [x] 建立 `index.html` 城市地圖。
95
- - [x] 生成 iPad 專用背景圖。
96
- - [x] 統一導航系統 (Back to Map 按鈕)
97
- - [x] **Phase 2: 現有程式優化**
98
- - [x] `function.html`: 音量核心遊戲完整化、Phase 4 進階邏輯完備、節奏遊戲計分平衡。
99
- - [x] `sequence.html`: 統一視覺風格,調難度曲線
100
- - [ ] **Phase 3: 新關卡開發**
101
- - [ ] 開發 `congruence.html` (全等重案組) 原型。
102
- - [ ] 開發 `parallel.html` (平行建構區) 原型。
103
-
104
- ---
105
-
106
- ## 6. 修正經驗與開發筆記 (Dev Log)
107
- 此區塊記錄開發過程中的重要迭代與修正經驗,供日後參考。
108
-
109
- ### [Sequence Canyon] 視覺與機制修正 (Visual & Mechanics Polish)
110
- * **視覺一致性的陷阱**: 修正了平台顏色提示過於明顯的問題,統一為碎石紋理,強制學生進行計算而非辨色。
111
- * **提示機制**: 僅由數列前兩項供視覺提示,後續完全依賴計算。
112
- * **裝飾物配置**: 將裝飾物移至非遊戲路徑的安全平台避免干擾判斷
113
-
114
- ### [Function Core] 進階教學設計 (Advanced Pedagogy Design)
115
- * **鷹架理論 (Scaffolding) 的應用**:
116
- * **變數分離**: 在推導 $y=ax+b$ 時,不要同時讓學生解兩個未知數。我們設計為:
117
- 1. 先令 $x=0$,讓 $a$ 項消失專注 $b$ (截距)
118
- 2. 知道 $b$再令 $x=1$,專注解 $a$ (斜率)。
119
- * 這樣能讓學生理解每個係數的幾何意義而非單純背誦聯立方程式
120
- * **視覺與代數的連結 (Visual-Algebra Connection)**:
121
- * **圖表驗證**: 在 Phase 4 Step 3,驗算出 $y=35$ 後,不僅僅是數字正確,程式還引導線看向圖表上$(10, 35)$ 點。這強化了「代數解」與「幾何圖形上的點」是同一回事的概念。
122
- * **正向回饋系統 (Positive Feedback Loop)**:
123
- * **避免挫折感**: 將傳 `alert()` 錯誤彈窗,改為「介面上的脈衝紅框」與「引導式錯誤訊息」。
124
- * **具體化錯誤**: 錯誤訊息不是只說「錯」而是說如果 x=2, y=11, 因 11 = 2a+5, 所以 a=?。這將**錯誤轉化為另一個數學題目**,而非單純的懲罰
125
- * **節奏遊戲獎勵**: 在完成艱澀的運算後Phase 6 的節奏遊戲提供純粹的感官刺激與高分回饋 (5000分)平衡大腦疲勞
126
- * **情境化總結 (Contextual Summary)**:
127
- * 任務完成頁面不僅列出分數,更點出核心數學素養:「預測」與「建模」。讓學生知道他們剛剛做的計算,本質上就是科學家預測未來的過程。
128
-
129
- ### [Function Core] 介面優化與除錯經驗
130
- * **按鈕鎖定邏輯**: 驗證成功後鎖定按鈕 (`disabled`) 非常重要,可防止重複觸發事件或重複計分。需確保 CSS Selector 選中正確的按鈕。
131
- * **樣式與動畫**:
132
- * ** vs 呼吸**: `animate-bounce` 用於錯誤訊息有時過於滑稽且難以閱讀,改為 `animate-pulse` (呼吸燈/閃耀) 配合紅色邊框,既有警示效果又保持質感。
133
- * **Canvas 文字清晰度**: 在 High-DPI 螢幕 (Retina) 上Canvas 必須正確縮放 (Scale by DevicePixelRatio),否則文字會模糊
134
-
135
- ---
136
-
137
- ## 7. 專案製作群 (Credits)
138
- * **遊戲設計**: 新竹縣精華國中 藍星宇老師
139
- * **社群**: [萬物皆數](https://www.facebook.com/groups/1554372228718393)
140
-
141
- *Created by Antigravity for 11402 Semester Project*
 
 
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

  • SHA256: 655d4beb583f100d3c4a61b12c8b4671bb3430635c976b18567abeca3ba5f287
  • Pointer size: 131 Bytes
  • Size of remote file: 787 kB
example/functionex2.png ADDED

Git LFS Details

  • SHA256: 6bbbd4d834fc48f7206e481843d61678d963bba1e0a4058bf8bd85fd02319dea
  • Pointer size: 132 Bytes
  • Size of remote file: 9.63 MB
example/functionex3.png ADDED

Git LFS Details

  • SHA256: 7af393bb2d05b576837b2c5b8ddc7fc52418fd533e4141d0deea8ee73f130c51
  • Pointer size: 132 Bytes
  • Size of remote file: 8.95 MB
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: #0f172a;
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(15, 23, 42, 0.6);
46
  /* Semi-transparent Slate 900 */
47
  z-index: -1;
48
  }
@@ -67,11 +74,12 @@
67
 
68
  /* Tech UI Styling */
69
  .glass-panel {
70
- background: rgba(15, 23, 42, 0.95);
71
  backdrop-filter: blur(12px);
72
  -webkit-backdrop-filter: blur(12px);
73
- border: 1px solid rgba(56, 189, 248, 0.3);
74
- box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.5), inset 0 0 20px rgba(56, 189, 248, 0.05);
 
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 = '#050b14';
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(15, 23, 42, 0.85)';
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 opacity-75" onclick="alert('🔒 區域封鎖:請先完成前置任務')">
 
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 opacity-75" onclick="alert('🚧 施工中:摩天大樓')">
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">摩天大樓</div>
284
- <div class="text-xs text-white opacity-80">Parallel Skyline</div>
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>