@mingxy/opencode-mascot 0.5.8 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,123 +1,147 @@
1
1
  # 🐱 opencode-mascot
2
2
 
3
- > OpenCode TUI 吉祥物插件框架让你的终端活起来
3
+ > OpenCode TUI mascot plugin framework bring your terminal to life
4
4
 
5
- 可自定义的 ASCII 吉祥物,在你的 OpenCode 终端里呼吸、走路、睡觉、被打飞、被炸碎,然后默默爬回来重新组装。
5
+ Customizable ASCII mascots that breathe, walk, sleep, get launched across the screen, blown up by falling bombs, then quietly reassemble themselves.
6
6
 
7
- ## 特色
7
+ [English](./README.md) | [简体中文](./README_zh-CN.md)
8
8
 
9
- ### 🎭 内置形象(2个)
9
+ ## 🏠 Home Page
10
10
 
11
- | 形象 | 描述 | 颜色 |
12
- |------|------|------|
13
- | **月儿** (yueer) | 紫发呆毛女孩,傲娇风格,默认形象 | `#8B7EB8` 淡紫 |
14
- | **包子** (baozi) | 热气腾腾的包子,温暖治愈 | `#D4885A` 暖橙 |
11
+ ![home demo](./assets/demo-home.gif)
15
12
 
16
- 每个形象包含 **5 种表情帧**:default / blink / happy / thinking / sleeping
13
+ ## 💼 Work Page
17
14
 
18
- ---
15
+ ![work demo](./assets/demo-work.gif)
19
16
 
20
- ### 🎬 自动动画(16种)
17
+ ---
21
18
 
22
- **Renderer 内置(所有形象共享):**
19
+ ## ✨ Features
23
20
 
24
- | # | 动画 | 触发 | 效果 |
25
- |---|------|------|------|
26
- | 1 | 眨眼 | 随机(30%概率/2.5s) | 切换 blink 帧 150ms |
27
- | 2 | 随机表情 | 每 8s | idle 时随机切换表情 |
28
- | 3 | 呼吸 | 每 3s | 行向上偏移一格,模拟呼吸起伏 |
29
- | 4 | 走路 | 每 20-40s | 左右晃动(14步路径) |
30
- | 5 | 跳跃 | 每 20-40s | 弹跳 -2→-1→0 |
31
- | 6 | 睡眠 | idle 90-120s | 自动闭眼 + 火星文 Zzz |
21
+ ### 🎭 Built-in Characters (3)
32
22
 
33
- **月儿专属(yueer effects):**
23
+ | Character | Description | Color |
24
+ |-----------|-------------|-------|
25
+ | **yueer** | Purple-haired girl with an ahoge, tsundere style, default mascot | `#8B7EB8` lavender |
26
+ | **baozi** | A steaming hot bun, warm and cozy | `#D4885A` warm orange |
27
+ | **cat** | Orange tabby cat — purring, kneading, tail-swishing | `#FFA500` orange |
34
28
 
35
- | # | 动画 | 触发 | 效果 |
36
- |---|------|------|------|
37
- | 7 | 呆毛闪烁 | 随机(25%/1.5s) | ☆ ↔ ★ |
38
- | 8 | 开心晃头 | happy 状态 | 脸左右摆 + 呆毛同步偏移 |
39
- | 9 | 思考跺脚 | thinking 状态 | 左脚固定 ║,右脚 ║↔_ 单脚跺 |
40
- | 10 | 思考变脸 | thinking 状态 | 6种表情轮换(o_o / O_O / >_o / o_< / ⊙_⊙ / ◔_◔) |
41
- | 11 | 火星文气泡 | busy/thinking | 12条上标火星文轮换 |
42
- | 12 | 拖拽扇手 | 拖拽中 | 手臂 ┃███┃ ↔ ╱███╲ |
43
- | 13 | 跳跃扇手 | 跳跃中 | 同上 |
29
+ Each character includes **5 expression frames**: default / blink / happy / thinking / sleeping
44
30
 
