From 7e2f689f266ee59391986979cbac32f2809daf52 Mon Sep 17 00:00:00 2001 From: JianMiau Date: Fri, 21 Aug 2026 16:48:43 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E9=96=8B=E5=95=9F=E8=BF=BD?= =?UTF-8?q?=E8=A6=96=E5=BE=8C=20renderer=20=E5=B4=A9=E6=BD=B0=EF=BC=8C?= =?UTF-8?q?=E8=BF=BD=E8=A6=96=E6=94=B9=E7=82=BA=E5=8F=AA=E5=9C=A8=E5=BE=85?= =?UTF-8?q?=E6=A9=9F=E6=99=82=E7=94=9F=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 摘要: startAnim 對 look 會丟 TypeError 導致動畫迴圈整個死掉;另把追視改成只在 idle 時覆蓋。 根本原因: 1. 追視是單一靜態擺姿,TIMINGS 裡沒有 look 這個動作。但序列模型改寫後, tick 在動畫改變時無條件呼叫 startAnim(anim),傳入 "look" 時會走到 cycleOf("look") → TIMINGS["look"].map → TypeError。 例外從 requestAnimationFrame 的 callback 拋出,迴圈就此中止, 整隻寵物凍結,也不再回應任何 hook 事件——就是使用者回報的「直接沒反應」。 2. 對照 Codex 時把 lookFrame 改成覆蓋所有狀態。但 Codex 的追視來源是 quick chat 的 文字游標,只有使用者真的在打字時才存在;滑鼠卻是一直都在,照搬會讓工作中的 running / waiting / review 動畫永遠被靜態擺姿蓋掉,寵物失去反映狀態的作用。 影響: 選單一勾「跟著游標看」,游標一動寵物就完全凍結,且不會恢復。 修法: - startAnim 開頭對 look 早退,直接設成空的幀序列(靜態擺姿), 不進入 cycleOf / baseSequence。draw 本來就單獨處理 look,tick 的推幀也已排除 look。 - resolveAnim 先取 baseState(),只有 base 為 idle 時才回傳 look; 一併避免原本重複呼叫 baseState() 的副作用。 - README 的特色、行為對照表、與 Codex 的對照三處同步更新, 對照表把「追視」拆成來源與優先序兩列並說明為何刻意不同。 不動的部分: hover 跳躍維持進入時播 3 次、之後停住不再跳(使用者確認就是要這個行為)。 另已確認跳躍結束後會回到當下的基礎狀態而非一律 idle。 驗證: - 單元測試(抽出實際程式碼執行):修正前 startAnim("look") 拋 TypeError: Cannot read properties of undefined (reading 'map'),修正後回傳空序列; idle 6 幀、running 24 幀正常;resolveAnim 在追視開啟下 idle→look、running→running、waiting→waiting、review→review, hover 跳躍優先於 look 與 running,跳躍結束回到 running 而非 idle,共 11 項全過。 - 實機開啟 followCursor 啟動:/state 持續回應且 anim 為 look(修正前此處即崩潰), log 無任何例外;灌入事件後 running / waiting 正確蓋過追視。 - 以 ELECTRON_RUN_AS_NODE 讀打包後 app.asar:版本 2.0.2、崩潰修正與追視優先序都在, hover 跳躍、矩形判定、右鍵選單未受影響;exe FileVersion 為 2.0.2。 Co-Authored-By: Claude Fable 5 --- README.md | 7 ++++--- package-lock.json | 4 ++-- package.json | 2 +- renderer/renderer.js | 15 ++++++++++++--- 4 files changed, 19 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index a7b1904..cd8a26b 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ - **完整實作 Codex V2 pet contract** — 8 欄 × 11 列、192×208 cell 的 spritesheet,9 個標準動作列 + 16 個看向方向,**每一幀的時長都照 Codex 規格**,動起來跟在 Codex app 裡一模一樣。 - **行為邏輯逐項對照 Codex** — 狀態優先序、各狀態到期時間、動畫時序、hover 版面、初次問候、拖曳方向判定……都是從 Codex app 的 `app.asar` 讀出來的數值(見[與 Codex 的對照](#與-codex-的對照))。 -- **16 方向追視(選配,預設關)** — 開啟後游標在哪她就看哪(000 = 正上方、順時針 22.5° 一格),追視時是靜態擺姿。Codex 的寵物**不看滑鼠**——它看的是 quick chat 的文字游標與 computer-use 的虛擬游標,所以預設關閉以求一致;喜歡的話在選單打開。 +- **16 方向追視(選配,預設關)** — 開啟後、且目前是待機狀態時,游標在哪她就看哪(000 = 正上方、順時針 22.5° 一格),追視時是靜態擺姿;Claude Code 在工作時以狀態動畫優先。Codex 的寵物**不看滑鼠**——它看的是 quick chat 的文字游標與 computer-use 的虛擬游標,所以預設關閉以求一致;喜歡的話在選單打開。 - **自動修正圖檔缺陷** — Codex 的 hatch 產生器有時會把整列 look 格子畫得比 idle 小(內附的小念第 10 列就小了 22%,游標一往下移人物就縮水)。以整列中位數偵測、用腳底當支點放大回正確身高;單格的姿勢差異不動。Codex 本身沒有做這件事。 - **與 Codex 同尺寸** — 100% 就是 Codex 桌面寵物的大小(126 × 137),高度用 Codex 的 `ceil(寬 × 208/192)` 算,兩邊並排看起來一樣大。 - **真正的桌面寵物** — 透明、無邊框、永遠置頂;透明區域**點穿**,只有壓在人物像素上滑鼠才會被吃掉,不擋你操作底下的視窗。 @@ -123,7 +123,7 @@ npm run autostart:install # (可選)開機自動啟動 | `SubagentStart` / `SubagentStop` | | `running` | | | `PreCompact` | | `running` | 🗜️ 整理記憶中… | | `SessionEnd` | | 移除該 session | 最後一個結束時 👋 掰掰 | -| (無事件) | 游標移動(追視開啟時) | 16 方向 `look`,覆蓋所有狀態 | | +| (無事件) | 游標移動(追視開啟時、且目前為 idle) | 16 方向 `look` | | | (無事件) | 拖曳 | 單次位移 ≥ 4 px 才決定 `running-left` / `running-right` | | | (無事件) | 游標進入人物方框 | `jumping` × 3 → 回原狀態 | | @@ -261,7 +261,8 @@ Claude Code ──hook(stdin JSON)──▶ hook/claude-pet-hook.js ──PO | `jumping` | 定義在 `respondToHover ? "jumping" : state`,但整個 app 沒人開啟那個 prop | **選擇打開**:游標進入人物方框就播 3 次 | | hover 版面 | 40 px 內進入、56 px 外退出;人物縮 0.9、下移 14 px、上方 24 px 控制鈕、200 ms | 未採用——控制鈕的功能就是選單,右鍵已經有了 | | hover 判定 | 用矩形(`pet-area-hover-changed` 由 rect 算) | 相同(逐像素會因每格輪廓不同而抖動) | -| 追視 | 看 quick chat 文字游標/computer-use 游標,死區 1 px,覆蓋所有狀態 | 沒有對應來源;改為**選配**看滑鼠,預設關 | +| 追視來源 | quick chat 的文字游標/computer-use 游標 | 沒有對應來源;改為**選配**看滑鼠,預設關 | +| 追視優先序 | `lookFrame` 覆蓋所有狀態 | 只在 idle 時追視。Codex 的文字游標只有你在打字時才存在,滑鼠卻是一直都在,照搬會讓工作中的動畫永遠被靜態擺姿蓋掉 | | 拖曳方向 | 單次取樣水平位移 ≥ 4 px | 相同 | | 拖曳放開 | 彈簧回彈(spring 220 / damping 26) | 無(直接停在放開處) | | 點擊 | 開 quick chat | 揮手(沒有對應功能) | diff --git a/package-lock.json b/package-lock.json index 7b2554d..5a2071e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "claude-pet", - "version": "2.0.1", + "version": "2.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "claude-pet", - "version": "2.0.1", + "version": "2.0.2", "devDependencies": { "electron": "^43.4.1", "electron-builder": "^26.0.12" diff --git a/package.json b/package.json index 93cd8b9..2458cae 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "claude-pet", - "version": "2.0.1", + "version": "2.0.2", "description": "Codex Pets 相容的 Claude Code 桌面寵物(spriteVersionNumber 2:9 個動作列 + 16 方向追視)", "main": "main.js", "private": true, diff --git a/renderer/renderer.js b/renderer/renderer.js index 07bc45e..84f7518 100644 --- a/renderer/renderer.js +++ b/renderer/renderer.js @@ -103,6 +103,12 @@ function baseSequence(anim) { const reducedMotionQuery = window.matchMedia("(prefers-reduced-motion: reduce)"); function startAnim(anim, now) { + // 追視是單一靜態擺姿,沒有幀序列(TIMINGS 裡沒有 look)。 + // 少了這道判斷會走進 cycleOf("look") 而丟 TypeError,requestAnimationFrame 迴圈整個死掉。 + if (anim === "look") { + player = { anim, frames: [], loopStart: null, step: 0, stepStart: now }; + return; + } let seq; if (reducedMotionQuery.matches) { // 與 Codex 相同:系統關閉動畫時只顯示該狀態的第一格 @@ -287,9 +293,12 @@ function resolveAnim() { // 一次性動作(點擊揮手、初次問候)。Codex 的 lookFrame 會蓋過這些,但它的追視來源不是滑鼠; // 滑鼠永遠在畫面上,若照搬會讓點擊揮手永遠看不到,所以這裡讓手勢優先。 if (overlay) return overlay.anim; - // Codex:lookFrame 一旦有值就覆蓋所有狀態,靜態擺姿 - if (followCursor && petInfo?.version === 2 && look.idx !== null) return "look"; - return baseState(); + const base = baseState(); + // Codex 的 lookFrame 會覆蓋所有狀態,但它的追視來源是 quick chat 的文字游標, + // 只有你真的在那裡打字時才存在。滑鼠則是一直都在,照搬會讓工作中的動畫永遠被靜態擺姿蓋掉, + // 寵物就失去反映 Claude Code 狀態的作用,所以只在 idle 時追視。 + if (base === "idle" && followCursor && petInfo?.version === 2 && look.idx !== null) return "look"; + return base; } function projectName(cwd) {