Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .serena/memories/project-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@
- 音效已整層移除(PR #72),唯一的聲音是答對時的 TTS 發音。兩種實作都敗給 iOS 音訊 session 行為,完整量測與 5 條死路留在設計文件 §13——**要加音效前先讀那節**,不要重走。
- 開場閘門(#gate)現在唯一的職責是喚醒 TTS 引擎(只有 click 叫得醒),拆掉會讓純拖曳的玩家整場沒發音。
- 匯出/匯入存檔:2026-07-22 拍板**暫緩至 Phase 2**(Phase 1 純單機,改 localStorage 本來就行;三筆要認的帳見設計文件 §16)。別再當待辦。
- Phase 1.5 PWA 已實作(2026-07-22,設計文件 §15 Phase 1.5):§16 原本的「❌ 不做 PWA」已翻案,理由與快取策略在 §2/§16。**Web 版的離線可玩到這一步才第一次真的成立**(在此之前斷網是白屏)。§17 的 Phase 1.5 那 6 條也已在正式站真機驗完並打勾(2026-07-25),已找人試玩過。**還沒打勾的只剩兩批**:上一條提到的「查詞提示」那幾條(2026-07-25 新加),以及分享那 3 條(2026-07-27 新加,見下一條),除此之外 Phase 1 + 1.5 全綠。
- Phase 1.5 PWA 已實作(2026-07-22,設計文件 §15 Phase 1.5):§16 原本的「❌ 不做 PWA」已翻案,理由與快取策略在 §2/§16。**Web 版的離線可玩到這一步才第一次真的成立**(在此之前斷網是白屏)。§17 的 Phase 1.5 那 6 條也已在正式站真機驗完並打勾(2026-07-25),已找人試玩過。**還沒打勾的只剩三批**:上一條提到的「查詞提示」那幾條(2026-07-25 新加)、分享那 3 條(2026-07-27 新加,見下一條),以及 UI 補丁那批(2026-07-28 新加,見下一條),除此之外 Phase 1 + 1.5 全綠。
- 分享(2026-07-27):修掉「電腦版分享只剩網址」,同批移除 `?code=` 兌換連結。機制與取捨在 UI 文件 §4-D(Windows 分支、提示規則)與設計文件 §9(為什麼分享不能送 `location.href`),別在這裡再抄一份。§17 那 3 條分享驗收待真機(Windows 桌機 + iOS/macOS 預覽卡片)。
- UI 補丁四項(2026-07-28):金幣進帳時頂列數字彈一下、`.screen` 收成 480px 一欄(桌機/平板才生效)、金幣不足時提示鈕的燈泡與價格一起轉暗、重玩舊關時開場閃一次「重玩不再獲得金幣」。細節在 UI 文件 §2/§3(含「為什麼重玩不在頂列常駐標記」),§17 新增的 5 條待真機/VoiceOver 驗。
- App 內的 PWA 安裝引導:2026-07-27 做過一版(Chrome 系 `beforeinstallprompt` 按鈕 + iOS 文字步驟,放在設定卡)**又整個拿掉**,程式碼已還原、別再當待辦。理由與重做前提在設計文件 §15 Phase 1.5-4(§16 有一句話版)——一句話:壓掉瀏覽器自己的招攬去換一顆沒人打得開的按鈕,而安裝率沒有分析可量。
- Phase 2(Capacitor)**因成本暫緩、不是取消**:需要一台 Mac + Apple $99/年 + Google Play $25,且純 WebView 有 Apple 4.2 被拒風險。等有留存數據或要收錢再啟動;存檔搬遷的做法寫在設計文件 §15 Phase 2。好友邀請碼也押在這一步(要 `Device.getId()` 才成立,理由見設計文件 §16,別在網頁版重試)。
- `mise run fetch-data` 只需跑一次(輸出在 tools/data/,已 gitignore);之後只需 `mise run gen`。
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,4 @@ Before finishing any change, update the docs that describe what you touched (a S
- Data/asset licensing is tracked in design doc §14; the attribution text lives in the About section of index.html, which links to `assets/licenses.txt` — the **full** WordNet and ECDICT license texts, shipped with the site because both licenses require the notice to travel with every copy and `data/levels/*.json` embeds their definitions. Naming the license is not enough, and the link has to point at our own copy, not upstream — linking out ships nothing and breaks the offline rule. Anything whose data ends up in `data/` needs its text in that file (build-time-only tools like OpenCC/NLTK/wordfreq don't). TWL/SOWPODS word lists and the COCA frequency table are prohibited (proprietary/paid).
- Animations should use `transform`/`opacity` only, and respect `prefers-reduced-motion`.
- **There is no in-app "add to home screen" prompt.** One was built and removed on 2026-07-27: intercepting `beforeinstallprompt` requires `preventDefault()`, which suppresses the browser's own install banner in exchange for a button inside a panel nobody opens, and with no analytics the install rate is unmeasurable. Design doc §15 Phase 1.5-4 has the full reasoning, the conditions for revisiting, and where it would go if it comes back (the opening gate, not the settings card); §16 lists it as deliberately not built.
- **There are no sound effects.** Two implementations were built and both lost to iOS audio-session behaviour: `<audio>` elements, whose `play()` blocks the main thread synchronously (191ms idle, 752ms after TTS held the session, 60-109ms when one element is retriggered fast) and caused the dropped frames, the undrawn selection line and the swallowed sfx; and Web Audio, whose `start()` is a clean 0.0ms but whose `AudioContext` will not wake while the iOS mute switch is on — `resume()` measured 3163/5124/8974ms, so the opening seconds were silent anyway. The whole layer is gone, along with the element pool, the per-element unlock loop, the silent warmup and the gate's unlock wait. The only audio left is the TTS pronunciation on a correct word; wrong words shake, bonuses show +N coins, level clear shows the card. Design doc §13 has every measurement and the dead ends — read it before proposing sfx again (§16 lists it as deliberately not built).
- **There are no sound effects.** Two implementations were built and both lost to iOS audio-session behaviour: `<audio>` elements, whose `play()` blocks the main thread synchronously (191ms idle, 752ms after TTS held the session, 60-109ms when one element is retriggered fast) and caused the dropped frames, the undrawn selection line and the swallowed sfx; and Web Audio, whose `start()` is a clean 0.0ms but whose `AudioContext` will not wake while the iOS mute switch is on — `resume()` measured 3163/5124/8974ms, so the opening seconds were silent anyway. The whole layer is gone, along with the element pool, the per-element unlock loop, the silent warmup and the gate's unlock wait. The only audio left is the TTS pronunciation on a correct word; wrong words shake, bonuses show +N coins and bump the top-bar coin total, level clear shows the card. Design doc §13 has every measurement and the dead ends — read it before proposing sfx again (§16 lists it as deliberately not built).
16 changes: 13 additions & 3 deletions docs/word-puzzle-game-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ function speak(word) {
- 格子尺寸用 container query 單位(`cqw`/`cqh`,以填字區實際分到的空間為準)計算,適配各種螢幕比例(包含平板),矮螢幕上不會溢出蓋到其他 UI。
- 即時回饋:滑字時上方顯示目前拼出的字母串;放開時——
- 命中目標字 → 字母飛入格子動畫;
- 命中 bonus → 金幣 +N 動畫;
- 命中 bonus → 金幣 +N 動畫(同時頂列的金幣數字彈一下——「+N」在畫面中央、數字在最上角,是畫面上距離最遠的兩點,不動一下沒人會把兩者連起來;只在金幣**變多**時播,細節見 UI 文件 §2);
- 重複的字 → 短暫「已找過」提示;
- 無效字 → 字串抖動後消失。
- ~~Haptic(震動)在命中時給一下~~:已移除。web 的 `navigator.vibrate` 在 iOS Safari 完全不支援、Android 上 10ms 脈衝短到 LRA 馬達振不出來,實測兩邊都無感;一個永遠沒作用的設定開關比沒有更糟。Phase 2 若接上 Capacitor 原生 haptics 再視體感決定要不要加回來。
Expand All @@ -193,7 +193,7 @@ function speak(word) {
- 關卡選擇:線性解鎖(最遠玩到 `currentLevel`);已完成關卡可重玩,但不重複給金幣(獎勵已按關卡記錄)。
- 已找到的目標字可點擊查看釋義與發音(見 §6),卡片位置貼著被點的格子彈出,點卡片外任意處關閉。
- **沒有音效。** 曾經有 4 個短音(命中、無效字、金幣、過關),兩種實作都試過、都在 iOS 上輸給同一件事——音訊 session 的行為,完整量測與經過見 §13 的「音效的兩次嘗試與收場」。唯一的聽覺回饋是答對目標字時的 TTS 發音(§6.1),無效字有抖動動畫、bonus 有 +N 金幣動畫、過關有過關卡片,四個缺口都已經有視覺回饋。多數 iOS 遊戲在靜音模式下本來就是安靜的,這也更貼近平台慣例。別再加回來(§16)。
- `prefers-reduced-motion` 開啟時跳過飛入/撒花動畫,直接顯示結果。實作是**一條全域規則**(`*, *::before, *::after` 把 `animation-duration`/`transition-duration` 夾成 `0.01ms`、`animation-iteration-count` 夾成 1),不是逐一列出選擇器的名單。兩個理由,別改回名單:(1) `animation: none` 的元素**不會發 `animationend`**,而 `grid.js`(格子的 `.pop`)與 `main.js` 的 `playOnce`(領取鈕的 `.pop`、提示鈕與領取鈕的 `.shake`)都是靠那個事件把 class 拆掉的——名單版等於讓那些 class 永久殘留,`0.01ms` 一樣看不見但動畫確實跑完、事件照常送到,兩邊的清理程式碼不必為這個模式分岔;(2) 名單要靠人記得維護,新增一個有 transition 的元件就得回來補一筆,漏了不會有任何報錯,而漏掉的代價正好落在最需要它的人身上。`iteration-count` 要一起夾,否則 infinite 的 pulse 會用 0.01ms 的週期狂跑。
- `prefers-reduced-motion` 開啟時跳過飛入/撒花動畫,直接顯示結果。實作是**一條全域規則**(`*, *::before, *::after` 把 `animation-duration`/`transition-duration` 夾成 `0.01ms`、`animation-iteration-count` 夾成 1),不是逐一列出選擇器的名單。兩個理由,別改回名單:(1) `animation: none` 的元素**不會發 `animationend`**,而 `grid.js`(格子的 `.pop`)與 `main.js` 的 `playOnce`(領取鈕的 `.pop`、提示鈕與領取鈕的 `.shake`、金幣數字的 `.bump`)都是靠那個事件把 class 拆掉的——名單版等於讓那些 class 永久殘留,`0.01ms` 一樣看不見但動畫確實跑完、事件照常送到,兩邊的清理程式碼不必為這個模式分岔;(2) 名單要靠人記得維護,新增一個有 transition 的元件就得回來補一筆,漏了不會有任何報錯,而漏掉的代價正好落在最需要它的人身上。`iteration-count` 要一起夾,否則 infinite 的 pulse 會用 0.01ms 的週期狂跑。
- 設定入口(齒輪按鈕):發音開關(存檔欄位仍叫 `settings.sound`,音效移除後它只管答對時的自動發音)。
- 關卡全數破完後顯示「更多關卡即將推出」畫面——關卡被玩完的速度比想像中快,別讓玩家點下一關時當掉。

Expand All @@ -216,7 +216,7 @@ function speak(word) {
- 提示 = 隨機揭示一格**未填**的格子,揭示後直接顯示字母並鎖定。不做「指定格子揭示」的進階提示——一種提示先驗證夠不夠用。
- 一個目標字的所有格子都被填滿(不論來自拼字還是提示)→ 即算找到:照常播動畫、計入勝利判定、轉盤拼它時提示「已找到」、可以點開查詞(含還沒學會查詞時的那句提示,UI 文件 §4-F)。
- 拼出「部分格子已被提示揭示」的字 → 照常整字填入,算玩家找到。
- 金幣不足 → 提示按鈕抖動並顯示價格,不彈任何購買視窗(IAP 是 Phase 3 的事)。
- 金幣不足 → 提示按鈕抖動並顯示價格,不彈任何購買視窗(IAP 是 Phase 3 的事)。金幣不足時**燈泡與價格文字平常就是暗的**(UI 文件 §2 有「為什麼只淡價格不夠」),按之前就看得出買不起;按鈕不停用、抖動照舊。

## 9. 狀態與進度儲存

Expand Down Expand Up @@ -423,6 +423,7 @@ export const bridge = {
- ❌ 遊戲引擎 —— 沒有 game loop 的回合制 UI。
- ❌ 即時對戰 / 排行榜 —— Phase 3 以後的事。
- ❌ 執行期產生關卡 —— 離線產生品質可控、可人工審核。
- ❌ 同一關排除單複數對(RAT/RATS)—— 2026-07-28 有玩家反應後評估,決定不做。實測 500 關:同關同時把 X 與 XS 當目標字的有 89 關、100 組(如 155 關 `TESTS,SETS,TEST,SET`);目標字以 S 結尾者佔 11%(2444 個中 265 個),但多數不是同關某目標的複數;**23% 的關卡基底字本身就是複數**(GRAMS、CROPS、ROCKS);其他衍生形(-ED/-ER/-Y/-LY)同關成對合計僅約 50 組。否決理由:(1) 全面禁複數會砍掉近四分之一的基底字候選,3 字母那帶本來就只有 20 出頭種組合可出題(§5 步驟 5);(2) 玩家抱怨的是同一關的重複感,不是複數這個詞類有問題——基底字是 GRAMS 的關卡沒有任何不對;(3) 真要做也**只能把複數退成 bonus 字、絕不能判無效**,§5 已寫明「玩家想得到的字被判無效是這類遊戲最大的差評來源」。改法本身很短(`buildLevel()` 挑目標時多一道過濾),但要重跑整批產生器,其中 8 關會因候選不足換掉基底字、band 游標一動就讓後面所有關卡跟著重排——500 個檔案全變、玩家手上那一關變成另一道題(存檔不會壞,`createGame` 對認不得的 `foundWords` 是容忍的)。翻案條件:哪天為別的理由本來就要重跑整批關卡,順手加上這道過濾的邊際成本趨近於零。
- ✅ **PWA / Service Worker —— 2026-07-22 翻案,已實作**(原文:「Phase 2 用 Capacitor 打包後天生離線;Web 版只是測試通路,不值得維護快取失效邏輯」)。翻案理由:Capacitor 的成本(一台 Mac、Apple $99/年、審查週期)讓 Phase 2 不再是近期必然,而「天生離線」正是靠它兌現的——沒有它,§2 列為硬需求的離線可玩在 Web 版根本沒成立過(斷網就是白屏)。快取失效那個顧慮則被策略解掉了,見 §2。
- ❌ 多種提示類型(指定格、開頭字母、火箭)—— 先驗證一種隨機揭示夠不夠。
- ❌ Bonus 字釋義、多義項/詞性/例句、發音腔調選擇(美式/英式)—— 目標字的單一短釋義 + 系統預設 TTS 就夠,見 §6。
Expand Down Expand Up @@ -485,6 +486,15 @@ Phase 1 沒有自動化 UI 測試(§12),這份清單就是驗收標準兼
- [ ] **VoiceOver 打開**:拖曳轉盤拼字期間**不會**逐字唸出 C→CA→CAT(`#preview` 不是 live region);但拼到已找到的字時要唸出「已找到」(`#status`)
- [x] 真機測過:iOS Safari + 一台中低階 Android(§12)

2026-07-28 的四項 UI 補丁(金幣回饋、桌機收邊、提示鈕買不起、重玩標記)——**都已在桌機 Chrome 上以實際尺寸驗過**,這裡列的是只有真機/VoiceOver 才驗得準的那半:

- [ ] 拼出 bonus 字、領取金幣 → 頂列金幣數字**彈一下**;按提示扣錢時**不彈**(只慶祝變多的,UI 文件 §2)。過關與兌換同樣有進帳,但當下有過關卡片/設定卡蓋著頂列,看不到彈跳是**預期行為**,不是漏播。彈跳是放大再回來、不是從縮小長出來(`bump`,不是格子字母的 `pop`)
- [ ] 金幣 < 25 時,提示鈕的**燈泡與「−25」都是暗的**(同一個 `--text-soft`),但底色、邊框、陰影跟平常一樣——要看起來像「現在買不起」,不是像「這顆壞了」;領取或拿到 bonus 湊回 25 以上 → 立刻恢復成珊瑚色(`updateCoins` 每次都重算)。按下去的抖動與「金幣不足」不變
- [ ] 從關卡選擇進**已完成的舊關** → 開場在拼字預覽列閃一次「重玩不再獲得金幣」(900ms 後自己消失),頂列關卡鈕**照常寫「第 N 關」**;破完那關的過關卡片仍寫同一句、金幣沒有增加。中途切到關卡選擇再回來會再閃一次(`startLevel` 每次都跑),這是預期行為
- [ ] **VoiceOver 打開**:提示鈕唸出「提示,花費 25 金幣」(不是光「提示」——鈕上看得見的「−25」被 `aria-label` 蓋掉了);進舊關時 `#status` 播報「重玩不再獲得金幣」(那句同時進畫面與 live region,`flashPreview` 的預設行為)
- [ ] **iPad/平板**(或桌機視窗拉寬):整個畫面收成一欄置中,頂列四顆鈕沒有各自貼到螢幕兩側;提示鈕仍貼著轉盤左下、不會飄到畫面邊緣。後期大盤面(第 300 關以後)在這個欄寬下格子會比以前小一點,但沒有溢出、也沒有壓到拼字預覽列
- [ ] 320px 寬的舊機(iPhone SE 一代)上,關卡編號進到三位數 + 領取鈕冷卻中 → 頂列會折行(**既有問題,不是這批改出來的**,見 UI 文件 §3)。這條只是記著,不擋驗收

Phase 1.5(PWA,§15)補的幾條——同樣只能手測,而且**只有正式站測得準**(`mise run serve` 是 http,SW 只在 localhost 例外生效,安裝到主畫面則完全測不到):

- [x] iOS 分享 →「加到主畫面」→ 從主畫面開:沒有網址列、狀態列是深色透明的、內容沒被瀏海壓到
Expand Down
Loading