45
- **包子专属(baozi effects):**
31
+ ---
46
32
 
47
- | # | 动画 | 触发 | 效果 |
48
- |---|------|------|------|
49
- | 14 | 冒热气 | 持续 | 4种蒸汽图案轮换 |
50
- | 15 | 火星文气泡 | busy/thinking | 12条上标火星文轮换 |
51
- | 16 | 拖拽惊恐 | 拖拽中 | `( °□° )` |
33
+ ### 🎬 Automatic Animations (16)
34
+
35
+ **Built-in (shared by all characters):**
36
+
37
+ | # | Animation | Trigger | Effect |
38
+ |---|-----------|---------|--------|
39
+ | 1 | Blink | Random (30% / 2.5s) | Switch to blink frame for 150ms |
40
+ | 2 | Random expression | Every 8s | Cycles expressions while idle |
41
+ | 3 | Breathing | Every 3s | Lines shift up one row, simulating breathing |
42
+ | 4 | Walking | Every 20-40s | Sways left and right (14-step path) |
43
+ | 5 | Jumping | Every 20-40s | Bounces -2 → -1 → 0 |
44
+ | 6 | Sleep | Idle 90-120s | Auto-closed eyes + alien-text Zzz |
45
+
46
+ **yueer exclusive:**
47
+
48
+ | # | Animation | Trigger | Effect |
49
+ |---|-----------|---------|--------|
50
+ | 7 | Ahoge sparkle | Random (25% / 1.5s) | ☆ ↔ ★ |
51
+ | 8 | Happy head sway | happy state | Face sways left/right + ahoge synced |
52
+ | 9 | Thinking foot stomp | thinking state | Left foot fixed, right foot stomps ║↔_ |
53
+ | 10 | Thinking face shift | thinking state | 6 expressions rotate (o_o / O_O / >_o / o_< / ⊙_⊙ / ◔_◔) |
54
+ | 11 | Alien text bubble | busy/thinking | 12 alien-text phrases rotate |
55
+ | 12 | Drag arm flail | While dragging | Arms ┃███┃ ↔ ╱███╲ |
56
+ | 13 | Jump arm flail | While jumping | Same as above |
57
+
58
+ **baozi exclusive:**
59
+
60
+ | # | Animation | Trigger | Effect |
61
+ |---|-----------|---------|--------|
62
+ | 14 | Steam | Continuous | 4 steam patterns rotate |
63
+ | 15 | Alien text bubble | busy/thinking | 12 alien-text phrases rotate |
64
+ | 16 | Drag panic | While dragging | `( °□° )` |
65
+
66
+ **cat exclusive:**
67
+
68
+ | # | Animation | Trigger | Effect |
69
+ |---|-----------|---------|--------|
70
+ | 17 | Tail swish | Every 0.6s | Tail `|_)` ↔ `|~)` |
71
+ | 18 | Ear twitch | Random (20% / 2.5s) | Ear `/\` → `/╲` |
72
+ | 19 | Pupil dilate | Random (15% / 4s, idle) | Eyes `o.o` → `@.@` |
73
+ | 20 | Kneading | idle state | Paws alternate `/|| |\` ↔ `/| ||\` |
74
+ | 21 | Purr bubble | busy/thinking | 12 purr phrases rotate (purrr~/mrrrow~/nyaa~) |
75
+ | 22 | Drag bristle | While dragging | Ears bristle `/╲╱╲` + shocked face `>.<` |
52
76
 
53
77
  ---
54
78
 
55
- ### 🖱️ 交互能力(5种)
79
+ ### 🖱️ Interactions (5)
56
80
 
57
- | # | 操作 | 效果 |
58
- |---|------|------|
59
- | 1 | **Alt + 鼠标拖拽** | 自由移动吉祥物位置(首页 + 工作页) |
60
- | 2 | **双击切换** | 300ms 内双击 循环切换形象 |
61
- | 3 | **拖拽变色** | 拖拽时身体 8 种高亮色 100ms 快速闪换,松手定格 |
62
- | 4 | **拖拽火星文** | 拖拽时头顶粉色"放开我"火星文轮换(ᶠᵃⁿᵍ/ᵏᵃⁱ/ᵇᵘᶠᵃⁿᵍ...) |
63
- | 5 | **拖拽惊恐** | 拖拽时 `( °□° )` 表情 + 手臂疯狂扇动 |
81
+ | # | Action | Effect |
82
+ |---|--------|--------|
83
+ | 1 | **Alt + drag** | Freely move the mascot anywhere |
84
+ | 2 | **Double-click** | Cycle through characters (within 300ms) |
85
+ | 3 | **Drag color flash** | Body flashes through 8 highlight colors at 100ms, locks on release |
86
+ | 4 | **Drag alien text** | Pink "let go of me" alien text appears above head while dragging |
87
+ | 5 | **Drag panic** | `( °□° )` face + arms flailing while dragging |
64
88
 
65
89
  ---
66
90
 
67
- ### 🫣 躲猫猫系统(工作页)
91
+ ### 🫣 Peek-a-Boo System (Work Page)
68
92
 
69
- | # | 动作 | 效果 |
70
- |---|------|------|
71
- | 1 | 拖到左边缘 | 吉祥物藏起来,只露 2 |
72
- | 2 | 松手贴边 | 自动进入探头循环(每 1.2s 偷偷多露 2 格再缩回) |
73
- | 3 | 点击 / 开始工作 | 从边界滑回 + bounce 弹跳归位 |
93
+ | # | Action | Effect |
94
+ |---|--------|--------|
95
+ | 1 | Drag to left edge | Mascot hides, only 2 rows visible |
96
+ | 2 | Release at edge | Auto peek cycle (peeks 2 more rows every 1.2s, then retreats) |
97
+ | 3 | Click / start working | Slides back from edge + bounce |
74
98
 
75
99
  ---
76
100
 
77
- ### 💥 随机意外事件(3种)
101
+ ### 💥 Random Events (3)
78
102
 
79
- | # | 事件 | 触发 | 效果 |
80
- |---|------|------|------|
81
- | 1 | **摔坏** | 跳跃落地 40% / bounce 归位 50% | 行散开倒地 → 1.5s 后自动重组 |
82
- | 2 | **天降炸弹** | idle 时 10% 概率替代走路 | 引线 ✦/◌ 燃烧 + 倒计时 ³·→²·→¹· → 白闪爆炸 + `ᵇᵒᵒᵐ~` → 重组 |
83
- | 3 | **打散聚合** | 启动 / 切到工作页 | 行从随机位置 15 帧线性插值聚合归位 |
103
+ | # | Event | Trigger | Effect |
104
+ |---|-------|---------|--------|
105
+ | 1 | **Fall apart** | Jump landing 40% / bounce 50% | Lines scatter reassemble after 1.5s |
106
+ | 2 | **Bomb drop** | 10% chance replacing walk (idle) | Fuse burns ✦/◌ + countdown ³·→²·→¹· → white flash explosion + `ᵇᵒᵒᵐ~` → reassemble |
107
+ | 3 | **Scatter & assemble** | Startup / switch to work page | Lines start from random positions, 15-frame linear interpolation to home |
84
108
 
85
109
  ---
86
110
 
87
- ### 🔄 自动更新
111
+ ### 🔄 Auto-Update
88
112
 
89
- - 启动时检测 npm 最新版 → semver 比较 → `npm pack` 下载 → `tar` 解压覆盖
90
- - 文件锁防并发(30s 过期自动清理)
91
- - 同步更新 opencode 插件管理清单版本号,防止重启回滚
92
- - 更新成功后吉祥物跳跃庆祝 + 火星文版本号 `ᵘᵖ→⁰·⁵·¹`
113
+ - Checks npm latest version on startup → semver compare → `npm pack` download → `tar` extract overwrite
114
+ - File lock prevents concurrency (30s expiry auto-cleanup)
115
+ - Syncs opencode plugin manifest version to prevent rollback on restart
116
+ - On successful update: mascot jumps to celebrate + alien-text version number `ᵘᵖ→⁰·⁵·¹`
93
117
 
94
118
  ---
95
119
 
96
- ### 🎵 状态联动
120
+ ### 🎵 State Sync
97
121
 
98
- | 触发 | 效果 |
99
- |------|------|
100
- | session busy | 火星文气泡 + 8色高亮闪烁 |
101
- | session thinking | 跺脚 + 变脸 + 火星文气泡 |
102
- | session happy | 晃头庆祝 3s |
103
- | session idle 超时 | 自动睡觉 + 火星文 Zzz(`zᶻ...` → `zᶻᶻ...` → `zᶻᶻᶻ...`) |
104
- | 拖拽睡眠中 | 惊醒到 idle + 扇手惊恐 |
122
+ | Trigger | Effect |
123
+ |---------|--------|
124
+ | session busy | Alien text bubble + 8-color highlight flash |
125
+ | session thinking | Foot stomp + face shift + alien text |
126
+ | session happy | Head sway celebration 3s |
127
+ | session idle timeout | Auto-sleep + alien-text Zzz (`zᶻ...` → `zᶻᶻ...` → `zᶻᶻᶻ...`) |
128
+ | Drag while sleeping | Startles awake to idle + arm flail panic |
105
129
 
106
- > 默认所有状态使用月儿形象。可通过右键双击手动切换包子。
130
+ > All states default to yueer. Double-click to manually switch to baozi.
107
131
 
108
132
  ---
109
133
 
110
- ### 🚀 启动效果
134
+ ### 🚀 Startup Effects
111
135
 
112
- - 启动 2s 后头顶显示火星文版本号 `ᵛ⁰·⁵·¹`(3秒)
113
- - 首页吉祥物水平随机位置出现
114
- - 工作页首次对话 / `opencode -c` 时打散聚合动画
136
+ - Version number shown 2s after startup as alien-text `ᵛ⁰·⁵·¹` (3s duration)
137
+ - Home page mascot appears at random horizontal position
138
+ - Work page scatter-assemble animation on first message / `opencode -c`
115
139
 
116
140
  ---
117
141
 
118
- ## 📦 安装
142
+ ## 📦 Installation
119
143
 
120
- `~/.config/opencode/tui.json` 中添加插件:
144
+ Add to `~/.config/opencode/tui.json`:
121
145
 
122
146
  ```json
123
147
  {
@@ -125,51 +149,48 @@
125
149
  }
126
150
  ```
127
151
 
128
- 重启 opencode 即可。插件会自动更新到最新版。
152
+ Restart opencode. The plugin auto-updates to the latest version.
129
153
 
130
- ## 🛠️ 技术栈
154
+ ## 🛠️ Tech Stack
131
155
 
132
156
  - **TypeScript** ESM
133
- - **@opentui/solid** — SolidJS 响应式 TUI 渲染
134
- - **@opencode-ai/plugin** — OpenCode 插件 API
135
- - 零运行时依赖(peer dep only
136
- - TypeScript 类型检查通过
157
+ - **@opentui/solid** — SolidJS reactive TUI rendering
158
+ - **@opencode-ai/plugin** — OpenCode plugin API
159
+ - Zero runtime dependencies (peer dep only)
160
+ - TypeScript type-checked
137
161
 
138
- ## 📂 项目结构
162
+ ## 📂 Project Structure
139
163
 
140
164
  ```
141
165
  opencode-mascot/
142
- ├── tui.tsx # 插件入口,注册 slots + 启动逻辑
166
+ ├── tui.tsx # Plugin entry, registers slots + startup
143
167
  ├── src/
144
168
  │ ├── core/
145
- │ │ ├── types.ts # MascotPack / MascotState / Effect 类型
146
- │ │ ├── ascii-renderer.tsx # 核心渲染引擎(574行,16+ 动画)
147
- │ │ ├── mascot-loader.ts # 内置形象加载器
148
- │ │ ├── celebration-bus.ts # 模块级事件总线(celebrate/version/scatter)
149
- │ │ ├── updater.ts # npm 自动更新(pack + tar + 版本同步)
150
- │ │ └── logger.ts # 文件日志(~/.cache/opencode/logs/mascot.log)
169
+ │ │ ├── types.ts # MascotPack / MascotState / Effect types
170
+ │ │ ├── ascii-renderer.tsx # Core rendering engine (574 lines, 16+ animations)
171
+ │ │ ├── mascot-loader.ts # Built-in character loader
172
+ │ │ ├── celebration-bus.ts # Module-level event bus
173
+ │ │ ├── updater.ts # npm auto-update
174
+ │ │ └── logger.ts # File logger
151
175
  │ ├── components/
152
- │ │ ├── home-mascot.tsx # 首页吉祥物(随机位置 + translate 拖拽)
153
- │ │ └── sidebar-mascot.tsx # 工作页吉祥物(absolute 定位 + 躲猫猫)
176
+ │ │ ├── home-mascot.tsx # Home page mascot
177
+ │ │ └── sidebar-mascot.tsx # Work page mascot (peek-a-boo)
154
178
  │ └── builtins/
155
- │ ├── yueer/ # 月儿形象(frames + effects
156
- ├── frames.ts # 5 ASCII art
157
- └── index.ts # 专属动画(呆毛/晃头/跺脚/变脸/气泡/扇手)
158
- │ └── baozi/ # 包子形象(frames + effects)
159
- │ ├── frames.ts # 5 帧 ASCII art
160
- │ └── index.ts # 专属动画(蒸汽/气泡/惊恐)
179
+ │ ├── yueer/ # yueer (frames + effects)
180
+ │ ├── baozi/ # baozi (frames + effects)
181
+ │ └── cat/ # cat (frames + effects)
161
182
  ```
162
183
 
163
- ## 🎨 自定义形象
184
+ ## 🎨 Custom Characters
164
185
 
165
- 创建新的吉祥物只需定义一个 `MascotPack`:
186
+ Define a `MascotPack`:
166
187
 
167
188
  ```typescript
168
189
  import type { MascotPack } from "@mingxy/opencode-mascot/types";
169
190
 
170
191
  const myMascot: MascotPack = {
171
192
  name: "@mingxy/mascot-custom",
172
- displayName: "小猫",
193
+ displayName: "Kitty",
173
194
  version: "0.1.0",
174
195
  author: "you",
175
196
  description: "My custom mascot",
@@ -191,22 +212,22 @@ const myMascot: MascotPack = {
191
212
  };
192
213
  ```
193
214
 
194
- 所有内置动画(眨眼/呼吸/走路/跳跃/睡眠/拖拽/变色/炸弹/摔坏/重组)**自动生效**。
215
+ All built-in animations (blink/breath/walk/jump/sleep/drag/color-flash/bomb/fall-apart/reassemble) **work automatically**.
195
216
 
196
- ## 📊 能力统计
217
+ ## 📊 Capabilities
197
218
 
198
- | 类别 | 数量 |
199
- |------|------|
200
- | 内置形象 | 2 |
201
- | 表情帧 | 5 / 形象 |
202
- | 自动动画 | 16 |
203
- | 交互操作 | 5 |
204
- | 躲猫猫行为 | 3 |
205
- | 随机意外 | 3 |
206
- | 火星文气泡 | 2412/形象) |
207
- | 闪色颜色 | 8 |
208
- | 拖拽火星文 | 6 |
209
- | **总能力** | **33+** |
219
+ | Category | Count |
220
+ |----------|-------|
221
+ | Built-in characters | 3 |
222
+ | Expression frames | 5 / character |
223
+ | Auto animations | 22 |
224
+ | Interactions | 5 |
225
+ | Peek-a-boo behaviors | 3 |
226
+ | Random events | 3 |
227
+ | Alien text phrases | 24 (12/character) |
228
+ | Flash colors | 8 |
229
+ | Drag alien text | 6 |
230
+ | **Total** | **39+** |
210
231
 
211
232
  ## 📄 License
212
233
 
@@ -216,3 +237,4 @@ MIT © [mingxy](https://github.com/mengfanbo123)
216
237
 
217
238
  - [GitHub](https://github.com/mengfanbo123/opencode-mascot)
218
239
  - [npm](https://www.npmjs.com/package/@mingxy/opencode-mascot)
240
+ - [中文文档](./README_zh-CN.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mingxy/opencode-mascot",
3
- "version": "0.5.8",
3
+ "version": "0.6.0",
4
4
  "description": "OpenCode TUI mascot plugin framework - customizable ASCII mascots for your terminal",
5
5
  "author": "mingxy",
6
6
  "license": "MIT",
@@ -0,0 +1,48 @@
1
+ // 0123456789
2
+ const defaultFrame = [
3
+ " /\\_/\\ ",
4
+ " ( o.o ) ",
5
+ " > ^ < ",
6
+ " /| |\\ ",
7
+ "(_| |_) ",
8
+ ];
9
+
10
+ const blinkFrame = [
11
+ " /\\_/\\ ",
12
+ " ( -.- ) ",
13
+ " > ^ < ",
14
+ " /| |\\ ",
15
+ "(_| |_) ",
16
+ ];
17
+
18
+ const happyFrame = [
19
+ " /\\_/\\ ",
20
+ " ( ^ω^ ) ",
21
+ " > ω < ",
22
+ " /| |\\ ",
23
+ "(_| |_) ",
24
+ ];
25
+
26
+ const thinkingFrame = [
27
+ " /\\_/\\ ",
28
+ " ( o_o ) ",
29
+ " > ? < ",
30
+ " /| |\\ ",
31
+ "(_| |_) ",
32
+ ];
33
+
34
+ const sleepingFrame = [
35
+ " /\\_/\\ ",
36
+ " ( -.- ) ",
37
+ " > z < ",
38
+ " /| |\\ ",
39
+ "(_| |_) ",
40
+ ];
41
+
42
+ export const frames = {
43
+ default: defaultFrame,
44
+ blink: blinkFrame,
45
+ happy: happyFrame,
46
+ thinking: thinkingFrame,
47
+ sleeping: sleepingFrame,
48
+ };
@@ -0,0 +1,167 @@
1
+ import type { MascotPack, EffectRenderCtx } from "../../core/types";
2
+ import { frames } from "./frames";
3
+
4
+ const PURR_TEXTS = [
5
+ "ᵖᵘʳʳʳ~", "ᵐʳʳᵒʷ~", "ᵐʷᵃᵃ~", "ⁿʸᵃᵃ~",
6
+ "ᵖᵘʳʳ..", "ᵐʸᵃ~..", "ᶠᵘʳʳ~", "ᵐⁱᵃᵒ~",
7
+ "ᶜʰᵘʳʳ..", "ᵉᵘⁿ~..", "ˢʰᵖᵘʳʳ~", "ⁿʸᵒ~",
8
+ ];
9
+ const THINKING_FACES = ["o_o", "O_O", ">_o", "o_<", "⊙_⊙", "◑_◑"];
10
+
11
+ const catEffects: MascotPack["effects"] = {
12
+ signals: [
13
+ { name: "tailAlt", initial: false },
14
+ { name: "earTwitch", initial: false },
15
+ { name: "pupilWide", initial: false },
16
+ { name: "kneadAlt", initial: false },
17
+ { name: "purrIdx", initial: 0 },
18
+ { name: "thinkingFaceIdx", initial: 0 },
19
+ { name: "thinkingCountdown", initial: 0 },
20
+ ],
21
+
22
+ timers: [
23
+ {
24
+ interval: 600,
25
+ update(ctx) {
26
+ ctx.set("tailAlt", !(ctx.get("tailAlt") as boolean));
27
+ },
28
+ },
29
+ {
30
+ interval: 2500,
31
+ update(ctx) {
32
+ if (Math.random() < 0.2) {
33
+ ctx.set("earTwitch", true);
34
+ setTimeout(() => ctx.set("earTwitch", false), 250);
35
+ }
36
+ },
37
+ },
38
+ {
39
+ interval: 4000,
40
+ update(ctx) {
41
+ if (ctx.state === "idle" && Math.random() < 0.15) {
42
+ ctx.set("pupilWide", true);
43
+ setTimeout(() => ctx.set("pupilWide", false), 1000);
44
+ }
45
+ },
46
+ },
47
+ {
48
+ interval: 400,
49
+ update(ctx) {
50
+ if (ctx.state === "idle") {
51
+ ctx.set("kneadAlt", !(ctx.get("kneadAlt") as boolean));
52
+ }
53
+ },
54
+ },
55
+ {
56
+ interval: 2000,
57
+ update(ctx) {
58
+ if (ctx.state === "busy" || ctx.state === "thinking") {
59
+ ctx.set("purrIdx", ((ctx.get("purrIdx") as number) + 1) % PURR_TEXTS.length);
60
+ }
61
+ },
62
+ },
63
+ {
64
+ interval: 1000,
65
+ update(ctx) {
66
+ if (ctx.state === "thinking" || ctx.state === "busy") {
67
+ let cd = ctx.get("thinkingCountdown") as number;
68
+ if (!cd || cd <= 0) cd = 5 + Math.floor(Math.random() * 11);
69
+ cd--;
70
+ if (cd <= 0) {
71
+ ctx.set("thinkingFaceIdx", ((ctx.get("thinkingFaceIdx") as number) + 1) % THINKING_FACES.length);
72
+ }
73
+ ctx.set("thinkingCountdown", cd);
74
+ } else {
75
+ ctx.set("thinkingFaceIdx", 0);
76
+ ctx.set("thinkingCountdown", 0);
77
+ }
78
+ },
79
+ },
80
+ ],
81
+
82
+ render(lines: string[], ctx: EffectRenderCtx): string[] {
83
+ const { state, breathPhase, dragging, get } = ctx;
84
+ let result = [...lines];
85
+
86
+ const tailAlt = get("tailAlt") as boolean;
87
+ const earTwitch = get("earTwitch") as boolean;
88
+ const pupilWide = get("pupilWide") as boolean;
89
+ const kneadAlt = get("kneadAlt") as boolean;
90
+
91
+ if (dragging) {
92
+ result[0] = " /╲╱╲\\ ";
93
+ result[1] = " ( >.< ) ";
94
+ result[2] = " > ! < ";
95
+ result[3] = " /| |\\ ";
96
+ result[4] = "(_| |_) ";
97
+ return result;
98
+ }
99
+
100
+ if (!breathPhase) {
101
+ result = result.map((l) => l.replace(/\^/g, "'"));
102
+ }
103
+
104
+ if (earTwitch) {
105
+ result[0] = result[0].replace("/\\_/\\", "/╲_/\\");
106
+ }
107
+
108
+ if (tailAlt) {
109
+ result[4] = "(_| |~) ";
110
+ } else {
111
+ result[4] = "(_| |_) ";
112
+ }
113
+
114
+ if (pupilWide) {
115
+ const faceLine = result.findIndex((l) => /\(.*\)/.test(l));
116
+ if (faceLine >= 0) {
117
+ result[faceLine] = result[faceLine].replace("o.o", "@.@");
118
+ }
119
+ }
120
+
121
+ if (state === "idle") {
122
+ const armLine = result.findIndex((l) => l.includes("/| |\\"));
123
+ if (armLine >= 0) {
124
+ result[armLine] = kneadAlt ? " /|| |\\ " : " /| ||\\ ";
125
+ }
126
+ }
127
+
128
+ if (state === "thinking" || state === "busy") {
129
+ const faceIdx = get("thinkingFaceIdx") as number;
130
+ const faceLine = result.findIndex((l) => /\(.*\)/.test(l));
131
+ if (faceLine >= 0) {
132
+ result[faceLine] = result[faceLine].replace(/\(.*?\)/, `( ${THINKING_FACES[faceIdx]} )`);
133
+ }
134
+ }
135
+
136
+ if (state === "busy" || state === "thinking") {
137
+ const idx = get("purrIdx") as number;
138
+ const earLine = 0;
139
+ result[earLine] = result[earLine].trimEnd() + " " + PURR_TEXTS[idx];
140
+ }
141
+
142
+ return result;
143
+ },
144
+ };
145
+
146
+ export const catPack: MascotPack = {
147
+ name: "@mingxy/mascot-cat",
148
+ displayName: "小猫",
149
+ version: "0.1.0",
150
+ author: "mingxy",
151
+ description: "Orange tabby cat — purring, kneading, and knocking things off your terminal.",
152
+
153
+ frames,
154
+
155
+ colors: {
156
+ defaultFg: "#FFA500",
157
+ },
158
+
159
+ animations: {
160
+ blinkInterval: 2500,
161
+ blinkChance: 0.3,
162
+ expressionInterval: 8000,
163
+ idleTimeout: 90000,
164
+ },
165
+
166
+ effects: catEffects,
167
+ };
@@ -117,6 +117,7 @@ export function SidebarMascot(props: SidebarMascotProps): JSX.Element {
117
117
  const returnToView = () => {
118
118
  if (!hideSide) return;
119
119
  stopPeek();
120
+ stopReturn();
120
121
  const cw = getCw();
121
122
  const cur = posX();
122
123
  const targetX = hideSide === "left" ? 0 : Math.max(0, cw - MASCOT_WIDTH);
@@ -128,7 +129,10 @@ export function SidebarMascot(props: SidebarMascotProps): JSX.Element {
128
129
  setPosX(targetX);
129
130
  stopReturn();
130
131
  hideSide = null;
131
- renderers[currentName()].bounce();
132
+ const r = renderers[currentName()];
133
+ if (r.getState() === "idle") {
134
+ r.bounce();
135
+ }
132
136
  return;
133
137
  }
134
138
  setPosX(now + step);
@@ -46,6 +46,7 @@ function getFrameLines(pack: MascotPack, frameName: string): string[] {
46
46
 
47
47
  export function createAnimatedRenderer(pack: MascotPack): {
48
48
  element: () => JSX.Element;
49
+ getState: () => MascotState;
49
50
  setState: (s: MascotState) => void;
50
51
  toggleWalk: () => void;
51
52
  setDragging: (v: boolean) => void;
@@ -573,5 +574,5 @@ export function createAnimatedRenderer(pack: MascotPack): {
573
574
  }, 700);
574
575
  };
575
576
 
576
- return { element, setState, toggleWalk, setDragging, celebrateUpdate, bounce, showVersion, scatterIn, explode };
577
+ return { element, getState: currentState, setState, toggleWalk, setDragging, celebrateUpdate, bounce, showVersion, scatterIn, explode };
577
578
  }
@@ -1,10 +1,12 @@
1
1
  import type { MascotPack } from "./types"
2
2
  import { yueerPack } from "../builtins/yueer"
3
3
  import { baoziPack } from "../builtins/baozi"
4
+ import { catPack } from "../builtins/cat"
4
5
 
5
6
  const BUILTINS: Record<string, MascotPack> = {
6
7
  yueer: yueerPack,
7
8
  baozi: baoziPack,
9
+ cat: catPack,
8
10
  }
9
11
 
10
12
  const ALL_NAMES = Object.keys(BUILTINS)